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
37 changes: 37 additions & 0 deletions .changeset/25712-sdui-parser-one-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
'@objectstack/sdui-parser': patch
---

feat(sdui-parser)!: one parser body: the renderer copy's additions join `@objectstack/sdui-parser` (#25712)

Clause-②: yes (narrowing: `validateTree` now also judges the nodes held in a manifest entry's declared `slots`, so a caller whose manifest carries `slots` gets diagnostics on slot-held nodes it did not get before)
Metadata-protocol: none

Two copies of this parser existed: this package, which the save gate runs, and `@object-ui/sdui-parser`, which the renderer runs. `check:sdui-lockstep` held them to one grammar and one code set, and they drifted everywhere else. This change makes this package the one body. The renderer moves onto it and the second copy is deleted in follow-up changes under the same card.

**Added (from the renderer's copy).**

- Node-slot validation (objectui#11170). `ManifestComponent.slots` lists the positions besides `children` where a renderer reads nodes, such as `trigger`, `items[].content` and `regions[].components`. `validateTree` descends each one the way it descends `children`. `manifestFromConfigs` gains a `slotsFor` option that writes the list, and a manifest built without it carries none, so the walk follows `children` alone.
- The retired `body` child-list key (objectui#6771). Before, `body` drew the bare `unknown-prop`. Now the message names `children`. A `body` child list under a component that takes no children draws `not-a-container`, the code the `children` spelling draws. Both remain warnings. Exported as `checkRetiredBodyDialect` and `RETIRED_CHILD_LIST_KEY`.
- html-tier provenance (objectui#4000). `parseJsx` stamps every element it builds with an enumerable symbol-keyed marker, `Symbol.for('@object-ui/sdui-parser.html-tier-node')`. `JSON.stringify` and `Object.keys` do not see it. Exported as `HTML_TIER_NODE`, `isHtmlTierNode` and `markHtmlTierNode`. The registry key keeps its original package name on purpose, so a node either copy stamped reads the same.
- More exports:
- `acceptsChildren`, `CHILD_LIST_KEY`, `SDUI_BASE_PROPS`, `SduiBaseProp` and `SduiBasePropScope`;
- `assertFullyLoaded`, the manifest generators' guard against lazy stubs, with `RegistryConfigLike.lazy`;
- `NodeSlotDeclarationLike`.
- The generated `.d.ts` header gains `/* eslint-disable */` and names this package.

**Renamed, with the old names kept.** `ParsedSchemaNode` is the parsed node and `ManifestValidationResult` is `validateTree`'s result. These are the names the renderer's copy took so it would not collide with `@object-ui/types`' `SchemaNode` or the spec's `ValidationResult` (objectui#6349, objectui#3161). `SchemaNode` and `ValidationResult` stay exported as `@deprecated` aliases of them, so nothing exported is removed.

**Unchanged.**

- Every `type-mismatch` and `member-type-mismatch` is still an `error`. The renderer's copy graded them `error` only beside an `enum` arm; this package's rule is the one kept.
- `unconsumed-widget-option` is still emitted as an inline literal.
- The grammar, the 25 diagnostic codes and the containment predicate.

**BREAKING** (a narrowing of `validateTree` for a manifest that carries `slots`), shipped as `patch` because no metadata protocol kind moves. The save gate's verdicts do not move, and that was measured: the manifest that `objectstack validate`, `build`, `lint` and `serve` read is the tracked `sdui.manifest.json`, which `@objectstack/console` ships. It carries `slots` on 0 of its 106 entries, because its generator calls `manifestFromConfigs` without `slotsFor`. So no save that was accepted before is refused now. The walk has work only for a caller that builds its manifest with `slotsFor`, and today that is the renderer, whose own copy already walks the slots.

Migration, for a TypeScript caller: none is required. To move off the deprecated names, change `SchemaNode` to `ParsedSchemaNode` and `ValidationResult` to `ManifestValidationResult`.

`body-dialect.ts` and `provenance.ts` moved in from the MIT-licensed `@object-ui/sdui-parser` and keep that licence. Each carries the notice, and the package now ships `LICENSE-MIT`.

<!-- 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 `validateTree`, a function of this library, for a manifest that carries the new `slots` key: nodes held in those positions are now judged. No manifest the save gate reads carries that key (the tracked `sdui.manifest.json`: 0 of 106 entries), so no stored or authored page changes verdict there. A caller who builds a slotted manifest gets diagnostics on slot-held nodes, and the repair is the page author's per diagnostic, which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed, since the two renamed types stay exported as deprecated aliases (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
21 changes: 21 additions & 0 deletions packages/sdui-parser/LICENSE-MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 ObjectQL

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 2 additions & 1 deletion packages/sdui-parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"files": [
"dist",
"README.md",
"CHANGELOG.md"
"CHANGELOG.md",
"LICENSE-MIT"
],
"engines": {
"node": ">=22.0.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE-MIT file beside this package's manifest.
*/

/**
* `bind` and `hidden` are base props of the tier (objectui#11008).
*
* `BaseSchema` (`@object-ui/types`) declares both for every node, and no
* registration declares either as an input. Before they joined `BASE_PROPS`,
* the undeclared-key branch answered every authored one with `unknown-prop`
* ("has no prop"), including on the nodes that honour it: `SchemaRenderer`
* reads `hidden` for every node, and every `useDataScope` renderer reads
* `bind`. This file pins the TIER's reading of a manifest; the live-registry
* half — `list`, `tree-view` and `data-table` — is
* `ui/packages/components/src/renderers/__tests__/bind-base-prop-parser-tier-11008.test.tsx`.
*
* Three facts, kept apart because each one fails differently:
*
* - both keys draw nothing, in every value shape `BaseSchema` admits for
* them — the fix;
* - a near-miss spelling (`bindTo`, `hide`) on the SAME node still draws
* `unknown-prop` naming it — the lit control. Without it, "draws nothing"
* would also pass against a tier that stopped warning on undeclared keys;
* - a registration that DECLARES `placeholder` keeps its `type-mismatch`.
* `placeholder` is a `BaseSchema` member too, and it was left out of the
* `every-node` members because they skip the declared-input lookup. It
* joined later as a `where-undeclared` member (objectui#11044), which a
* declared input outranks: this row goes red if it is ever made an
* `every-node` member the way `bind` and `hidden` were.
*/
import { describe, expect, it } from 'vitest';
import { manifestFromConfigs, validateTree } from '../index.js';
import type { Diagnostic, Manifest, SchemaElement } from '../types.js';

const manifest: Manifest = manifestFromConfigs([
// a leaf that declares neither key, as no live registration does
{ type: 'leaf', namespace: 'ui', inputs: [{ name: 'content', type: 'string' }] },
// the input-family shape: `placeholder` declared as a typed input
{ type: 'field', namespace: 'ui', inputs: [{ name: 'placeholder', type: 'string' }] },
] as unknown as Parameters<typeof manifestFromConfigs>[0]);

const diagnose = (node: unknown): Diagnostic[] =>
validateTree(node as SchemaElement, manifest).diagnostics;

const LEAF = { type: 'leaf', content: 'x' };

/** Every value shape `BaseSchema` declares for the two keys. */
const BASE_VALUES: Record<'bind' | 'hidden', unknown[]> = {
bind: ['users', 'app.settings.users'],
hidden: [true, false, '${data.role !== "admin"}', { dialect: 'cel', source: "record.status == 'draft'" }],
};

describe('objectui#11008 — `bind` and `hidden` are base props of the parser tier', () => {
it('the control node draws nothing', () => {
expect(diagnose(LEAF)).toEqual([]);
});

it.each(Object.keys(BASE_VALUES) as Array<keyof typeof BASE_VALUES>)(
'`%s` draws no diagnostic, in every value shape BaseSchema admits',
(key) => {
const drew = BASE_VALUES[key]
.map((value) => [value, diagnose({ ...LEAF, [key]: value })] as const)
.filter(([, found]) => found.length > 0)
.map(([value, found]) => `${JSON.stringify(value)} → ${JSON.stringify(found.map((d) => d.code))}`);
expect(drew).toEqual([]);
},
);

it.each([
['bindTo', 'users'],
['hide', true],
] as const)('lit control: the near-miss `%s` still draws one `unknown-prop` naming it', (key, value) => {
const found = diagnose({ ...LEAF, [key]: value });
expect(found.map((d) => d.code)).toEqual(['unknown-prop']);
expect(found[0]!.message).toContain(`"${key}"`);
});

it('a registration that declares `placeholder` keeps its type check — why `placeholder` is not `every-node`', () => {
expect(diagnose({ type: 'field', placeholder: 'Search…' })).toEqual([]);
const found = diagnose({ type: 'field', placeholder: 42 });
expect(found.map((d) => d.code)).toEqual(['type-mismatch']);
expect(found[0]!.message).toContain('"placeholder"');
});
});
Loading
Loading