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
12 changes: 12 additions & 0 deletions .changeset/12464-retire-page-kind-jsx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@object-ui/types': minor
'@object-ui/console': patch
---

The page kind `jsx` is retired from the page node, following `@objectstack/spec` 18 (objectstack-ai/objectstack#23638), which refuses it at parse and names `html` as its replacement (objectui#12464).

**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.
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('ADR-0080 preview harnesses — page-source query params', () => {
expect(pages.length).toBeGreaterThanOrEqual(4);
expect(pages.map((p) => `${p.file}:${p.name}:${p.kind}`)).toEqual(
expect.arrayContaining([
'sdui-jsx-preview.tsx:command_center:jsx',
'sdui-jsx-preview.tsx:command_center:html',
'sdui-tiers-preview.tsx:release_notes:html',
'sdui-tiers-preview.tsx:pipeline_react:react',
'sdui-workbench-preview.tsx:crm_workbench:react',
Expand Down
6 changes: 3 additions & 3 deletions apps/console/src/sdui-jsx-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* ADR-0080 browser preview: a kind:'jsx' page rendered through the real
/* ADR-0080 browser preview: a kind:'html' page rendered through the real
* PageRenderer (compile -> SchemaRenderer).
*
* ADR-0080 EXCEPTION — Tailwind in page source
Expand All @@ -12,7 +12,7 @@
*
* It renders fully styled HERE only because this harness file is itself inside
* the scanned `src` — precisely the "works only by coincidence" failure mode
* ADR-0065 names. This file is a renderer-PLUMBING preview (does the jsx tier
* ADR-0065 names. This file is a renderer-PLUMBING preview (does the html tier
* compile and mount the registered blocks), not an authoring example: DO NOT
* copy its source string into a page. The authoring example is
* `sdui-tiers-preview.tsx`, whose sources carry no className at all and style
Expand Down Expand Up @@ -84,7 +84,7 @@ const source = `
</grid>
</flex>`;

const page = { type: 'home', kind: 'jsx', name: 'command_center', label: 'Command Center', source };
const page = { type: 'home', kind: 'html', name: 'command_center', label: 'Command Center', source };

createRoot(document.getElementById('root')!).render(
<React.StrictMode>
Expand Down
4 changes: 2 additions & 2 deletions content/docs/guide/react-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ that are awkward to express as nested JSON:
| `"html"` | Constrained JSX/HTML | **No** — parsed into a schema tree | Untrusted OK |
| `"react"` | Real React (hooks, handlers, arbitrary JS) | **Yes** — in the main React tree | Trusted only |

Both set `source` and leave `regions` unused. `"jsx"` is a deprecated alias for
`"html"` and is still accepted.
Both set `source` and leave `regions` unused. The former alias `"jsx"` was retired
in `@objectstack/spec` 18 and is refused; author `"html"`.

> Page `kind` also carries the record-page override values `"full"` (default)
> and `"slotted"` — a different axis, covered in [Slotted Pages](./slotted-pages.md).
Expand Down
15 changes: 10 additions & 5 deletions packages/components/src/renderers/layout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -662,17 +662,22 @@ export const PageRenderer: React.FC<{

// Select the layout variant based on template or page type
const layoutElement = useMemo(() => {
// `PageSchema['kind']` now spells the source-authored values too, matching
// @objectstack/spec — no cast needed to read what the renderer dispatches on.
const kind = schema.kind;
// 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;
// `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. The legacy
// 'jsx' value is still accepted as a deprecated alias.
// (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.
// 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
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/__tests__/authoring-nodes-11364.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ describe('objectui#11364: the spec-declared nodes have a TypeScript authoring fa

it('a stored page document is typed under its page kind', () => {
const home: PageDocumentNode = { type: 'home', kind: 'html', name: 'landing', label: 'Landing', source: '<Page />' };
// @ts-expect-error `kind` is the spec's override mode (full | slotted | html | react | jsx)
// @ts-expect-error `kind` is the spec's override mode (full | slotted | html | react)
const badKind: PageDocumentNode = { type: 'home', kind: 'markdown', name: 'landing', label: 'Landing' };
// @ts-expect-error `type` is the page kind the registry dispatches on, so the node must carry it
const untyped: PageDocumentNode = { name: 'landing', label: 'Landing' };
Expand Down
51 changes: 51 additions & 0 deletions packages/types/src/__tests__/page-kind-jsx-retired-12464.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* 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.
*/

/**
* objectui#12464: `@objectstack/spec` 18 retired the page kind `jsx`
* (objectstack-ai/objectstack#23638), naming `html` as its replacement, so the
* SDUI page node no longer declares it.
*
* The assertions are TYPE-level: they are judged by this package's
* `type-check` (its `tsc -p tsconfig.test.json` leg), never by vitest, which
* strips types. A `@ts-expect-error` that stops being needed is TS2578, so the
* directive below fails in both directions.
*
* `Equal` (not `Extends`) on the whole union: re-adding `'jsx'` widens it, and
* only an exact comparison fails on a widening.
*
* The zod face (`zod/layout.zod.ts` `PageNodeSchema`) refuses the same value
* at `kind`: it picks its enum out of the spec's by reference, so it does not
* wait for the resolved spec to carry the retirement.
*/
import { describe, expect, it } from 'vitest';
import type { PageNodeSchema } from '../layout';
import { PageNodeSchema as PageNodeZod } from '../zod/layout.zod';

type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
type Assert<T extends true> = T;

type _KindIsTheSpecVocabulary = Assert<
Equal<NonNullable<PageNodeSchema['kind']>, 'full' | 'slotted' | 'html' | 'react'>
>;

describe('PageNodeSchema.kind no longer declares the retired page kind jsx (objectui#12464)', () => {
it('refuses kind jsx and accepts its replacement html', () => {
const html: PageNodeSchema = { type: 'page', kind: 'html', source: '<Page />' };
// @ts-expect-error `jsx` was retired from the page kind in @objectstack/spec 18; author `html`
const jsx: PageNodeSchema = { type: 'page', kind: 'jsx', source: '<Page />' };
expect([html.kind, jsx.kind]).toEqual(['html', 'jsx']);
});

it('the zod face refuses kind jsx at kind and accepts html', () => {
const jsx = PageNodeZod.safeParse({ type: 'page', kind: 'jsx', source: '<Page />' });
expect(jsx.success).toBe(false);
expect(jsx.error?.issues.map((i) => i.path.join('.'))).toEqual(['kind']);
expect(PageNodeZod.safeParse({ type: 'page', kind: 'html', source: '<Page />' }).success).toBe(true);
});
});
6 changes: 3 additions & 3 deletions packages/types/src/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1773,8 +1773,8 @@ export interface PageNodeSchema extends BaseSchema, Omit<SpecPage, (typeof PAGE_
* - `"html"`: constrained JSX, PARSED into a SchemaNode tree and
* rendered. Never executed — safe for untrusted authors. Styled with
* the blocks' own structured props (`<flex direction gap>`,
* `<grid columns>`) plus a JSON `style` object. `"jsx"` is a
* deprecated alias, still accepted.
* `<grid columns>`) plus a JSON `style` object. Its former alias
* `"jsx"` was retired in `@objectstack/spec` 18 and is refused here.
* - `"react"`: real React, transpiled and EVALUATED in the main tree.
* No sandbox; gated behind the `react-pages` host capability. Styled
* with inline `style` objects.
Expand All @@ -1792,7 +1792,7 @@ export interface PageNodeSchema extends BaseSchema, Omit<SpecPage, (typeof PAGE_
*
* @default 'full'
*/
kind?: 'full' | 'slotted' | 'html' | 'jsx' | 'react';
kind?: 'full' | 'slotted' | 'html' | 'react';
/**
* Slotted override map. Each slot accepts a single SchemaNode or an
* array (arrays are flattened into the slot position). Slots not
Expand Down
11 changes: 11 additions & 0 deletions packages/types/src/zod/layout.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,17 @@ export const PageNodeSchema = BaseSchema.extend(SpecPageFields.shape).extend({
icon: z.string().optional().describe('Page icon (Lucide icon name)'),
description: z.string().optional().describe('Page description'),
pageType: PageTypeSchema.optional().describe('Page type (record, home, app, utility)'),
// 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`.
kind: stripImportedDefaults(SpecPageSchema)
.shape.kind.unwrap()
.extract(['full', 'slotted', 'html', 'react'])
.optional()
.describe('Page override mode (full | slotted | html | react)'),
object: z.string().optional().describe('Bound object name (for record pages)'),
template: z.string().optional().describe('Layout template name'),
variables: z.array(PageVariableSchema).optional().describe('Local page state variables'),
Expand Down
Loading