Skip to content

Commit 80f267d

Browse files
refactor(ui)!: the renderer runs @objectstack/sdui-parser; delete @object-ui/sdui-parser (#25712 2g PR 2) (#26343)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao7EJRrpBukNTDcMt2XiXQX1)_ Part of #25712 (item 3 = 2g, PR 2 of 3: rewrite the importers onto `@objectstack/sdui-parser` and delete `ui/packages/sdui-parser`; Tier H: over 3000 lines, and `skills/**`). Part of #25677. 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 ## 维护者速读(草稿) - **改了什么:** - 按 2g 裁定(6418729849,E+B),ui 里所有对 `@object-ui/sdui-parser` 的引用改为 `@objectstack/sdui-parser`,包括 import、`package.json` 依赖、vite/vitest 别名和 tsconfig 路径。 - 删除 `ui/packages/sdui-parser`(不发转发版本),并把它移出 changesets 的 fixed 组。 - `gen-manifest.ts` 移到 `ui/apps/console/scripts/`。PR 1 留下的 5 个测试搬到各自归属的包。 - ui 里按包数计数的几个门禁同步减一。manifest 记录不再包含这个包。 - **哪些原来能渲染的现在会报错:** 渲染器原来那份解析器只在 enum 旁把类型不符判为 error,其他情况只给 warning。现在统一按保存门的规则判为 error。所以 html 层页面里属性类型写错(例如 `columns='["name","amount"]'`,而 `columns` 声明为数组)会显示编译错误块,不再静默丢掉这个值。保存门本来就拒绝这种页面,所以经保存门保存的页面不受影响。 - **为什么改:** 两份解析器只剩一份,渲染器和保存门跑同一套代码。PR 3(#26221)和 PR 1(#26211)已经合入。 - **风险与代价(含回滚):** 约 8400 行变更,其中约 7000 行是删除(被删的包和锁文件)。回滚 = revert 本 PR。合入后 npm 上的 `@object-ui/sdui-parser` 停在 18.1,18.2 之后由维护者执行 npm deprecate。 - **需要你确认的一件事:** 两个待发布的 changeset(`25831-object-ui-18-1-to-merge.md`、`26276-ui-package-test-vitest.md`)原本列着 `@object-ui/sdui-parser`。包删掉后,`changeset version` 遇到不存在的包会失败,所以本 PR 删掉了这两行。`check-empty-changeset` 把这种改动当作需要人确认的「主动更正」,会一直是红的,直到你确认。 - **你要做的:** 确认上面两行删除,然后批准(Tier H)。 ## 席位核对 (left for the dispatch seat) ## Why Ruling 6418729849 on #25712 keeps `@objectstack/sdui-parser` as the one parser, deletes `ui/packages/sdui-parser` with no forwarding release, and takes `@object-ui/sdui-parser` out of the fixed group. The maintainer deprecates the npm package after 18.2. PR 3 (#26221, lockstep gate retired) and PR 1 (#26211, one body in `@objectstack/sdui-parser`) are on main. This PR moves the renderer onto that body and deletes the second copy. ## What changes **Importers (no behaviour of their own).** - Every `@object-ui/sdui-parser` import, `package.json` dependency key, vite alias (console, console-starter, plugin-editor, plugin-markdown, runner), the `ui/vitest.config.mts` alias and the `ui/tsconfig.json` paths now name `@objectstack/sdui-parser`, which resolves to the root `packages/sdui-parser`. - No importer uses the deprecated `SchemaNode` / `ValidationResult` aliases. - Kept on purpose: - the `Symbol.for('@object-ui/sdui-parser.html-tier-node')` registry key, which is the cross-instance contract; - the MIT "moved in from `@object-ui/sdui-parser`" notices; - CHANGELOGs and dated QA notes. **Deleted:** `ui/packages/sdui-parser` (32 files). **Moved:** | From | To | |---|---| | `ui/packages/sdui-parser/scripts/gen-manifest.ts` | `ui/apps/console/scripts/gen-manifest.ts` (its only caller is `emit-sdui-manifest.ts`) | | `…/__tests__/render.test.tsx`, `inline-whitespace.test.tsx`, `injected-component-input-6950.test.ts` | `ui/packages/react/src/__tests__/sdui-*` (they import `../SchemaRenderer`, not `@object-ui/react`, per `check-package-self-import`) | | `…/__tests__/spec-symbol-batch7.test.ts` | `packages/lint/src/sdui-spec-symbol-batch7.test.ts` | | `…/__tests__/dashboard-widget-options-census.test.ts` | `ui/packages/plugin-dashboard/src/__tests__/` (leg 4 no longer counts the ui copy) | `@object-ui/react` gains `@objectstack/sdui-parser` as a devDependency for the re-homed tests. **Guards that counted the package:** - package floors drop by one: - `package-files-exist`: owing floor 38→37; - `side-effects-declaration-consistency`: 29→28 and `false` 12→11; - `check-dist-completeness`: tsc floor 12→11; - `QUICK_REFERENCE.md`: 37→36 published packages; - `check:lockstep-package-count`: 111→110 in `backward-compatibility.mdx` and `publish-smoke-pack.mjs`. - `turbo.objectui.json` drops the 10 build inputs that pointed at the deleted package. - `select-type-check-scope`'s fixture now uses a live file. - `check-type-check-coverage` drops the deleted scripts directory. - The cli `workspace-vite` alias list drops the package. - `ui/.github/labeler.yml` drops its rule. - `.changeset/config.json` removes it from the fixed group. **Relative reads re-rooted** to the root parser: - `designerRegistrationInputs-11434`; - `body-dialect-children-arm-7181`; - `component-input-retired-keys-7493`; - `unconsumed-widget-option-claim-6186`; - the `ui/README.md` link, which is now a GitHub URL because `check-doc-links` treats `ui/` as its root. **Severity pin.** `htmlTierColumnSpellings-6598` form 2 (a JSON string where `columns` is declared `array`) moves from "the block's defaults render" to "fails loudly". The page now shows the compile error naming `columns`. The test's invariant, "never the reported state", holds either way. **Manifest record.** `node scripts/gen-sdui-manifest-node.mjs` drops `scripts/sdui-manifest.sources/sdui-parser.json` ("no longer reached") and re-records components, core, plugin-dashboard and types. `sdui.manifest.json` is byte-unchanged (106 components, sha256 `1f6e1ccb925a…`). **Pending changesets.** `25831-object-ui-18-1-to-merge.md` and `26276-ui-package-test-vitest.md` each lose their `'@object-ui/sdui-parser'` line, and nothing else changes. Without the edit, `changeset version` fails on a package that is not in the workspace. This is the DELIBERATE CORRECTION class of `check-empty-changeset`, so that gate is red until a maintainer confirms the two deletions. ## Verification Head `7fd87174` on main `62941574`. 185 paths: +1182 / −7242 lines (8424), of which the deleted package and the lockfile account for −7007. Governed: `skills/**` ×1 (`skills/objectui/README.md`, one line) and over the 3000 human-merge line. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands` derives 137. All 137 ran: - 136 exit 0; - `check-empty-changeset` exits 1 for the reason above; - 119 ran on `90f44cec`. The 18 families added by the `check:lockstep-package-count --fix` commit, plus that gate, ran on `0b13f123`, whose tree equals `7fd87174`. Only the commit message was reworded. - `--ran` reconciles 137 derived, 137 run, 0 NOT-MEASURED, 0 UNRUN. - **ui vitest:** - On this head: the 83 changed or importing test files, 1663 tests, all pass. - On the pre-rebase tree (the same change on PR 1's head), the whole `unit` project ran: 1643 files and 31054 tests. Every red was fixed in this diff, except the 47→48 `ObjectSchema` key pin, which main already carries. - Also on the pre-rebase tree: `dom-heavy` passed 35 of 35 files, and `dom` shards 1–3 of 4 passed 2914 files. Shard 4 was cut off by the local time limit, and CI's 16 shards cover it. - **Push battery** (`GITHUB_ACTIONS=true GITHUB_EVENT_NAME=push node scripts/pm/check-dispatch-gates.mjs`) on `7fd87174`: `✓ dispatch-gates self-test: 2164 cases pass`, exit 0. - **2i overlap:** 2i PR A (#26315) touches the ui filter and the manifest prose, so whichever lands second rebases. ## Changeset `.changeset/25712-renderer-runs-one-sdui-parser.md`: a `patch` for the 13 published `@object-ui/*` packages whose dependency moved, with the Clause-② narrowing above and an ADR-0087 `not-required (no-migration-prescription)` marker. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Xe3LjBvLrRVA6dmFUEGskm --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a66c9ff commit 80f267d

181 files changed

Lines changed: 390 additions & 6437 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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@object-ui/components': patch
3+
'@object-ui/console': patch
4+
'@object-ui/layout': patch
5+
'@object-ui/plugin-calendar': patch
6+
'@object-ui/plugin-dashboard': patch
7+
'@object-ui/plugin-detail': patch
8+
'@object-ui/plugin-gantt': patch
9+
'@object-ui/plugin-grid': patch
10+
'@object-ui/plugin-kanban': patch
11+
'@object-ui/plugin-map': patch
12+
'@object-ui/plugin-timeline': patch
13+
'@object-ui/plugin-tree': patch
14+
'@object-ui/react': patch
15+
---
16+
17+
refactor(ui)!: the renderer runs `@objectstack/sdui-parser`, and `@object-ui/sdui-parser` is deleted (#25712)
18+
19+
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)
20+
Metadata-protocol: none
21+
22+
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.
23+
24+
- **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/`.
25+
- **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: `<list-view columns='["name","amount"]' />` (a string where `columns` is declared `array`). A page the save gate accepted is not affected, because the save gate already refused it.
26+
- **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.
27+
28+
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']}`.
29+
30+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition, stored row or stored shape is added, removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is the renderer's grading of a value that does not fit its declared type, which the save gate already refused, so no page saved through the gate changes verdict. The repair of a page written around the gate is the author's per diagnostic, which no ledger entry can derive. The other categories are closed on facts: no ADR-0087 id is named or touched (not registered or already-registered); the deleted `@object-ui/sdui-parser` stays published on npm at 18.1 and is deprecated there by the maintainer after 18.2, and no package in this change is unpublished (not unpublished); nothing exported by a package above is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->

‎.changeset/25831-object-ui-18-1-to-merge.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
"@object-ui/providers": minor
3636
"@object-ui/react": minor
3737
"@object-ui/react-runtime": minor
38-
"@object-ui/sdui-parser": minor
3938
"@object-ui/types": minor
4039
---
4140

‎.changeset/25926-retire-node-visibility-dialects.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
'@object-ui/types': minor
33
'@object-ui/react': minor
4-
'@object-ui/sdui-parser': minor
4+
'@objectstack/sdui-parser': patch
55
'@object-ui/app-shell': patch
66
---
77

@@ -16,7 +16,7 @@ Metadata-protocol: narrowed
1616

1717
- **`@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.
1818
- **`@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.
19-
- **`@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.
19+
- **`@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.
2020
- **`@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.
2121

2222
**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.

‎.changeset/26276-ui-package-test-vitest.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
'@object-ui/providers': patch
2020
'@object-ui/react': patch
2121
'@object-ui/react-runtime': patch
22-
'@object-ui/sdui-parser': patch
2322
'@object-ui/types': patch
2423
---
2524

‎.changeset/config.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@
117117
"@object-ui/providers",
118118
"@object-ui/react",
119119
"@object-ui/react-runtime",
120-
"@object-ui/sdui-parser",
121120
"@object-ui/types",
122121
"object-ui"
123122
]

‎content/docs/protocol/backward-compatibility.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Read the **Breaking?** column, not the version number: in the 17.x minors every
116116
1. **RFC (Request for Comments)** — Breaking changes are proposed as GitHub issues with the `protocol:breaking` label.
117117
2. **Deprecation** — The old behavior is deprecated in a MINOR release (see timeline above).
118118
3. **Migration Guide** — A detailed migration guide is published before the removal lands, in the release notes for the version that carries it.
119-
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).
119+
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).
120120

121121
---
122122

@@ -196,7 +196,7 @@ The `@objectstack/spec` package provides additional stability guarantees:
196196

197197
### Which surfaces this covers
198198

199-
**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.
199+
**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.
200200

201201
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.
202202

ui/packages/sdui-parser/src/__tests__/spec-symbol-batch7.test.ts renamed to packages/lint/src/sdui-spec-symbol-batch7.test.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ import { describe, it, expect } from 'vitest';
2929
import type { ValidationResult as SpecKernelValidationResult } from '@objectstack/spec/kernel';
3030
import type { ValidationResult as SpecContractsValidationResult } from '@objectstack/spec/contracts';
3131

32-
import type { ManifestValidationResult } from '../types.js';
33-
import { validateTree } from '../validate.js';
32+
import { validateTree, type ManifestValidationResult } from '@objectstack/sdui-parser';
3433

3534
describe('validateTree still returns the manifest-shaped result', () => {
3635
it('returns diagnostics / requires / bindings, not a `valid` flag', () => {
@@ -50,22 +49,17 @@ type Extends<A, B> = [A] extends [B] ? true : false;
5049
type IsAny<T> = 0 extends 1 & T ? true : false;
5150
type HasKey<T, K extends string> = K extends keyof T ? true : false;
5251

53-
describe('the spec ValidationResults are plugin-manifest results, this one is not', () => {
54-
it('is pinned at compile time', () => {
55-
// Load-bearing imports: the day the spec retires either name, this file
56-
// stops compiling and the convention can be revisited.
57-
type _KernelIsReal = Assert<Equal<IsAny<SpecKernelValidationResult>, false>>;
58-
type _ContractsIsReal = Assert<Equal<IsAny<SpecContractsValidationResult>, false>>;
52+
// Load-bearing imports: the day the spec retires either name, this file stops
53+
// compiling and the convention can be revisited. Exported so the strict
54+
// `noUnusedLocals` of tsconfig.test.json reads them as used.
55+
export type _KernelIsReal = Assert<Equal<IsAny<SpecKernelValidationResult>, false>>;
56+
export type _ContractsIsReal = Assert<Equal<IsAny<SpecContractsValidationResult>, false>>;
5957

60-
type _SpecCarriesAValidFlag = Assert<HasKey<SpecKernelValidationResult, 'valid'>>;
61-
type _ThisOneDoesNot = Assert<Equal<HasKey<ManifestValidationResult, 'valid'>, false>>;
62-
type _ThisOneCarriesRequires = Assert<HasKey<ManifestValidationResult, 'requires'>>;
58+
export type _SpecCarriesAValidFlag = Assert<HasKey<SpecKernelValidationResult, 'valid'>>;
59+
export type _ThisOneDoesNot = Assert<Equal<HasKey<ManifestValidationResult, 'valid'>, false>>;
60+
export type _ThisOneCarriesRequires = Assert<HasKey<ManifestValidationResult, 'requires'>>;
6361

64-
// Not assignable in either direction — sharing the name was never a
65-
// narrowing or a widening, it was two subjects under one word.
66-
type _NotAssignable = Assert<Equal<Extends<ManifestValidationResult, SpecKernelValidationResult>, false>>;
67-
type _NorTheOtherWay = Assert<Equal<Extends<SpecKernelValidationResult, ManifestValidationResult>, false>>;
68-
69-
expect(true).toBe(true);
70-
});
71-
});
62+
// Not assignable in either direction — sharing the name was never a narrowing
63+
// or a widening, it was two subjects under one word.
64+
export type _NotAssignable = Assert<Equal<Extends<ManifestValidationResult, SpecKernelValidationResult>, false>>;
65+
export type _NorTheOtherWay = Assert<Equal<Extends<SpecKernelValidationResult, ManifestValidationResult>, false>>;

‎packages/sdui-parser/src/__tests__/base-props-one-list-11044.test.ts‎

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
* nothing, while a near-miss spelling still draws `unknown-prop` (the lit
2626
* control — without it, "draws nothing" would also pass against a tier
2727
* that stopped warning on undeclared keys);
28-
* - MEMBER 1: `visibleWhen`, `hiddenOn` and `testId` are accepted on every
29-
* node, as `bind` and `hidden` are;
28+
* - MEMBER 1: `visibleWhen` and `testId` are accepted on every node, as
29+
* `bind` and `hidden` are; the retired `visibleOn` / `hiddenOn` /
30+
* `disabledOn` dialects are answered by name (`retired-visibility-dialects.ts`);
3031
* - MEMBER 2 (the triage ruling): a `'where-undeclared'` member is a base
3132
* prop only where the type declares no input of that name. Where one does,
3233
* the declared input wins, `type-mismatch` included;
@@ -100,11 +101,8 @@ const WELL_TYPED: Record<string, unknown> = {
100101
style: { padding: 4 },
101102
visible: true,
102103
visibleWhen: "data.role == 'admin'",
103-
visibleOn: "data.role == 'admin'",
104104
hidden: true,
105-
hiddenOn: "data.role != 'admin'",
106105
disabled: false,
107-
disabledOn: 'data.locked',
108106
bind: 'users',
109107
testId: 'users-list',
110108
children: [],
@@ -155,11 +153,27 @@ describe('objectui#11044 — the base props are one list, read by the validator
155153
});
156154
});
157155

158-
describe('objectui#11044 member 1 — `visibleWhen`, `hiddenOn` and `testId` are base props of every node', () => {
159-
it.each(['visibleWhen', 'hiddenOn', 'testId'])('`%s` is an `every-node` member', (key) => {
156+
describe('objectui#11044 member 1 — `visibleWhen` and `testId` are base props of every node', () => {
157+
it.each(['visibleWhen', 'testId'])('`%s` is an `every-node` member', (key) => {
160158
expect(SDUI_BASE_PROPS.find((p) => p.name === key)?.scope).toBe('every-node');
161159
expect(diagnose({ ...LEAF, [key]: WELL_TYPED[key] })).toEqual([]);
162160
});
161+
162+
// The retired node-level dialects are NOT base props: each draws ONE
163+
// `unknown-prop` that names the canonical key, the way `body` names
164+
// `children` — a replacement for the bare "has no prop", never a second line.
165+
it.each([
166+
['visibleOn', 'visibleWhen'],
167+
['hiddenOn', 'hidden'],
168+
['disabledOn', 'disabled'],
169+
] as const)('the retired `%s` draws one `unknown-prop` naming `%s`', (key, canonical) => {
170+
expect(SDUI_BASE_PROPS.find((p) => p.name === key)).toBeUndefined();
171+
const found = diagnose({ ...LEAF, [key]: 'data.locked' });
172+
expect(found.map((d) => d.code)).toEqual(['unknown-prop']);
173+
expect(found[0]!.message).toContain(`"${key}"`);
174+
expect(found[0]!.message).toContain(`"${canonical}"`);
175+
expect(found[0]!.message).toContain('retired spelling');
176+
});
163177
});
164178

165179
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 }
185199
{ what: '`bind` (objectui#11008)', rejected: false, code: `export const c1 = <list bind="users" />;` },
186200
{ what: '`hidden` (objectui#11008)', rejected: false, code: `export const c2 = <list hidden={true} />;` },
187201
{ what: '`visibleWhen`', rejected: false, code: `export const c3 = <list visibleWhen="data.role == 'admin'" />;` },
188-
{ what: '`hiddenOn`', rejected: false, code: `export const c4 = <list hiddenOn="data.archived" />;` },
202+
{ what: 'the retired `hiddenOn` is no JSX attribute either', rejected: true, code: `export const c4 = <list hiddenOn="data.archived" />;` },
189203
{ what: '`testId`', rejected: false, code: `export const c5 = <list testId="users-list" />;` },
190204
{ what: 'an undeclared `label`', rejected: false, code: `export const c6 = <list label="Users" />;` },
191205
{ what: 'lit control: a near-miss', rejected: true, code: `export const c7 = <list bindTo="users" />;` },

‎packages/sdui-parser/src/__tests__/base-props-one-list.test.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,16 @@ const codes = (node: SchemaElement): string[] =>
2727
validateTree(node, manifest).diagnostics.map((d) => d.code);
2828

2929
const EVERY_NODE = [
30-
'id', 'className', 'style', 'visible', 'visibleWhen', 'visibleOn', 'hidden',
31-
'hiddenOn', 'disabled', 'disabledOn', 'bind', 'testId', 'children',
30+
'id', 'className', 'style', 'visible', 'visibleWhen', 'hidden',
31+
'disabled', 'bind', 'testId', 'children',
3232
];
33+
// The retired node-level dialects (#25926) left the list: each draws ONE
34+
// `unknown-prop` that names its canonical key (`retired-visibility-dialects.ts`).
35+
const RETIRED_DIALECTS = [
36+
['visibleOn', 'visibleWhen'],
37+
['hiddenOn', 'hidden'],
38+
['disabledOn', 'disabled'],
39+
] as const;
3340
// `type` is the one `every-node` entry with no attribute form: on a node it
3441
// IS the tag, so a node cannot carry it without being the component under test.
3542
// It is pinned through the list below, not through `validateTree`.
@@ -41,6 +48,12 @@ describe('base props — one list, two scopes (objectui#11008, #11044)', () => {
4148
expect(codes(node)).not.toContain('unknown-prop');
4249
});
4350

51+
it.each(RETIRED_DIALECTS)('the retired `%s` draws one unknown-prop naming `%s`', (key, canonical) => {
52+
const diags = validateTree({ type: 'leaf', [key]: 'x' } as SchemaElement, manifest).diagnostics;
53+
expect(diags.map((d) => d.code)).toEqual(['unknown-prop']);
54+
expect(diags[0]!.message).toContain(`"${canonical}"`);
55+
});
56+
4457
it.each(WHERE_UNDECLARED)('undeclared `%s` draws no unknown-prop', (key) => {
4558
expect(codes({ type: 'leaf', [key]: 'x' } as SchemaElement)).toEqual([]);
4659
});
@@ -51,7 +64,7 @@ describe('base props — one list, two scopes (objectui#11008, #11044)', () => {
5164
expect(validateTree({ type: 'labelled', label: 'ok' } as SchemaElement, manifest).diagnostics).toEqual([]);
5265
});
5366

54-
it('the list is the 20 entries, `type` alone having no attribute type', () => {
67+
it('the list is the 17 entries, `type` alone having no attribute type', () => {
5568
expect(SDUI_BASE_PROPS.map((p) => p.name)).toEqual(['type', ...EVERY_NODE, ...WHERE_UNDECLARED]);
5669
expect(SDUI_BASE_PROPS.filter((p) => p.tsType === null).map((p) => p.name)).toEqual(['type']);
5770
expect(SDUI_BASE_PROPS.filter((p) => p.scope === 'every-node').map((p) => p.name)).toEqual(['type', ...EVERY_NODE]);

‎packages/sdui-parser/src/dashboard-widget-options.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
* repo-wide scan would red on a TRUE claim — and an assertion that reds on
6666
* legitimate code gets deleted by the next person who hits it, which puts
6767
* the claim back where it started. Leg 2 of
68-
* `ui/packages/sdui-parser/src/__tests__/dashboard-widget-options-census.test.ts`
68+
* `ui/packages/plugin-dashboard/src/__tests__/dashboard-widget-options-census.test.ts`
6969
* derives exactly this bound: the DatasetWidget read set is the declared
7070
* set, and neither `format` nor `thresholds` is in it. That absence is
7171
* asserted in its OWN right rather than riding on an equality whose
@@ -101,7 +101,7 @@
101101
*
102102
* ## Maintenance — how this list stays true
103103
*
104-
* `ui/packages/sdui-parser/src/__tests__/dashboard-widget-options-census.test.ts`
104+
* `ui/packages/plugin-dashboard/src/__tests__/dashboard-widget-options-census.test.ts`
105105
* re-runs the census on every test run: it re-derives the five declared keys from the installed
106106
* `@objectstack/spec`, re-extracts the `options.<key>` reads from
107107
* `DatasetWidget.tsx` source text (and fails loudly if that file gains a

0 commit comments

Comments
 (0)