diff --git a/.changeset/25712-renderer-runs-one-sdui-parser.md b/.changeset/25712-renderer-runs-one-sdui-parser.md new file mode 100644 index 00000000000..3a7068a98cb --- /dev/null +++ b/.changeset/25712-renderer-runs-one-sdui-parser.md @@ -0,0 +1,30 @@ +--- +'@object-ui/components': patch +'@object-ui/console': patch +'@object-ui/layout': patch +'@object-ui/plugin-calendar': patch +'@object-ui/plugin-dashboard': patch +'@object-ui/plugin-detail': patch +'@object-ui/plugin-gantt': patch +'@object-ui/plugin-grid': patch +'@object-ui/plugin-kanban': patch +'@object-ui/plugin-map': patch +'@object-ui/plugin-timeline': patch +'@object-ui/plugin-tree': patch +'@object-ui/react': patch +--- + +refactor(ui)!: the renderer runs `@objectstack/sdui-parser`, and `@object-ui/sdui-parser` is deleted (#25712) + +Clause-②: yes (narrowing: a `type-mismatch` or `member-type-mismatch` the html-tier compile used to grade `warning` is now an `error`, so a page that carried one now refuses to compile instead of rendering with the value dropped) +Metadata-protocol: none + +The renderer and the save gate each ran their own copy of the SDUI parser. The previous change under this card made `@objectstack/sdui-parser` the one body. This change moves every `@object-ui/*` package, the console and the examples onto it, and deletes `ui/packages/sdui-parser`. There is no forwarding release of `@object-ui/sdui-parser`: it leaves the fixed version group, and the 18.1 line on npm is its last. + +- **Dependencies.** Each package above now depends on `@objectstack/sdui-parser` where it depended on `@object-ui/sdui-parser`. The console's `gen-manifest.ts` moved from the deleted package to `ui/apps/console/scripts/`. +- **Severity, the one narrowing.** The renderer's copy graded a `type-mismatch` as an `error` only beside an `enum` arm, and as a `warning` otherwise. The kept rule grades every `type-mismatch` and `member-type-mismatch` an `error`, which is what the save gate already did. So an html-tier page whose attribute does not fit the declared type now shows the compile-error block naming the prop, where it rendered the block with that value dropped. Example: `` (a string where `columns` is declared `array`). A page the save gate accepted is not affected, because the save gate already refused it. +- **Body-dialect messages.** The retired `body` child-list key's `unknown-prop` message now ends `("body" is its retired spelling)`, and its `not-a-container` message drops the `(objectui#6771)` parenthetical. + +Migration: none for a page the save gate accepted. A page that relied on the warning has a value the declared type does not take: write it in the declared type, for example `columns={['name','amount']}`. + + diff --git a/.changeset/25831-object-ui-18-1-to-merge.md b/.changeset/25831-object-ui-18-1-to-merge.md index a17d8569526..9f95a263fc4 100644 --- a/.changeset/25831-object-ui-18-1-to-merge.md +++ b/.changeset/25831-object-ui-18-1-to-merge.md @@ -35,7 +35,6 @@ "@object-ui/providers": minor "@object-ui/react": minor "@object-ui/react-runtime": minor -"@object-ui/sdui-parser": minor "@object-ui/types": minor --- diff --git a/.changeset/25926-retire-node-visibility-dialects.md b/.changeset/25926-retire-node-visibility-dialects.md index 294d44258c3..ca37af05db3 100644 --- a/.changeset/25926-retire-node-visibility-dialects.md +++ b/.changeset/25926-retire-node-visibility-dialects.md @@ -1,7 +1,7 @@ --- '@object-ui/types': minor '@object-ui/react': minor -'@object-ui/sdui-parser': minor +'@objectstack/sdui-parser': patch '@object-ui/app-shell': patch --- @@ -16,7 +16,7 @@ Metadata-protocol: narrowed - **`@object-ui/types`**: `BaseSchema.visibleOn`, `hiddenOn` and `disabledOn` are `never` on the TypeScript face and alias refusals on the Zod mirror. A document that writes one is refused at that key, and the message names the key that survives (`visibleWhen`, `hidden`, `disabled`), says the polarity is the same, and says the value is unchanged. `objectui validate` reads the same mirror, so it prints the same refusal. The `object-view` table slot's refusal of the same three keys now names the surviving node key too (write `visibleWhen` / `hidden` / `disabled` on the object-view node itself) instead of sending the author to a retired spelling. Boolean `visible` stays: the spec declares it on actions, navigation entries, tabs, alerts and settings, and the renderer keeps evaluating it. - **`@object-ui/react`**: `SchemaRenderer`'s visibility chain has three legs, `visibleWhen` and `visible` (shown when truthy) and `hidden` (hidden when truthy), and the enablement gate has one, `disabled`. A stale retired key that reaches the renderer unvalidated is not consulted and is held out of the component's props, so it neither gates the node nor lands in the DOM. -- **`@object-ui/sdui-parser`**: the three keys are no longer base props. `validateTree` reports each as an `unknown-prop` diagnostic that names the surviving key, so a TSX tree is told the same thing a JSON document is. `checkRetiredVisibilityDialect` and `RETIRED_VISIBILITY_DIALECTS` are exported for that reading. +- **`@objectstack/sdui-parser`**: the three keys are no longer base props. `validateTree` reports each as an `unknown-prop` diagnostic that names the surviving key, so a TSX tree is told the same thing a JSON document is. `checkRetiredVisibilityDialect` and `RETIRED_VISIBILITY_DIALECTS` are exported for that reading. - **`@object-ui/app-shell`**: the command palette reads a navigation entry's `visible` only; the `visibleOn` fallback it carried read a key `NavigationItem` never declared. **BREAKING** (an accept-set narrowing) for any page, view or component document that writes `visibleOn`, `hiddenOn` or `disabledOn` on a node. No shipped example, fixture or skill example in this repository wrote one; the ones in the documentation and the `objectui` skill are rewritten in this change. diff --git a/.changeset/26276-ui-package-test-vitest.md b/.changeset/26276-ui-package-test-vitest.md index b511d602d6a..0b83f5afb68 100644 --- a/.changeset/26276-ui-package-test-vitest.md +++ b/.changeset/26276-ui-package-test-vitest.md @@ -19,7 +19,6 @@ '@object-ui/providers': patch '@object-ui/react': patch '@object-ui/react-runtime': patch -'@object-ui/sdui-parser': patch '@object-ui/types': patch --- diff --git a/.changeset/config.json b/.changeset/config.json index a6098cd3aac..ca7dc39bec5 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -117,7 +117,6 @@ "@object-ui/providers", "@object-ui/react", "@object-ui/react-runtime", - "@object-ui/sdui-parser", "@object-ui/types", "object-ui" ] diff --git a/content/docs/protocol/backward-compatibility.mdx b/content/docs/protocol/backward-compatibility.mdx index 3ce98359950..29423b59a0c 100644 --- a/content/docs/protocol/backward-compatibility.mdx +++ b/content/docs/protocol/backward-compatibility.mdx @@ -116,7 +116,7 @@ Read the **Breaking?** column, not the version number: in the 17.x minors every 1. **RFC (Request for Comments)** — Breaking changes are proposed as GitHub issues with the `protocol:breaking` label. 2. **Deprecation** — The old behavior is deprecated in a MINOR release (see timeline above). 3. **Migration Guide** — A detailed migration guide is published before the removal lands, in the release notes for the version that carries it. -4. **Release** — In the 17.x minors the breaking change shipped in the next **MINOR** version, carrying a changeset entry marked `**BREAKING**`, and `scripts/check-changeset-no-major.mjs` failed a pull request that declared a `major` bump, because under lockstep one `major` would promote all 111 published packages. During the v18 prerelease it shipped in the next `18.0.0-next.N`, because the guard stood aside in pre mode and a pull request could declare a major bump. Changesets has exited pre mode, so the guard fails a `major` bump again. See [how a breaking stage lands on the v18 line](/docs/releases/v18#how-a-breaking-stage-lands-on-this-line). +4. **Release** — In the 17.x minors the breaking change shipped in the next **MINOR** version, carrying a changeset entry marked `**BREAKING**`, and `scripts/check-changeset-no-major.mjs` failed a pull request that declared a `major` bump, because under lockstep one `major` would promote all 110 published packages. During the v18 prerelease it shipped in the next `18.0.0-next.N`, because the guard stood aside in pre mode and a pull request could declare a major bump. Changesets has exited pre mode, so the guard fails a `major` bump again. See [how a breaking stage lands on the v18 line](/docs/releases/v18#how-a-breaking-stage-lands-on-this-line). --- @@ -196,7 +196,7 @@ The `@objectstack/spec` package provides additional stability guarantees: ### Which surfaces this covers -**All of them.** It was not scoped to an experimental corner or a pre-release channel: all **111** packages published from this repository belong to a single Changesets `fixed` group, so they share one version number and one policy. No published surface was exempt. +**All of them.** It was not scoped to an experimental corner or a pre-release channel: all **110** packages published from this repository belong to a single Changesets `fixed` group, so they share one version number and one policy. No published surface was exempt. The convention was enforced rather than informal — `scripts/check-changeset-no-major.mjs` failed a pull request that introduced a `major` bump, because under lockstep a single `major` on one package would promote the entire stack. During the v18 prerelease Changesets was in pre mode with the tag `next` and the guard stood aside, so a pull request on the v18 line could declare a major bump. Changesets has exited pre mode, and the guard fails a `major` bump again. diff --git a/ui/packages/sdui-parser/src/__tests__/spec-symbol-batch7.test.ts b/packages/lint/src/sdui-spec-symbol-batch7.test.ts similarity index 65% rename from ui/packages/sdui-parser/src/__tests__/spec-symbol-batch7.test.ts rename to packages/lint/src/sdui-spec-symbol-batch7.test.ts index 1a890a22903..5c7814b1ac5 100644 --- a/ui/packages/sdui-parser/src/__tests__/spec-symbol-batch7.test.ts +++ b/packages/lint/src/sdui-spec-symbol-batch7.test.ts @@ -29,8 +29,7 @@ import { describe, it, expect } from 'vitest'; import type { ValidationResult as SpecKernelValidationResult } from '@objectstack/spec/kernel'; import type { ValidationResult as SpecContractsValidationResult } from '@objectstack/spec/contracts'; -import type { ManifestValidationResult } from '../types.js'; -import { validateTree } from '../validate.js'; +import { validateTree, type ManifestValidationResult } from '@objectstack/sdui-parser'; describe('validateTree still returns the manifest-shaped result', () => { it('returns diagnostics / requires / bindings, not a `valid` flag', () => { @@ -50,22 +49,17 @@ type Extends = [A] extends [B] ? true : false; type IsAny = 0 extends 1 & T ? true : false; type HasKey = K extends keyof T ? true : false; -describe('the spec ValidationResults are plugin-manifest results, this one is not', () => { - it('is pinned at compile time', () => { - // Load-bearing imports: the day the spec retires either name, this file - // stops compiling and the convention can be revisited. - type _KernelIsReal = Assert, false>>; - type _ContractsIsReal = Assert, false>>; +// Load-bearing imports: the day the spec retires either name, this file stops +// compiling and the convention can be revisited. Exported so the strict +// `noUnusedLocals` of tsconfig.test.json reads them as used. +export type _KernelIsReal = Assert, false>>; +export type _ContractsIsReal = Assert, false>>; - type _SpecCarriesAValidFlag = Assert>; - type _ThisOneDoesNot = Assert, false>>; - type _ThisOneCarriesRequires = Assert>; +export type _SpecCarriesAValidFlag = Assert>; +export type _ThisOneDoesNot = Assert, false>>; +export type _ThisOneCarriesRequires = Assert>; - // Not assignable in either direction — sharing the name was never a - // narrowing or a widening, it was two subjects under one word. - type _NotAssignable = Assert, false>>; - type _NorTheOtherWay = Assert, false>>; - - expect(true).toBe(true); - }); -}); +// Not assignable in either direction — sharing the name was never a narrowing +// or a widening, it was two subjects under one word. +export type _NotAssignable = Assert, false>>; +export type _NorTheOtherWay = Assert, false>>; diff --git a/packages/sdui-parser/src/__tests__/base-props-one-list-11044.test.ts b/packages/sdui-parser/src/__tests__/base-props-one-list-11044.test.ts index 8f8360e7410..f9b8fc96eb3 100644 --- a/packages/sdui-parser/src/__tests__/base-props-one-list-11044.test.ts +++ b/packages/sdui-parser/src/__tests__/base-props-one-list-11044.test.ts @@ -25,8 +25,9 @@ * nothing, while a near-miss spelling still draws `unknown-prop` (the lit * control — without it, "draws nothing" would also pass against a tier * that stopped warning on undeclared keys); - * - MEMBER 1: `visibleWhen`, `hiddenOn` and `testId` are accepted on every - * node, as `bind` and `hidden` are; + * - MEMBER 1: `visibleWhen` and `testId` are accepted on every node, as + * `bind` and `hidden` are; the retired `visibleOn` / `hiddenOn` / + * `disabledOn` dialects are answered by name (`retired-visibility-dialects.ts`); * - MEMBER 2 (the triage ruling): a `'where-undeclared'` member is a base * prop only where the type declares no input of that name. Where one does, * the declared input wins, `type-mismatch` included; @@ -100,11 +101,8 @@ const WELL_TYPED: Record = { style: { padding: 4 }, visible: true, visibleWhen: "data.role == 'admin'", - visibleOn: "data.role == 'admin'", hidden: true, - hiddenOn: "data.role != 'admin'", disabled: false, - disabledOn: 'data.locked', bind: 'users', testId: 'users-list', children: [], @@ -155,11 +153,27 @@ describe('objectui#11044 — the base props are one list, read by the validator }); }); -describe('objectui#11044 member 1 — `visibleWhen`, `hiddenOn` and `testId` are base props of every node', () => { - it.each(['visibleWhen', 'hiddenOn', 'testId'])('`%s` is an `every-node` member', (key) => { +describe('objectui#11044 member 1 — `visibleWhen` and `testId` are base props of every node', () => { + it.each(['visibleWhen', 'testId'])('`%s` is an `every-node` member', (key) => { expect(SDUI_BASE_PROPS.find((p) => p.name === key)?.scope).toBe('every-node'); expect(diagnose({ ...LEAF, [key]: WELL_TYPED[key] })).toEqual([]); }); + + // The retired node-level dialects are NOT base props: each draws ONE + // `unknown-prop` that names the canonical key, the way `body` names + // `children` — a replacement for the bare "has no prop", never a second line. + it.each([ + ['visibleOn', 'visibleWhen'], + ['hiddenOn', 'hidden'], + ['disabledOn', 'disabled'], + ] as const)('the retired `%s` draws one `unknown-prop` naming `%s`', (key, canonical) => { + expect(SDUI_BASE_PROPS.find((p) => p.name === key)).toBeUndefined(); + const found = diagnose({ ...LEAF, [key]: 'data.locked' }); + expect(found.map((d) => d.code)).toEqual(['unknown-prop']); + expect(found[0]!.message).toContain(`"${key}"`); + expect(found[0]!.message).toContain(`"${canonical}"`); + expect(found[0]!.message).toContain('retired spelling'); + }); }); describe('objectui#11044 member 2 — a declared input outranks a `where-undeclared` base prop', () => { @@ -185,7 +199,7 @@ const TSX_CASES: ReadonlyArray<{ what: string; code: string; rejected: boolean } { what: '`bind` (objectui#11008)', rejected: false, code: `export const c1 = ;` }, { what: '`hidden` (objectui#11008)', rejected: false, code: `export const c2 =