Skip to content

Commit b777474

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-23593-checklist-anchors
Claude-Session: https://claude.ai/code/session_01ESFcvW3staenyBCGq7kcDH Co-authored-by: Claude <noreply@anthropic.com>
2 parents 832cfc6 + c5dd7fc commit b777474

52 files changed

Lines changed: 1148 additions & 736 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/metadata-protocol': patch
4+
'@objectstack/cli': patch
5+
'@objectstack/lint': patch
6+
'@objectstack/platform-objects': patch
7+
---
8+
9+
feat(spec)!: the page kind `jsx` is retired — an authored `kind: 'jsx'` is refused with the prescription naming `html`, and `page-kind-jsx-to-html` leaves the load path
10+
11+
Clause-②: yes (narrowing)
12+
13+
**BREAKING (authoring):** `jsx` was the first name of the constrained-JSX page tier (ADR-0080), which ADR-0081 renamed `html`. Through 17 it stayed a declared `PageSchema.kind` member and the loader rewrote an authored `kind: 'jsx'` to `html` with a notice. At 18 it is retired: `PageSchema.kind` declares it through `enumWithRetiredValues`, so it is refused by name at `os validate`, `os build`, the metadata save door and `defineStack`, so under `os lint` too when the config calls it: "Page `kind: 'jsx'` was removed in @objectstack/spec 18 (ADR-0087 D2) … Use `kind: 'html'`." Writing it in TypeScript no longer compiles either. The ADR-0087 D2 conversion `page-kind-jsx-to-html` is retired from the load path (`retiredAfter: '17.7.0'`), so the authoring funnel no longer rewrites the value. `COMPILED_PAGE_KINDS` is now `['html']`: `requires` is accepted on an `html` page only.
14+
15+
Migration:
16+
17+
- `kind: 'jsx'` → `kind: 'html'`. Change the value; the page's `source` is compiled and rendered exactly as before, and `requires` stays legal on it.
18+
- A script or integration that saves a page through the metadata API with `kind: 'jsx'` → send `kind: 'html'`.
19+
20+
`os migrate meta` does not list this edit: the rename belongs to protocol 11, below the migration floor, so make it by hand. The D3 entry `page-kind-jsx-retired` states it for upgraders.
21+
22+
Stored metadata needs no edit. Every stored-row rehydration seam replays the retired conversion, so a `sys_metadata` page row written as `jsx` keeps loading as an `html` page, and a built artifact whose declared protocol floor is at or below `17.7.0` is still converted at the artifact-ingestion door.
23+
24+
The readers that branched on the retired value dropped it: the metadata save door's html-page compile gate (`@objectstack/metadata-protocol`), the CLI's SDUI-manifest page count, and the lint page walk, `validateJsxPages` and the page-source styling rule (`@objectstack/lint`) now read `html` (and `react` where they did before) only.
25+
26+
`@objectstack/platform-objects` publishes the Studio page form's `source` help text in four locales (`en`, `zh-CN`, `ja-JP`, `es-ES`); it no longer names `jsx`.
27+
28+
<!-- adr-0087: registered page-kind-jsx-retired -->

‎content/docs/references/ui/page.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,10 @@ View filter rule
214214
| **interfaceConfig** | `{ source?: string; columns?: string[] \| object[]; sort?: object[]; filterBy?: object[]; … }` | optional | Interface-level page configuration (for Airtable-style interface pages) |
215215
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
216216
| **print** | `{ paperSize?: Enum<'A4' \| 'A5' \| 'Letter' \| 'Legal'>; orientation?: Enum<'portrait' \| 'landscape'>; margins?: object; repeatHeader?: boolean; … }` | optional | Print declaration — makes this page a print page, a document authored in the page's own blocks: paper size and orientation, margins in millimetres, the running header and footer (the page's own `header` / `footer` regions), page numbers and page-break hints, each mapped to print CSS. A print page is a `kind: 'full'` page of `type: 'record'`, `'home'` or `'app'` with its blocks in `regions`, and every block in it must come from the printable block subset: containers that draw every child (`page:section`, `page:card`, `page:footer`), field blocks (`record:details`, `record:highlights`), the child-record table `record:line_items`, `element:text`, `element:image`, `element:divider`, `element:definition-list`, `element:repeater`, `element:number` and `object-metric`; `os validate` / `os build` / `os lint` and the metadata save door refuse any other block inside it. Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, the declaration is validated but nothing applies it. A list view's print button is `allowPrinting`, not this. |
217-
| **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react' \| 'jsx'>` | optional (default: `"full"`) | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; the legacy value 'jsx' is a deprecated alias), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors; react = real-React source executed at render by the runtime (ADR-0081), styled by inline `style` with the same token colors; it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via the OS_PAGE_REACT=off env toggle. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). |
217+
| **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react'>` | optional (default: `"full"`) | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; ADR-0081 named it html), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors; react = real-React source executed at render by the runtime (ADR-0081), styled by inline `style` with the same token colors; it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via the OS_PAGE_REACT=off env toggle. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). |
218218
| **slots** | `{ header?: object \| object[]; actions?: object \| object[]; alerts?: object \| object[]; highlights?: object \| object[]; … }` | optional | Slot override map for slotted pages |
219-
| **source** | `string` | optional | Page source text. For kind==='html' (alias 'jsx') it is constrained JSX compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors. For kind==='react' it is real React/JSX executed at render by @object-ui/react-runtime (trusted tier), styled by inline `style` with the same token colors. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). Authoritative over `regions` in both. |
220-
| **requires** | `string[]` | optional | Plugin namespaces the page's source uses, derived from the source at save — omit it. The key exists only on a kind==='html' page (alias 'jsx'), the kinds whose source is compiled at save; on a 'react', 'full' or 'slotted' page — and a page that omits kind, which is 'full' — it is refused at parse. On a server that has the deployment's SDUI component manifest, saving an html page compiles its source and stores the namespaces it uses here; a written list that disagrees with the source is refused (422 INVALID_METADATA, page-requires-disagrees-with-source) — on a draft save it is kept until the draft's publish, which refuses it. At load, a stored page whose list names a plugin no component in that manifest carries is reported, page and plugin named, and is still served. A server with no manifest checks neither and says so once at boot. |
219+
| **source** | `string` | optional | Page source text. For kind==='html' it is constrained JSX compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors. For kind==='react' it is real React/JSX executed at render by @object-ui/react-runtime (trusted tier), styled by inline `style` with the same token colors. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). Authoritative over `regions` in both. |
220+
| **requires** | `string[]` | optional | Plugin namespaces the page's source uses, derived from the source at save — omit it. The key exists only on a kind==='html' page, the kind whose source is compiled at save; on a 'react', 'full' or 'slotted' page — and a page that omits kind, which is 'full' — it is refused at parse. On a server that has the deployment's SDUI component manifest, saving an html page compiles its source and stores the namespaces it uses here; a written list that disagrees with the source is refused (422 INVALID_METADATA, page-requires-disagrees-with-source) — on a draft save it is kept until the draft's publish, which refuses it. At load, a stored page whose list names a plugin no component in that manifest carries is reported, page and plugin named, and is still served. A server with no manifest checks neither and says so once at boot. |
221221
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
222222
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
223223
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |

‎content/docs/ui/pages.mdx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const homePage = {
6262
| `type` | `enum` | optional | Page type (see below; default `'record'`) |
6363
| `object` | `string` | optional | Associated object (for `record` type) |
6464
| `template` | `string` | optional | Layout template name (default: `'default'`) |
65-
| `kind` | `enum` | optional | Page override mode (default `'full'`). `full` \| `slotted` = structured authoring; `html` = author-written constrained JSX/HTML compiled (parsed, never executed) to the tree (ADR-0080; the legacy value `'jsx'` is a deprecated alias); `react` = real-React source executed at render by the runtime (ADR-0081) — it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via `OS_PAGE_REACT=off`. See [Authoring modes](#authoring-modes) |
65+
| `kind` | `enum` | optional | Page override mode (default `'full'`). `full` \| `slotted` = structured authoring; `html` = author-written constrained JSX/HTML compiled (parsed, never executed) to the tree (ADR-0080; ADR-0081 named it `html`); `react` = real-React source executed at render by the runtime (ADR-0081) — it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via `OS_PAGE_REACT=off`. See [Authoring modes](#authoring-modes) |
6666
| `regions` | `PageRegion[]` | optional | Layout regions with components (default `[]` — `list` pages render via `interfaceConfig`, and an empty `record`/`home`/`app` page falls back to the synthesized default layout) |
6767
| `source` | `string` | optional | Page source text — required (and authoritative over `regions`) when `kind` is `'html'` or `'react'`. For `kind: 'html'` it is constrained JSX/HTML compiled to the tree by `@objectstack/sdui-parser` at save time (parse, never execute). For `kind: 'react'` it is real React/JSX executed at render by `@object-ui/react-runtime` (trusted tier). See [React Pages](/docs/ui/react-pages) |
6868
| `variables` | `PageVariable[]` | optional | Local state variables |
@@ -93,10 +93,12 @@ this page describes the first one:
9393

9494
The two source tiers set `source` instead of `regions`. **`source` is authoritative over
9595
`regions`**: when both are present the source wins, and `regions` holds at most a derived
96-
cache of it. A page whose `kind` is `html`, `react` or `jsx` with no non-empty `source`
96+
cache of it. A page whose `kind` is `html` or `react` with no non-empty `source`
9797
is rejected by the schema rather than rendering empty.
9898

99-
`'jsx'` is a deprecated alias for `'html'`, still accepted and converted at load time.
99+
`'jsx'`, the tier's first name, was retired in `@objectstack/spec` 18: an authored
100+
`kind: 'jsx'` is refused with the prescription naming `'html'`. Stored pages written as
101+
`'jsx'` keep loading as `html` pages.
100102

101103
Because `kind: 'react'` executes author JavaScript, it is gated by a host capability that
102104
defaults **ON** and is disabled server-side per deployment with `OS_PAGE_REACT=off`. The

‎content/docs/ui/react-pages.mdx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ string** instead, for layouts and interactions the fixed schema cannot express.
1515

1616
Both set `source` and leave `regions` unused. `source` is **authoritative over
1717
`regions`** on both tiers, and a page with either `kind` and no non-empty `source` is
18-
rejected by the schema rather than rendering empty. `'jsx'` is a deprecated alias for
19-
`'html'` that is still accepted and converted at load.
18+
rejected by the schema rather than rendering empty. `'jsx'`, the first name of the
19+
`'html'` tier, was retired in `@objectstack/spec` 18 and is refused with the prescription
20+
naming `'html'`.
2021

2122
## Choosing between `react` and `html`
2223

‎docs/qa/platform-checklist/areas/api-backend.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2258,7 +2258,7 @@
22582258
"source": [
22592259
"packages/spec/liveness/batch_endpoints.json (the ADR-0049 ledger this item answers: maxBatchSize, enableBatchEndpoint and three operations.* live, upsertMany and defaultAtomic dead)",
22602260
"packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema (declared bounds 1..1000 int, default 200, and the two retiredKey tombstones)",
2261-
"packages/rest/src/rest-server.ts#registerBatchEndpoints (the four per-object gates, each ANDed with its protocol member, and the unconditionally-mounted cross-object door), (enforceBatchSize — the one envelope), (normalizeConfig), (parseDeclaredSubConfig)",
2261+
"packages/rest/src/batch-routes.ts#registerBatchEndpoints (the four per-object gates, each ANDed with its protocol member, and the unconditionally-mounted cross-object door) + packages/rest/src/rest-server.ts#enforceBatchSize (enforceBatchSize — the one envelope), (normalizeConfig), (parseDeclaredSubConfig)",
22622262
"packages/spec/src/api/error-code-ledger.zod.ts#BATCH_TOO_LARGE (the code the cap answers with)",
22632263
"#3939 (one cap, one envelope, every door) · #11984 (maxBatchSize 0 was the live cap under the old cast) · #14691 (the two retirements) · ADR-0119 D4 (per-request atomicity)",
22642264
"sibling clause: api-backend.bulk-write-contract and api-backend.batch-transactional-discovery (they drive the doors' BEHAVIOUR on the default config; this item covers the config that decides the doors exist)"

‎packages/cli/src/utils/i18n-extract.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,7 @@ function walkObjectSections(config: any, out: ExpectedEntry[]): void {
965965
// live inside the untyped `properties` bag at the positions spec's
966966
// `pageComponentSlotPositions()` derives (`page:tabs` →
967967
// `properties.items[].children`, `page:card` → `properties.footer`, every
968-
// container → `properties.children`), and source-authored pages (`kind: 'html' | 'react' | 'jsx'`) hold only a
968+
// container → `properties.children`), and source-authored pages (`kind: 'html' | 'react'`) hold only a
969969
// DERIVED region cache that the author never wrote — scaffolding translation
970970
// keys off that cache would invent an authoring surface.
971971
//
@@ -1152,7 +1152,7 @@ export function authorWarnedTranslationGroups(): ReadonlySet<string> {
11521152
* derived slot list, and `page:card`'s `properties.body` / `.footer` were the
11531153
* last positions lint walked and the resolver did not (`slots.<slot>` roots
11541154
* and `properties.items[].children` closed at #16772) — but it is still
1155-
* NARROWER in one way (it skips `kind: 'html' | 'react' | 'jsx'` pages, which
1155+
* NARROWER in one way (it skips `kind: 'html' | 'react'` pages, which
11561156
* `translatePage` walks), and a mismatch in either direction is one half of
11571157
* the failure pair `PAGE_COMPONENT_COPY_KEYS`' own JSDoc names.
11581158
*/

‎packages/cli/src/utils/sdui-manifest.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ describe('countJsxGatePages — the pages the JSX gate checks against a manifest
273273
it('counts the name-keyed map shape too, and ignores non-records', () => {
274274
expect(
275275
countJsxGatePages({
276-
pages: { a: { kind: 'html', source: '<div />' }, b: { kind: 'jsx', source: '<div />' }, c: 'x', d: null },
276+
pages: { a: { kind: 'html', source: '<div />' }, b: { kind: 'html', source: '<div />' }, c: 'x', d: null },
277277
}),
278278
).toBe(2);
279279
expect(countJsxGatePages({ pages: 'nope' })).toBe(0);
@@ -350,7 +350,7 @@ describe('resolveJsxGateManifest — one decision, three commands', () => {
350350

351351
it('absent with a page to check: parse level, and ONE notice carrying the count and every place looked', () => {
352352
const r = resolveJsxGateManifest(
353-
{ pages: [...HTML_STACK.pages, { name: 'b', kind: 'jsx', source: '<div />' }] },
353+
{ pages: [...HTML_STACK.pages, { name: 'b', kind: 'html', source: '<div />' }] },
354354
PROJECT_DIR,
355355
ABSENT,
356356
);

‎packages/cli/src/utils/sdui-manifest.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export function jsxGateStacks(stack: AnyRec): AnyRec[] {
293293

294294
/**
295295
* The pages one stack's JSX gate checks AGAINST a manifest: `kind: 'html'`
296-
* (and its deprecated alias `'jsx'`) with a non-empty `source`, over the
296+
* with a non-empty `source`, over the
297297
* collection authored either as an array or as a name-keyed map — each keyed
298298
* by the name the gate reports it under (`page "NAME"`; a map entry's key when
299299
* the entry carries no `name`), or by the record itself when it has neither.
@@ -314,7 +314,7 @@ function checkedPageKeys(stack: AnyRec): unknown[] {
314314
const keys: unknown[] = [];
315315
for (const [mapKey, page] of entries) {
316316
if (!isRecord(page)) continue;
317-
if (page.kind !== 'html' && page.kind !== 'jsx') continue;
317+
if (page.kind !== 'html') continue;
318318
if (typeof page.source !== 'string' || page.source.trim() === '') continue;
319319
const name = typeof page.name === 'string' && page.name !== '' ? page.name : mapKey;
320320
keys.push(name === undefined ? page : `page:${name}`);

0 commit comments

Comments
 (0)