From bc88318175dce3aa5368b067c52e41b1317c3efb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 13:09:51 +0000 Subject: [PATCH 1/3] feat(components): the page renderer no longer compiles kind jsx as html (objectui#12486) @objectstack/spec 18 retired the page kind jsx, and objectui now resolves 18.0.0-next.2, whose COMPILED_PAGE_KINDS is html alone and whose stored-row rehydration rewrites a stored jsx page to html. So the renderer's kind === 'jsx' arm goes, and the kind read is the authoring type again, with no widening annotation. A raw node that still carries jsx takes the template / page-type path a node with no kind takes. - page.tsx: the arm, the widened read and the getJsxManifest comment. - layout.zod.ts: comments that named jsx as live. - the app-shell requires-stamp server double reads the installed spec's COMPILED_PAGE_KINDS instead of a hand copy that still listed jsx. - a render pin: a jsx node paints nothing from its source and draws what an absent-kind node draws; html still compiles. Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude --- .changeset/12486-drop-page-kind-jsx-arm.md | 11 +++ ...eEditPage.pageRequiresStamp-11357.test.tsx | 13 ++- .../page-kind-jsx-not-compiled-12486.test.tsx | 96 +++++++++++++++++++ .../components/src/renderers/layout/page.tsx | 25 +++-- packages/types/src/zod/layout.zod.ts | 10 +- 5 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 .changeset/12486-drop-page-kind-jsx-arm.md create mode 100644 packages/components/src/renderers/layout/__tests__/page-kind-jsx-not-compiled-12486.test.tsx diff --git a/.changeset/12486-drop-page-kind-jsx-arm.md b/.changeset/12486-drop-page-kind-jsx-arm.md new file mode 100644 index 0000000000..9ab367aca6 --- /dev/null +++ b/.changeset/12486-drop-page-kind-jsx-arm.md @@ -0,0 +1,11 @@ +--- +'@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. + +This supersedes the objectui#12464 entry's paragraph that kept the renderer's `jsx` branch until the resolved spec carried the retirement. diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx index c88e2b65ec..1f61a0ee32 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx @@ -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 @@ -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 = { @@ -82,9 +89,11 @@ const server = vi.hoisted(() => { Kanban: 'plugin-kanban', }; type Doc = Record; + /** 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)) { diff --git a/packages/components/src/renderers/layout/__tests__/page-kind-jsx-not-compiled-12486.test.tsx b/packages/components/src/renderers/layout/__tests__/page-kind-jsx-not-compiled-12486.test.tsx new file mode 100644 index 0000000000..67d4d20cdc --- /dev/null +++ b/packages/components/src/renderers/layout/__tests__/page-kind-jsx-not-compiled-12486.test.tsx @@ -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; + +/** 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: `

${FROM_SOURCE}

`, + 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().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); + }); +}); diff --git a/packages/components/src/renderers/layout/page.tsx b/packages/components/src/renderers/layout/page.tsx index 521dc573d4..b2b9417532 100644 --- a/packages/components/src/renderers/layout/page.tsx +++ b/packages/components/src/renderers/layout/page.tsx @@ -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; @@ -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 ; } - // `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 // (``, ``) 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 diff --git a/packages/types/src/zod/layout.zod.ts b/packages/types/src/zod/layout.zod.ts index 53fdc88552..326af9b05a 100644 --- a/packages/types/src/zod/layout.zod.ts +++ b/packages/types/src/zod/layout.zod.ts @@ -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']) @@ -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 From 240a30f9e44e70df309a992cd6acec8de1fa3db1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 13:29:13 +0000 Subject: [PATCH 2/3] docs(changeset): drop the 12464 entry's paragraph this PR makes false (objectui#12486) The pending objectui#12464 changeset said the page renderer still renders a stored kind jsx page as html. This branch drops that arm, so the paragraph is deleted; this PR's own changeset states the new behaviour. Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude --- .changeset/12464-retire-page-kind-jsx.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.changeset/12464-retire-page-kind-jsx.md b/.changeset/12464-retire-page-kind-jsx.md index f22b77832c..ec4b3a919f 100644 --- a/.changeset/12464-retire-page-kind-jsx.md +++ b/.changeset/12464-retire-page-kind-jsx.md @@ -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. From ea56ec6f5c4aa2ead2cbef63048f26406299e7e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 13:30:23 +0000 Subject: [PATCH 3/3] docs(changeset): drop the 12486 entry's supersede sentence (objectui#12486) The objectui#12464 paragraph it pointed at was deleted in the previous commit, so the sentence named nothing. Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude --- .changeset/12486-drop-page-kind-jsx-arm.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.changeset/12486-drop-page-kind-jsx-arm.md b/.changeset/12486-drop-page-kind-jsx-arm.md index 9ab367aca6..fd5260dd7c 100644 --- a/.changeset/12486-drop-page-kind-jsx-arm.md +++ b/.changeset/12486-drop-page-kind-jsx-arm.md @@ -7,5 +7,3 @@ The page renderer no longer compiles a `kind: 'jsx'` page node as `html`. This c **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. - -This supersedes the objectui#12464 entry's paragraph that kept the renderer's `jsx` branch until the resolved spec carried the retirement.