Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .changeset/12464-retire-page-kind-jsx.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,4 @@ The page kind `jsx` is retired from the page node, following `@objectstack/spec`

**Breaking (levelled `minor`, per this repo's version policy): `PageNodeSchema.kind` narrows.** The exported `PageNodeSchema` type's `kind` is now `'full' | 'slotted' | 'html' | 'react'`; a page node literal typed with it no longer compiles with `kind: 'jsx'`. Author `kind: 'html'` instead: the page's `source` is compiled and rendered exactly as before. The zod face (`PageNodeSchema` from the package's zod entry) refuses `kind: 'jsx'` at `kind` the same way, so `objectui validate` and `objectui check` report it.

Unchanged: `@object-ui/components`' page renderer still renders a stored `kind: 'jsx'` page as `html`. A server on a spec release built before the retirement can still serve such a row, so that branch is dropped only once the resolved `@objectstack/spec` carries the retirement.

The console's `sdui-jsx-preview` sample page now authors `kind: 'html'`, and the React Pages guide no longer presents `jsx` as an accepted alias.
9 changes: 9 additions & 0 deletions .changeset/12486-drop-page-kind-jsx-arm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@object-ui/components': minor
---

The page renderer no longer compiles a `kind: 'jsx'` page node as `html`. This completes the retirement of the page kind `jsx` that `@objectstack/spec` 18 made (objectstack-ai/objectstack#23638) and objectui#12464 began (objectui#12486).

**Breaking (levelled `minor`, per this repo's version policy): the page renderer narrows.** `PageRenderer` used to compile a node's `source` when its `kind` was `html` or `jsx`. It now compiles it for `html` only. A raw page node that still carries `kind: 'jsx'` loses its compiled `source`: it takes the template / page-type layout path that a node with no `kind` takes, so it draws its `children` or `regions` and never reads `source`. Such a node can only come from code that builds it past the types, which have refused `kind: 'jsx'` since objectui#12464. Author `kind: 'html'` instead; the source compiles and renders exactly as before.

A page served by an `@objectstack/spec` 18 server cannot carry `jsx`, so served pages are unaffected. `18.0.0-next.2`, which objectui now resolves, has `COMPILED_PAGE_KINDS` of `html` alone and refuses `jsx` at parse, and spec 18's stored-row rehydration rewrites a stored `jsx` page to `html` before it is served.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
* (`422 INVALID_METADATA`, the disagreeing namespace named), and otherwise
* promotes the draft with the same stamp.
* The "compile" maps each JSX tag to the namespace of the manifest component
* it names. Only that mapping is invented here; the two rules are the door's.
* it names. Only that mapping is invented here; the two rules are the door's,
* and so is the set of kinds they compile: the double reads the installed
* `@objectstack/spec`'s `COMPILED_PAGE_KINDS` rather than copying it, so it
* follows the spec when that set moves (spec 18 dropped the retired `jsx`,
* objectui#12486).
*
* Everything on the client side is shipping code: the page resource config
* registered at load, `MetadataResourceEditPage` with its Save and Publish
Expand Down Expand Up @@ -72,6 +76,9 @@ const PUBLISHED_HTML_PAGE: Row = {

const key = (type: string, name: string) => `${type}/${name}`;

/** The page kinds the save door compiles, from the installed spec (hoisted with the double that reads it). */
const { COMPILED_PAGE_KINDS } = await vi.hoisted(async () => await import('@objectstack/spec/ui'));

const server = vi.hoisted(() => {
/** The manifest's answer per tag: intrinsic tags are `ui`, `Kanban` is the plugin's. */
const NAMESPACE_OF: Record<string, string> = {
Expand All @@ -82,9 +89,11 @@ const server = vi.hoisted(() => {
Kanban: 'plugin-kanban',
};
type Doc = Record<string, unknown>;
/** The kinds the door compiles, read as a list a stored `kind` of any value can be looked up in. */
const compiledKinds: readonly unknown[] = COMPILED_PAGE_KINDS;
/** The compiled namespaces of an html page's source, in first-use order; `null` when it is not compiled. */
const compiledRequires = (body: Doc): string[] | null => {
if (body.kind !== 'html' && body.kind !== 'jsx') return null;
if (!compiledKinds.includes(body.kind)) return null;
if (typeof body.source !== 'string' || body.source.trim() === '') return null;
const used: string[] = [];
for (const m of body.source.matchAll(/<([A-Za-z][\w-]*)/g)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* The page renderer no longer compiles a `kind: 'jsx'` node as html
* (objectui#12486, half 2 of objectui#12464).
*
* ## What was retired
*
* `jsx` was the first name of the constrained-JSX page tier, which ADR-0081
* renamed `html`. `@objectstack/spec` 18 retired it: the page parse refuses it
* by name with a prescription naming `html`, stored-row rehydration rewrites a
* stored `jsx` page to `html` before it is served, and `COMPILED_PAGE_KINDS` is
* `html` alone. objectui#12464 narrowed the page node's `kind` to match, and
* kept the renderer's `kind === 'jsx'` arm only until the resolved spec carried
* the retirement. It does now, so the arm is gone.
*
* A raw node that still carries `kind: 'jsx'` (one a host builds in code past
* the types, since the authoring face refuses the value) is therefore a kind the
* renderer does not name. It takes the path every such kind takes, the
* template / page-type layout, exactly as a node with no `kind` does: its
* `source` is not read. ⛔ No arm names `jsx`, neither to honour it nor to
* refuse it; the refusal lives at the parse.
*
* ## The rows
*
* - `subject`: a `jsx` node whose `source` compiles cleanly as html paints
* nothing from that source. Red while the arm exists.
* - `same path`: the `jsx` node draws exactly what the same node with no `kind`
* draws, its `children` included. Red while the arm exists (the arm draws the
* compiled source and ignores `children`).
* - `control`: the same node as `kind: 'html'` paints its source. Green in both
* worlds; it is what keeps `subject` from passing on a renderer that compiles
* nothing at all.
*
* ⛔ No row pins message prose.
*/

import { describe, it, expect, afterEach } from 'vitest';
import { render, cleanup } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
// Registered at module scope, NOT in a hook: a cold transform billed to
// `hookTimeout` is narrower than the timeout it replaces (objectui#3010).
import '../../../renderers';

afterEach(() => cleanup());

type Node = Record<string, unknown>;

/** A heading only the compiled `source` can produce. */
const FROM_SOURCE = 'Painted from source';
/** A heading only the page-type layout's `children` can produce. */
const FROM_CHILDREN = 'Painted from children';

/** A page node as a host hands it: `source` that compiles as html, and `children`. */
const page = (kind?: string): Node => ({
type: 'home',
name: 'retired_kind_page',
label: 'Retired kind page',
...(kind === undefined ? {} : { kind }),
source: `<flex><h1>${FROM_SOURCE}</h1></flex>`,
children: [{ type: 'text', content: FROM_CHILDREN }],
});

/** Render one node through the registered page renderer; return its root. */
function paint(node: Node): HTMLElement {
const C = ComponentRegistry.get('home', 'ui');
if (!C) throw new Error('no renderer registered for ui:home');
return render(<C schema={node} />).container;
}

describe('the page renderer does not compile kind jsx as html (objectui#12486)', () => {
it('subject: a jsx node paints nothing from its source', () => {
const root = paint(page('jsx'));
expect(root.textContent).not.toContain(FROM_SOURCE);
});

it('same path: a jsx node draws what the same node with no kind draws', () => {
const absent = paint(page()).innerHTML;
cleanup();
const jsx = paint(page('jsx'));
expect(jsx.textContent).toContain(FROM_CHILDREN);
expect(jsx.innerHTML).toBe(absent);
});

it('control: the same node as kind html compiles its source', () => {
const root = paint(page('html'));
expect(root.textContent).toContain(FROM_SOURCE);
expect(root.textContent).not.toContain(FROM_CHILDREN);
});
});
25 changes: 12 additions & 13 deletions packages/components/src/renderers/layout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ const UtilityPageLayout: React.FC<{ schema: PageNodeSchema }> = ({ schema }) =>
};

// ---------------------------------------------------------------------------
// ADR-0080: a `kind:'jsx'` page carries a constrained JSX `source`. We compile
// ADR-0080: a `kind:'html'` page carries a constrained JSX `source`. We compile
// it (parse, never execute) into a SchemaNode tree and render it. The whitelist
// / contract is the live registry. Cached; rebuilt when the registry grows.
let _jsxManifestSig = -1;
Expand Down Expand Up @@ -662,29 +662,28 @@ export const PageRenderer: React.FC<{

// Select the layout variant based on template or page type
const layoutElement = useMemo(() => {
// Read as the stored WIRE value, not as the authoring type. `PageNodeSchema['kind']`
// follows @objectstack/spec 18, which retired `'jsx'` (objectui#12464); a
// server still on a spec release built before that retirement can serve a
// stored `jsx` page that spec 18's rehydration would have turned into
// `html`. Widening (not a cast) keeps the `'jsx'` arm below compiling until
// the resolved spec carries the retirement and that arm is dropped.
const kind: string | undefined = schema.kind;
// `PageNodeSchema['kind']` is the spec's own page-kind vocabulary, so this
// reads the authoring type: no cast, no widening. @objectstack/spec 18
// retired the tier's first name, `jsx` (objectui#12486): the spec refuses
// it at parse, a stored `jsx` row is rehydrated to `html` before it is
// served, and `COMPILED_PAGE_KINDS` is `html` alone. So nothing here reads
// `jsx`; a raw node that still carries it is a kind this renderer does not
// name, and takes the template / page-type path below like any other.
const kind = schema.kind;
// `kind:'react'` — TRUSTED execution tier: real React, run (not parsed) by
// @object-ui/react-runtime, gated behind the host CAP_REACT_PAGES flag.
if (kind === 'react') {
return <ReactKindPage schema={schema} />;
}
// `kind:'html'` (formerly 'jsx') — author-written constrained JSX compiled
// (parsed, never executed) to a SchemaNode tree and rendered. A stored
// 'jsx' page is still rendered as html until the resolved spec carries its
// retirement; it can no longer be authored against the types.
// `kind:'html'` — author-written constrained JSX compiled (parsed, never
// executed) to a SchemaNode tree and rendered.
// Styling on this tier is the blocks' own structured props
// (`<flex direction gap>`, `<grid columns>`) plus a JSON `style` object —
// NOT Tailwind utility classes. `source` is runtime metadata, so the build's
// Tailwind scan never sees it and an authored class produces no CSS and no
// error anywhere; `os validate` reports `page-source-className-tailwind`.
// (ADR-0065; ADR-0080's 2026-06-30 amendment.)
if (kind === 'html' || kind === 'jsx') {
if (kind === 'html') {
const src = (schema as { source?: string }).source ?? '';
const { tree, diagnostics } = compile(src, getJsxManifest());
const errors = diagnostics
Expand Down
10 changes: 5 additions & 5 deletions packages/types/src/zod/layout.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,9 +1298,9 @@ export const PageNodeSchema = BaseSchema.extend(SpecPageFields.shape).extend({
// objectui#12464: the page kind `jsx` retired in @objectstack/spec 18
// (objectstack-ai/objectstack#23638), naming `html`. Picked out of the spec's
// own enum by reference (`.extract`), the way `PAGE_KIND_NODE_TYPE` is below:
// identical to the spec's enum once the resolved spec carries the
// retirement, it refuses `jsx` before that, and a kind the spec drops fails to
// compile here. Mirrors the TS face's `PageNodeSchema.kind`.
// the resolved spec carries the retirement, so this extracts exactly the
// spec's members, and a kind the spec drops fails to compile here. Mirrors
// the TS face's `PageNodeSchema.kind`.
kind: stripImportedDefaults(SpecPageSchema)
.shape.kind.unwrap()
.extract(['full', 'slotted', 'html', 'react'])
Expand Down Expand Up @@ -1348,11 +1348,11 @@ export const PageNodeSchema = BaseSchema.extend(SpecPageFields.shape).extend({
// ⭐ THE SPEC'S OBJECT-LEVEL CHECKS, re-attached (objectui#7715, ruling B1).
// {@link SpecPageFields} rebuilds a fresh object from the spec's `.shape`, so
// it drops the two checks the spec's `PageSchema` carries on the OBJECT:
// - an `html` / `react` / `jsx` page with no non-empty `source` renders
// - an `html` / `react` page with no non-empty `source` renders
// nothing and is refused at `source` (`checkPageSourceCompleteness`,
// objectstack#16489);
// - `requires` is refused at `requires` on a page whose `kind` the platform
// does not compile at save — anything but `html` / `jsx`, an absent `kind`
// does not compile at save — anything but `html`, an absent `kind`
// included (`checkPageRequiresKind`, objectstack#21459, the spec's 17.7.0);
// - a page that authors both `slots.details` and `slots.tabs` is refused at
// `slots.details`, because the `tabs` slot replaces the strip the details
Expand Down
Loading