@object-ui/sdui-parser 17.3.0 → 17.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen.js +33 -8
- package/dist/dashboard-widget-options.d.ts +95 -0
- package/dist/dashboard-widget-options.js +73 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +9 -14
- package/dist/input-type.d.ts +46 -0
- package/dist/input-type.js +69 -0
- package/dist/parse.js +34 -4
- package/dist/provenance.d.ts +65 -0
- package/dist/provenance.js +74 -0
- package/dist/types.d.ts +15 -2
- package/dist/types.js +1 -1
- package/dist/validate.js +92 -27
- package/package.json +4 -4
package/dist/codegen.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* fiction — no real React intrinsic named `flex` exists; the interpreter
|
|
8
8
|
* fulfills it via the registry at render time.
|
|
9
9
|
*/
|
|
10
|
+
import { inputTypeArms } from './input-type.js';
|
|
10
11
|
export function generateDts(manifest, options = {}) {
|
|
11
12
|
const { standaloneJsx = true } = options;
|
|
12
13
|
const comps = Object.values(manifest.components).sort((a, b) => a.type.localeCompare(b.type));
|
|
@@ -50,19 +51,35 @@ ${intrinsics}
|
|
|
50
51
|
export {};
|
|
51
52
|
`;
|
|
52
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A `'slot'` arm names a child position, not a prop value — `SduiBaseProps`
|
|
56
|
+
* already types `children`, so a slot input contributes no attribute. An input
|
|
57
|
+
* is therefore emitted when it has at least one NON-slot arm, and typed from
|
|
58
|
+
* those arms only (objectui#3832): the old test was `i.type !== 'slot'`, which a
|
|
59
|
+
* union like `['slot', 'string']` would have passed while `tsType` fell through
|
|
60
|
+
* to the default and typed it `string` anyway.
|
|
61
|
+
*/
|
|
53
62
|
function emitInterface(comp) {
|
|
54
63
|
const lines = comp.inputs
|
|
55
|
-
.filter((i) => i.
|
|
64
|
+
.filter((i) => valueArms(i).length > 0)
|
|
56
65
|
.map((i) => ` ${propLine(i)}`)
|
|
57
66
|
.join('\n');
|
|
58
67
|
return `export interface ${propsName(comp.type)} extends SduiBaseProps {\n${lines}\n}`;
|
|
59
68
|
}
|
|
69
|
+
/** The arms that describe a VALUE (every arm except `'slot'`). */
|
|
70
|
+
const valueArms = (input) => inputTypeArms(input.type).filter((arm) => arm !== 'slot');
|
|
60
71
|
function propLine(input) {
|
|
61
72
|
const opt = input.required ? '' : '?';
|
|
62
73
|
return `${quoteKeyIfNeeded(input.name)}${opt}: ${tsType(input)};`;
|
|
63
74
|
}
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The TypeScript type for one arm. Kinds that are a string with a narrower
|
|
77
|
+
* authoring control (`color`, `date`, `code`, `file`) are `string` here, as
|
|
78
|
+
* before — the JSX surface types the VALUE, and the control kind is the
|
|
79
|
+
* designer's business.
|
|
80
|
+
*/
|
|
81
|
+
function armTsType(arm, input) {
|
|
82
|
+
switch (arm) {
|
|
66
83
|
case 'number':
|
|
67
84
|
return 'number';
|
|
68
85
|
case 'boolean':
|
|
@@ -75,15 +92,23 @@ function tsType(input) {
|
|
|
75
92
|
const vals = (input.enum ?? []).map((e) => (typeof e === 'object' ? e.value : e));
|
|
76
93
|
return vals.length ? vals.map((v) => JSON.stringify(v)).join(' | ') : 'string';
|
|
77
94
|
}
|
|
78
|
-
case 'string':
|
|
79
|
-
case 'color':
|
|
80
|
-
case 'date':
|
|
81
|
-
case 'code':
|
|
82
|
-
case 'file':
|
|
83
95
|
default:
|
|
84
96
|
return 'string';
|
|
85
97
|
}
|
|
86
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* A union declaration emits a TypeScript union, so the `.d.ts` an author
|
|
101
|
+
* type-checks their page against accepts exactly the arms the manifest gate
|
|
102
|
+
* accepts. Arms that collapse to the same TS type (`string` and `color`, say)
|
|
103
|
+
* are de-duplicated rather than emitted as `string | string`.
|
|
104
|
+
*/
|
|
105
|
+
function tsType(input) {
|
|
106
|
+
const arms = valueArms(input);
|
|
107
|
+
if (arms.length === 0)
|
|
108
|
+
return 'string';
|
|
109
|
+
const emitted = [...new Set(arms.map((arm) => armTsType(arm, input)))];
|
|
110
|
+
return emitted.join(' | ');
|
|
111
|
+
}
|
|
87
112
|
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
88
113
|
const quoteKeyIfNeeded = (name) => (IDENT.test(name) ? name : JSON.stringify(name));
|
|
89
114
|
/** 'object-grid' -> 'ObjectGrid', 'record:details' -> 'RecordDetails' */
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — unconsumed dashboard-widget `options` keys (objectui#5709)
|
|
3
|
+
*
|
|
4
|
+
* `@objectstack/spec`'s `DashboardWidgetOptionsSchema` ends in `.passthrough()`
|
|
5
|
+
* ("declared query keys + open renderer extras"), so ANY key parses, validates
|
|
6
|
+
* and lints cleanly — including one no renderer reads. That is how a showcase
|
|
7
|
+
* dashboard shipped `options: { invert: true }` on a gauge with a comment
|
|
8
|
+
* saying what it was believed to do, and rendered the un-inverted measure with
|
|
9
|
+
* no diagnostic anywhere (objectui#5709). The 2026-08-23 maintainer ruling:
|
|
10
|
+
* open extras stay open — they just stop being SILENT. A key that reaches no
|
|
11
|
+
* renderer draws a WARNING naming the consumed set. Not an error: no gate
|
|
12
|
+
* weakening and no new red gates were ruled.
|
|
13
|
+
*
|
|
14
|
+
* ## The census (measured on origin/main @ 8689166f6, spec 17.1.0)
|
|
15
|
+
*
|
|
16
|
+
* The spec REQUIRES `dataset` on every widget (`DashboardWidgetSchema`), and
|
|
17
|
+
* both dashboard surfaces route a dataset-bound widget to `DatasetWidget`
|
|
18
|
+
* (`DashboardRenderer.tsx` renders `<DatasetWidget>` when `widget.dataset` is
|
|
19
|
+
* set; `DashboardGridLayout.tsx` mirrors the fork, objectui#4614). On that —
|
|
20
|
+
* the only spec-legal — path, the renderer-consumed `options` keys are exactly
|
|
21
|
+
* the five the spec DECLARES, all read as direct `options.<key>` accesses in
|
|
22
|
+
* `packages/plugin-dashboard/src/DatasetWidget.tsx`:
|
|
23
|
+
*
|
|
24
|
+
* dateGranularity, sortBy, sortOrder, limit (query-affecting, framework#3588)
|
|
25
|
+
* stageOrder (funnel/pyramid stage order)
|
|
26
|
+
*
|
|
27
|
+
* plus ONE undeclared key with a real read site:
|
|
28
|
+
*
|
|
29
|
+
* description — the metric-card sub-caption channel. Read at
|
|
30
|
+
* `DashboardRenderer.tsx` (`(widget.options as …)?.description`,
|
|
31
|
+
* objectui#4032 item 4); the server's `translateDashboard` OVERLAYS the
|
|
32
|
+
* `widgets.{id}.subCaption` translation onto this key (objectstack#8056,
|
|
33
|
+
* objectstack#5428 item-4: 「两个作者字段两个 key」). Warning on a key the
|
|
34
|
+
* platform's own translation pipeline writes would be a false positive on
|
|
35
|
+
* legal metadata, so it is in the accepted set even though the dataset-bound
|
|
36
|
+
* render path does not currently display it.
|
|
37
|
+
*
|
|
38
|
+
* Notably NOT consumed anywhere in this repository: `thresholds` (zero read
|
|
39
|
+
* sites repo-wide) and `format` — the dataset-bound value is formatted with
|
|
40
|
+
* the MEASURE's own metadata (`measureField(...).format`, from the dataset
|
|
41
|
+
* definition), not with `options.format`. Both were widely believed to work;
|
|
42
|
+
* both draw this warning, which is the point.
|
|
43
|
+
*
|
|
44
|
+
* ## Scope — where the warning deliberately does NOT fire
|
|
45
|
+
*
|
|
46
|
+
* - Widgets WITHOUT `dataset`: the legacy inline forms (`options.data`
|
|
47
|
+
* arrays, `provider: 'object'` bags) consume a much larger, spread-shaped
|
|
48
|
+
* key set (`{ ...options }` into `metric` / `object-metric` /
|
|
49
|
+
* `data-table` / …), whose true reach is each child component's prop
|
|
50
|
+
* surface. That form is spec-illegal today (`dataset` is required) and its
|
|
51
|
+
* census would be the unmaintainable one; skipping it keeps every warning
|
|
52
|
+
* this module emits a statement about the path the widget actually renders
|
|
53
|
+
* through.
|
|
54
|
+
* - Widgets in the legacy COMPONENT format (`widget.component`): `options`
|
|
55
|
+
* is not part of that contract.
|
|
56
|
+
* - Widgets carrying the spec's own escape hatch
|
|
57
|
+
* `suppressWarnings: ['unconsumed-widget-option']` — the spec models
|
|
58
|
+
* per-widget diagnostic suppression (`DashboardWidgetSchema`), so an
|
|
59
|
+
* author with a genuine out-of-band consumer can say so in metadata.
|
|
60
|
+
*
|
|
61
|
+
* ## Maintenance — how this list stays true
|
|
62
|
+
*
|
|
63
|
+
* `__tests__/dashboard-widget-options-census.test.ts` re-runs the census on
|
|
64
|
+
* every test run: it re-derives the five declared keys from the installed
|
|
65
|
+
* `@objectstack/spec`, re-extracts the `options.<key>` reads from
|
|
66
|
+
* `DatasetWidget.tsx` source text (and fails loudly if that file gains a
|
|
67
|
+
* consumption shape the extractor cannot see — a spread, a destructuring, a
|
|
68
|
+
* computed access), re-checks the sub-caption read site, and trips on any NEW
|
|
69
|
+
* file in `packages/*\/src` or `apps/*\/src` that starts reading
|
|
70
|
+
* `widget.options`. A renderer change that adds or removes a consumed key
|
|
71
|
+
* fails that test until this list is updated — the cost of keeping this
|
|
72
|
+
* warning honest is editing ONE array below plus re-reading the census notes.
|
|
73
|
+
*/
|
|
74
|
+
import type { Diagnostic, SchemaElement } from './types.js';
|
|
75
|
+
/** The diagnostic `code` — also the id `suppressWarnings` suppresses. */
|
|
76
|
+
export declare const UNCONSUMED_WIDGET_OPTION = "unconsumed-widget-option";
|
|
77
|
+
/**
|
|
78
|
+
* Component types that host a dashboard `widgets` array. Both resolve to the
|
|
79
|
+
* surfaces measured by the census above (`DashboardRenderer`,
|
|
80
|
+
* `DashboardGridLayout`), which share one dispatch (`widgetDispatch.ts`).
|
|
81
|
+
*/
|
|
82
|
+
export declare const DASHBOARD_WIDGET_HOST_TYPES: ReadonlySet<string>;
|
|
83
|
+
/**
|
|
84
|
+
* The accepted set: every `options` key with a renderer read site on the
|
|
85
|
+
* dataset-bound path, plus the sub-caption convention key. Alphabetical; the
|
|
86
|
+
* warning message prints it verbatim. Derivation and evidence: file header.
|
|
87
|
+
*/
|
|
88
|
+
export declare const CONSUMED_WIDGET_OPTION_KEYS: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* Diagnostics for `options` keys no renderer consumes, over one dashboard-host
|
|
91
|
+
* node's `widgets` array. Pure and shallow by design: it never descends into
|
|
92
|
+
* `children` (the caller's walk owns that) and answers `[]` for every shape
|
|
93
|
+
* outside its census — see the scope notes in the file header.
|
|
94
|
+
*/
|
|
95
|
+
export declare function checkDashboardWidgetOptions(node: SchemaElement): Diagnostic[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** The diagnostic `code` — also the id `suppressWarnings` suppresses. */
|
|
2
|
+
export const UNCONSUMED_WIDGET_OPTION = 'unconsumed-widget-option';
|
|
3
|
+
/**
|
|
4
|
+
* Component types that host a dashboard `widgets` array. Both resolve to the
|
|
5
|
+
* surfaces measured by the census above (`DashboardRenderer`,
|
|
6
|
+
* `DashboardGridLayout`), which share one dispatch (`widgetDispatch.ts`).
|
|
7
|
+
*/
|
|
8
|
+
export const DASHBOARD_WIDGET_HOST_TYPES = new Set([
|
|
9
|
+
'dashboard',
|
|
10
|
+
'dashboard-grid',
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* The accepted set: every `options` key with a renderer read site on the
|
|
14
|
+
* dataset-bound path, plus the sub-caption convention key. Alphabetical; the
|
|
15
|
+
* warning message prints it verbatim. Derivation and evidence: file header.
|
|
16
|
+
*/
|
|
17
|
+
export const CONSUMED_WIDGET_OPTION_KEYS = [
|
|
18
|
+
'dateGranularity',
|
|
19
|
+
'description',
|
|
20
|
+
'limit',
|
|
21
|
+
'sortBy',
|
|
22
|
+
'sortOrder',
|
|
23
|
+
'stageOrder',
|
|
24
|
+
];
|
|
25
|
+
const CONSUMED = new Set(CONSUMED_WIDGET_OPTION_KEYS);
|
|
26
|
+
const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
27
|
+
/** The parser's deferred-expression marker — opaque here, never evaluated. */
|
|
28
|
+
const isExpr = (v) => isPlainObject(v) && '$expr' in v;
|
|
29
|
+
/**
|
|
30
|
+
* Diagnostics for `options` keys no renderer consumes, over one dashboard-host
|
|
31
|
+
* node's `widgets` array. Pure and shallow by design: it never descends into
|
|
32
|
+
* `children` (the caller's walk owns that) and answers `[]` for every shape
|
|
33
|
+
* outside its census — see the scope notes in the file header.
|
|
34
|
+
*/
|
|
35
|
+
export function checkDashboardWidgetOptions(node) {
|
|
36
|
+
if (!DASHBOARD_WIDGET_HOST_TYPES.has(node.type))
|
|
37
|
+
return [];
|
|
38
|
+
const widgets = node.widgets;
|
|
39
|
+
if (!Array.isArray(widgets))
|
|
40
|
+
return [];
|
|
41
|
+
const diagnostics = [];
|
|
42
|
+
widgets.forEach((widget, index) => {
|
|
43
|
+
if (!isPlainObject(widget) || isExpr(widget))
|
|
44
|
+
return;
|
|
45
|
+
// Legacy component format: `options` is not part of that contract.
|
|
46
|
+
if (widget.component !== undefined)
|
|
47
|
+
return;
|
|
48
|
+
// Only the dataset-bound (spec-legal) path is censused — see file header.
|
|
49
|
+
if (widget.dataset === undefined || widget.dataset === null || widget.dataset === '')
|
|
50
|
+
return;
|
|
51
|
+
const options = widget.options;
|
|
52
|
+
if (!isPlainObject(options) || isExpr(options))
|
|
53
|
+
return;
|
|
54
|
+
if (Array.isArray(widget.suppressWarnings) &&
|
|
55
|
+
widget.suppressWarnings.includes(UNCONSUMED_WIDGET_OPTION)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const label = typeof widget.id === 'string' && widget.id !== '' ? widget.id : `#${index}`;
|
|
59
|
+
const widgetType = typeof widget.type === 'string' && widget.type !== '' ? widget.type : 'widget';
|
|
60
|
+
for (const key of Object.keys(options)) {
|
|
61
|
+
if (CONSUMED.has(key))
|
|
62
|
+
continue;
|
|
63
|
+
diagnostics.push({
|
|
64
|
+
severity: 'warning',
|
|
65
|
+
code: UNCONSUMED_WIDGET_OPTION,
|
|
66
|
+
message: `<${node.type}> widget "${label}" (${widgetType}): options.${key} reaches no renderer — ` +
|
|
67
|
+
`dashboard widget renderers read only: ${CONSUMED_WIDGET_OPTION_KEYS.join(', ')}`,
|
|
68
|
+
tag: node.type,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return diagnostics;
|
|
73
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export * from './types.js';
|
|
9
9
|
export { parseJsx, interpretBrace } from './parse.js';
|
|
10
|
+
export { HTML_TIER_NODE, isHtmlTierNode, markHtmlTierNode } from './provenance.js';
|
|
10
11
|
export { validateTree } from './validate.js';
|
|
12
|
+
export { checkDashboardWidgetOptions, CONSUMED_WIDGET_OPTION_KEYS, DASHBOARD_WIDGET_HOST_TYPES, UNCONSUMED_WIDGET_OPTION, } from './dashboard-widget-options.js';
|
|
11
13
|
export { generateDts, propsName, generateBlockList } from './codegen.js';
|
|
12
14
|
export type { CodegenOptions } from './codegen.js';
|
|
15
|
+
export { inputTypeArms, canonicalizeInputType, MANIFEST_INPUT_TYPES } from './input-type.js';
|
|
13
16
|
import type { Diagnostic, Manifest, SchemaElement, ManifestValidationResult } from './types.js';
|
|
14
17
|
export interface CompileResult {
|
|
15
18
|
tree: SchemaElement | null;
|
|
@@ -34,7 +37,14 @@ export interface RegistryConfigLike {
|
|
|
34
37
|
category?: string;
|
|
35
38
|
inputs?: Array<{
|
|
36
39
|
name: string;
|
|
37
|
-
|
|
40
|
+
/**
|
|
41
|
+
* One coarse kind, or the arms of a union (objectui#3832). Typed loosely
|
|
42
|
+
* (`string`) on purpose — this interface is the STRUCTURAL boundary that
|
|
43
|
+
* keeps this package free of a dependency on the registry, so an
|
|
44
|
+
* off-vocabulary value has to be representable here and is normalized by
|
|
45
|
+
* `canonicalizeInputType` on the way in.
|
|
46
|
+
*/
|
|
47
|
+
type: string | string[];
|
|
38
48
|
required?: boolean;
|
|
39
49
|
enum?: Array<string | {
|
|
40
50
|
value: unknown;
|
package/dist/index.js
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export * from './types.js';
|
|
9
9
|
export { parseJsx, interpretBrace } from './parse.js';
|
|
10
|
+
export { HTML_TIER_NODE, isHtmlTierNode, markHtmlTierNode } from './provenance.js';
|
|
10
11
|
export { validateTree } from './validate.js';
|
|
12
|
+
export { checkDashboardWidgetOptions, CONSUMED_WIDGET_OPTION_KEYS, DASHBOARD_WIDGET_HOST_TYPES, UNCONSUMED_WIDGET_OPTION, } from './dashboard-widget-options.js';
|
|
11
13
|
export { generateDts, propsName, generateBlockList } from './codegen.js';
|
|
14
|
+
export { inputTypeArms, canonicalizeInputType, MANIFEST_INPUT_TYPES } from './input-type.js';
|
|
12
15
|
import { parseJsx } from './parse.js';
|
|
13
16
|
import { validateTree } from './validate.js';
|
|
17
|
+
import { canonicalizeInputType } from './input-type.js';
|
|
14
18
|
/**
|
|
15
19
|
* The authoritative pipeline: parse (with the manifest's tags as the whitelist)
|
|
16
20
|
* → validate against the manifest → derive `requires` + binding sites.
|
|
@@ -52,19 +56,10 @@ export function assertFullyLoaded(configs) {
|
|
|
52
56
|
`so every prop an author passes them would be reported as unknown.\n` +
|
|
53
57
|
`Add an eager \`import\` of each block's plugin to the generator entry.`);
|
|
54
58
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'enum',
|
|
60
|
-
'array',
|
|
61
|
-
'object',
|
|
62
|
-
'color',
|
|
63
|
-
'date',
|
|
64
|
-
'code',
|
|
65
|
-
'file',
|
|
66
|
-
'slot',
|
|
67
|
-
]);
|
|
59
|
+
/* The arm vocabulary and the two projections over it now live in
|
|
60
|
+
* `input-type.ts` — `manifestFromConfigs` below, `validateTree` and the codegen
|
|
61
|
+
* all read `ManifestInput.type` and must agree on how, since it holds one arm
|
|
62
|
+
* or an array of them (objectui#3832). */
|
|
68
63
|
/**
|
|
69
64
|
* Project registry configs into the SDUI manifest.
|
|
70
65
|
*
|
|
@@ -95,7 +90,7 @@ export function manifestFromConfigs(configs, opts = {}) {
|
|
|
95
90
|
isContainer: c.isContainer,
|
|
96
91
|
inputs: (c.inputs ?? []).map((i) => ({
|
|
97
92
|
name: i.name,
|
|
98
|
-
type: (
|
|
93
|
+
type: canonicalizeInputType(i.type),
|
|
99
94
|
required: i.required,
|
|
100
95
|
enum: i.enum,
|
|
101
96
|
binding: i.binding,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — the arms of a manifest input's coarse type (objectui#3832)
|
|
3
|
+
*
|
|
4
|
+
* `ManifestInput.type` carries ONE coarse kind, or an ARRAY of kinds when the
|
|
5
|
+
* key's contract is a union. Every reader of that field needs the same two
|
|
6
|
+
* decisions made the same way — how to see the arms, and which single form to
|
|
7
|
+
* publish — so they live here once instead of at each call site. A reader that
|
|
8
|
+
* forgets is not loud: `switch (input.type)` handed an array falls through to
|
|
9
|
+
* the default branch and reports NOTHING, which looks exactly like a value that
|
|
10
|
+
* validated cleanly.
|
|
11
|
+
*/
|
|
12
|
+
import type { ManifestInput, ManifestInputType } from './types.js';
|
|
13
|
+
/** The eleven coarse kinds, as a runtime set for guarding untyped input. */
|
|
14
|
+
export declare const MANIFEST_INPUT_TYPES: ReadonlySet<string>;
|
|
15
|
+
/**
|
|
16
|
+
* The declared arms of an input's coarse type, always as an array.
|
|
17
|
+
*
|
|
18
|
+
* Exported (not merely internal) so a third-party manifest consumer — a
|
|
19
|
+
* designer panel, a codegen, a validator of its own — reads the arms through
|
|
20
|
+
* the same accessor this package's own gate does, rather than re-deriving the
|
|
21
|
+
* `Array.isArray` branch and getting it subtly wrong on the union form.
|
|
22
|
+
*/
|
|
23
|
+
export declare function inputTypeArms(type: ManifestInput['type'] | undefined): ManifestInputType[];
|
|
24
|
+
/**
|
|
25
|
+
* Project a DECLARED type (which may come from an untyped registry config) into
|
|
26
|
+
* the canonical published form: a bare string for one arm, an array for a real
|
|
27
|
+
* union.
|
|
28
|
+
*
|
|
29
|
+
* Two deliberate asymmetries between the single and array forms:
|
|
30
|
+
*
|
|
31
|
+
* - A single unrecognized kind still becomes `'string'`. That coercion predates
|
|
32
|
+
* the union work and is kept exactly: with one arm there is no other
|
|
33
|
+
* information to fall back on, and a manifest whose `type` is off-vocabulary
|
|
34
|
+
* would make every consumer's switch silently inert.
|
|
35
|
+
* - An unrecognized arm INSIDE an array is DROPPED, not coerced. Promoting it
|
|
36
|
+
* to `'string'` would publish an arm the author never declared — a widening
|
|
37
|
+
* invented by the serializer — and the surviving arms already carry the
|
|
38
|
+
* declaration. If dropping empties the array, the single-arm fallback applies
|
|
39
|
+
* so the output is always a valid manifest.
|
|
40
|
+
*
|
|
41
|
+
* Collapsing one arm to the bare string is what keeps this a backward-compatible
|
|
42
|
+
* extension of `sdui.manifest.json`: every input declared today serializes to
|
|
43
|
+
* the byte-identical entry it does now, and arrays appear only where a union was
|
|
44
|
+
* really declared.
|
|
45
|
+
*/
|
|
46
|
+
export declare function canonicalizeInputType(type: unknown): ManifestInputType | ManifestInputType[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — the arms of a manifest input's coarse type (objectui#3832)
|
|
3
|
+
*
|
|
4
|
+
* `ManifestInput.type` carries ONE coarse kind, or an ARRAY of kinds when the
|
|
5
|
+
* key's contract is a union. Every reader of that field needs the same two
|
|
6
|
+
* decisions made the same way — how to see the arms, and which single form to
|
|
7
|
+
* publish — so they live here once instead of at each call site. A reader that
|
|
8
|
+
* forgets is not loud: `switch (input.type)` handed an array falls through to
|
|
9
|
+
* the default branch and reports NOTHING, which looks exactly like a value that
|
|
10
|
+
* validated cleanly.
|
|
11
|
+
*/
|
|
12
|
+
/** The eleven coarse kinds, as a runtime set for guarding untyped input. */
|
|
13
|
+
export const MANIFEST_INPUT_TYPES = new Set([
|
|
14
|
+
'string',
|
|
15
|
+
'number',
|
|
16
|
+
'boolean',
|
|
17
|
+
'enum',
|
|
18
|
+
'array',
|
|
19
|
+
'object',
|
|
20
|
+
'color',
|
|
21
|
+
'date',
|
|
22
|
+
'code',
|
|
23
|
+
'file',
|
|
24
|
+
'slot',
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* The declared arms of an input's coarse type, always as an array.
|
|
28
|
+
*
|
|
29
|
+
* Exported (not merely internal) so a third-party manifest consumer — a
|
|
30
|
+
* designer panel, a codegen, a validator of its own — reads the arms through
|
|
31
|
+
* the same accessor this package's own gate does, rather than re-deriving the
|
|
32
|
+
* `Array.isArray` branch and getting it subtly wrong on the union form.
|
|
33
|
+
*/
|
|
34
|
+
export function inputTypeArms(type) {
|
|
35
|
+
if (type === undefined)
|
|
36
|
+
return [];
|
|
37
|
+
return Array.isArray(type) ? type : [type];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Project a DECLARED type (which may come from an untyped registry config) into
|
|
41
|
+
* the canonical published form: a bare string for one arm, an array for a real
|
|
42
|
+
* union.
|
|
43
|
+
*
|
|
44
|
+
* Two deliberate asymmetries between the single and array forms:
|
|
45
|
+
*
|
|
46
|
+
* - A single unrecognized kind still becomes `'string'`. That coercion predates
|
|
47
|
+
* the union work and is kept exactly: with one arm there is no other
|
|
48
|
+
* information to fall back on, and a manifest whose `type` is off-vocabulary
|
|
49
|
+
* would make every consumer's switch silently inert.
|
|
50
|
+
* - An unrecognized arm INSIDE an array is DROPPED, not coerced. Promoting it
|
|
51
|
+
* to `'string'` would publish an arm the author never declared — a widening
|
|
52
|
+
* invented by the serializer — and the surviving arms already carry the
|
|
53
|
+
* declaration. If dropping empties the array, the single-arm fallback applies
|
|
54
|
+
* so the output is always a valid manifest.
|
|
55
|
+
*
|
|
56
|
+
* Collapsing one arm to the bare string is what keeps this a backward-compatible
|
|
57
|
+
* extension of `sdui.manifest.json`: every input declared today serializes to
|
|
58
|
+
* the byte-identical entry it does now, and arrays appear only where a union was
|
|
59
|
+
* really declared.
|
|
60
|
+
*/
|
|
61
|
+
export function canonicalizeInputType(type) {
|
|
62
|
+
const arms = (Array.isArray(type) ? type : [type]).filter((arm) => typeof arm === 'string' && MANIFEST_INPUT_TYPES.has(arm));
|
|
63
|
+
const distinct = [...new Set(arms)];
|
|
64
|
+
if (distinct.length === 0)
|
|
65
|
+
return 'string';
|
|
66
|
+
if (distinct.length === 1)
|
|
67
|
+
return distinct[0];
|
|
68
|
+
return distinct;
|
|
69
|
+
}
|
package/dist/parse.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* child := element | text | jsx-block-comment
|
|
14
14
|
* tag := [A-Za-z][A-Za-z0-9:_-]star (matches registry keys)
|
|
15
15
|
*/
|
|
16
|
+
import { markHtmlTierNode } from './provenance.js';
|
|
16
17
|
/** Event handlers and raw-HTML injection are never allowed (parse ≠ execute). */
|
|
17
18
|
const EVENT_ATTR = /^on[A-Z]/;
|
|
18
19
|
const FORBIDDEN_ATTRS = new Set(['dangerouslySetInnerHTML', 'ref', 'key']);
|
|
@@ -98,7 +99,15 @@ class Parser {
|
|
|
98
99
|
else {
|
|
99
100
|
this.error('unterminated-open-tag', `Unterminated <${tag}> open tag`, start, tag);
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
+
// Stamp html-tier provenance at the point of production (objectui#4000).
|
|
103
|
+
// Renderers whose type carries authoring advice aimed at the JSON surface
|
|
104
|
+
// read this to tell "the author wrote a tag in OUR tier, and there is no
|
|
105
|
+
// other spelling available to them" apart from "the author wrote this type
|
|
106
|
+
// in JSON, where the advice applies". Symbol-keyed, so it is invisible to
|
|
107
|
+
// JSON, to the DOM, and to anything an authored document could forge — see
|
|
108
|
+
// provenance.ts. Values reached through a braced attribute are NOT marked:
|
|
109
|
+
// that JSON was written by hand, and the JSON surface's advice does apply.
|
|
110
|
+
const node = markHtmlTierNode({ type: tag, ...props });
|
|
102
111
|
if (children && children.length)
|
|
103
112
|
node.children = children;
|
|
104
113
|
return node;
|
|
@@ -178,10 +187,31 @@ class Parser {
|
|
|
178
187
|
continue;
|
|
179
188
|
}
|
|
180
189
|
// text
|
|
190
|
+
//
|
|
191
|
+
// HTML collapses a whitespace run to ONE space; it does not delete it. A
|
|
192
|
+
// bare `.trim()` here deleted the space that separates a text run from an
|
|
193
|
+
// adjacent sibling element, so `A <strong>x</strong> page` compiled to
|
|
194
|
+
// `A`/`page` and rendered as `Axpage` (objectui#5661). The rule: collapse
|
|
195
|
+
// the run, then keep a single leading space only when a sibling precedes
|
|
196
|
+
// the run, and a single trailing space only when a sibling element
|
|
197
|
+
// follows it. At the parent's own start/end the edge space is still
|
|
198
|
+
// dropped, so `<p> hi </p>` stays `hi`. A whitespace-only run survives
|
|
199
|
+
// as one space only when it sits BETWEEN siblings — that is the bounded
|
|
200
|
+
// over-generosity of this rule inside block containers like `<ul>`,
|
|
201
|
+
// pinned in the tests.
|
|
181
202
|
const text = this.readTextRun();
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
203
|
+
const collapsed = text.replace(/\s+/g, ' ');
|
|
204
|
+
const core = collapsed.trim();
|
|
205
|
+
const afterSibling = children.length > 0;
|
|
206
|
+
const beforeElement = this.peek() === '<' && !this.src.startsWith('</', this.pos);
|
|
207
|
+
if (core) {
|
|
208
|
+
const lead = afterSibling && collapsed.startsWith(' ') ? ' ' : '';
|
|
209
|
+
const trail = beforeElement && collapsed.endsWith(' ') ? ' ' : '';
|
|
210
|
+
children.push(`${lead}${core}${trail}`);
|
|
211
|
+
}
|
|
212
|
+
else if (collapsed && afterSibling && beforeElement) {
|
|
213
|
+
children.push(' ');
|
|
214
|
+
}
|
|
185
215
|
}
|
|
186
216
|
return children;
|
|
187
217
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — html-tier provenance (ADR-0080, objectui#4000)
|
|
3
|
+
*
|
|
4
|
+
* Records the one fact a renderer cannot otherwise recover: whether the node it
|
|
5
|
+
* is about to render was EMITTED BY THIS PARSER (an author's `kind:'html'`
|
|
6
|
+
* source, compiled tag-name-straight-through) or AUTHORED AS JSON metadata.
|
|
7
|
+
*
|
|
8
|
+
* Why a renderer needs it: the two surfaces have different vocabularies. A type
|
|
9
|
+
* the JSON surface is steering authors away from can still be a first-class,
|
|
10
|
+
* permanently-supported tag in the html tier — the plain box element is exactly
|
|
11
|
+
* that. Advice aimed at the JSON surface, delivered to an html-tier author, is
|
|
12
|
+
* advice they cannot act on: in that tier there is no other spelling. The
|
|
13
|
+
* deprecation notice fired at them anyway (objectui#4000), which is also why the
|
|
14
|
+
* type could never be retired — the engine's own compiler keeps emitting it.
|
|
15
|
+
*
|
|
16
|
+
* The judgement therefore has to be PROVENANCE, established by the producer,
|
|
17
|
+
* not a guess made at the consumer from the node's shape.
|
|
18
|
+
*
|
|
19
|
+
* ## Why a symbol, and why `Symbol.for`
|
|
20
|
+
*
|
|
21
|
+
* The marker is deliberately NOT a JSON-visible key:
|
|
22
|
+
*
|
|
23
|
+
* - `JSON.stringify` skips symbol keys, so provenance never lands in a
|
|
24
|
+
* persisted tree. That is the point, not a limitation: "our parser produced
|
|
25
|
+
* this node in this process" is not a fact about the saved document, and a
|
|
26
|
+
* saved document must not be able to REPLAY it. A string key like
|
|
27
|
+
* `_provenance: 'html'` would be copyable — an authored (or AI-generated)
|
|
28
|
+
* JSON page could paste it in and buy itself silence from a notice that is
|
|
29
|
+
* meant for it. Provenance that authored metadata can forge is not
|
|
30
|
+
* provenance.
|
|
31
|
+
* - `Object.keys` / `for...in` skip symbol keys, so the marker cannot leak out
|
|
32
|
+
* of a renderer onto a host element as a stray DOM attribute.
|
|
33
|
+
*
|
|
34
|
+
* It IS enumerable, which matters more than it looks: `SchemaRenderer` hands
|
|
35
|
+
* renderers a shallow copy of the node (`{ ...schema }`), and object spread
|
|
36
|
+
* copies own ENUMERABLE symbol keys while dropping non-enumerable ones. A
|
|
37
|
+
* non-enumerable marker would be lost in that copy and the misfire would come
|
|
38
|
+
* back — silently, and only in the render path. `render.test.tsx` and the
|
|
39
|
+
* components-side pin both go through the real `SchemaRenderer` for this reason.
|
|
40
|
+
*
|
|
41
|
+
* `Symbol.for` (the cross-realm registry) rather than a module-local `Symbol()`:
|
|
42
|
+
* this package ships as both ESM and CJS and is consumed from several packages,
|
|
43
|
+
* so two module instances are possible. Two local symbols would not compare
|
|
44
|
+
* equal, and the failure mode of that mismatch is the ORIGINAL BUG returning
|
|
45
|
+
* with no error anywhere. A registry key cannot drift.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* The provenance marker itself. Exported for tests and for any consumer that
|
|
49
|
+
* needs to reason about the key; prefer {@link isHtmlTierNode} for reading.
|
|
50
|
+
*/
|
|
51
|
+
export declare const HTML_TIER_NODE: symbol;
|
|
52
|
+
/**
|
|
53
|
+
* Stamp a node as html-tier output. Called by the parser as it builds each
|
|
54
|
+
* element — the producer is the only place that knows this for certain.
|
|
55
|
+
*/
|
|
56
|
+
export declare function markHtmlTierNode<T extends object>(node: T): T;
|
|
57
|
+
/**
|
|
58
|
+
* True when this node came out of the html tier's parser.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately a plain boolean read of the marker — no fallback to a
|
|
61
|
+
* string-keyed spelling, no shape heuristic ("looks like a raw tag"). Either
|
|
62
|
+
* the producer marked it or it did not; a consumer-side guess is precisely what
|
|
63
|
+
* this replaces.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isHtmlTierNode(node: unknown): boolean;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — html-tier provenance (ADR-0080, objectui#4000)
|
|
3
|
+
*
|
|
4
|
+
* Records the one fact a renderer cannot otherwise recover: whether the node it
|
|
5
|
+
* is about to render was EMITTED BY THIS PARSER (an author's `kind:'html'`
|
|
6
|
+
* source, compiled tag-name-straight-through) or AUTHORED AS JSON metadata.
|
|
7
|
+
*
|
|
8
|
+
* Why a renderer needs it: the two surfaces have different vocabularies. A type
|
|
9
|
+
* the JSON surface is steering authors away from can still be a first-class,
|
|
10
|
+
* permanently-supported tag in the html tier — the plain box element is exactly
|
|
11
|
+
* that. Advice aimed at the JSON surface, delivered to an html-tier author, is
|
|
12
|
+
* advice they cannot act on: in that tier there is no other spelling. The
|
|
13
|
+
* deprecation notice fired at them anyway (objectui#4000), which is also why the
|
|
14
|
+
* type could never be retired — the engine's own compiler keeps emitting it.
|
|
15
|
+
*
|
|
16
|
+
* The judgement therefore has to be PROVENANCE, established by the producer,
|
|
17
|
+
* not a guess made at the consumer from the node's shape.
|
|
18
|
+
*
|
|
19
|
+
* ## Why a symbol, and why `Symbol.for`
|
|
20
|
+
*
|
|
21
|
+
* The marker is deliberately NOT a JSON-visible key:
|
|
22
|
+
*
|
|
23
|
+
* - `JSON.stringify` skips symbol keys, so provenance never lands in a
|
|
24
|
+
* persisted tree. That is the point, not a limitation: "our parser produced
|
|
25
|
+
* this node in this process" is not a fact about the saved document, and a
|
|
26
|
+
* saved document must not be able to REPLAY it. A string key like
|
|
27
|
+
* `_provenance: 'html'` would be copyable — an authored (or AI-generated)
|
|
28
|
+
* JSON page could paste it in and buy itself silence from a notice that is
|
|
29
|
+
* meant for it. Provenance that authored metadata can forge is not
|
|
30
|
+
* provenance.
|
|
31
|
+
* - `Object.keys` / `for...in` skip symbol keys, so the marker cannot leak out
|
|
32
|
+
* of a renderer onto a host element as a stray DOM attribute.
|
|
33
|
+
*
|
|
34
|
+
* It IS enumerable, which matters more than it looks: `SchemaRenderer` hands
|
|
35
|
+
* renderers a shallow copy of the node (`{ ...schema }`), and object spread
|
|
36
|
+
* copies own ENUMERABLE symbol keys while dropping non-enumerable ones. A
|
|
37
|
+
* non-enumerable marker would be lost in that copy and the misfire would come
|
|
38
|
+
* back — silently, and only in the render path. `render.test.tsx` and the
|
|
39
|
+
* components-side pin both go through the real `SchemaRenderer` for this reason.
|
|
40
|
+
*
|
|
41
|
+
* `Symbol.for` (the cross-realm registry) rather than a module-local `Symbol()`:
|
|
42
|
+
* this package ships as both ESM and CJS and is consumed from several packages,
|
|
43
|
+
* so two module instances are possible. Two local symbols would not compare
|
|
44
|
+
* equal, and the failure mode of that mismatch is the ORIGINAL BUG returning
|
|
45
|
+
* with no error anywhere. A registry key cannot drift.
|
|
46
|
+
*/
|
|
47
|
+
/** Registry key for the marker. Stable — it is a cross-instance contract. */
|
|
48
|
+
const HTML_TIER_NODE_KEY = '@object-ui/sdui-parser.html-tier-node';
|
|
49
|
+
/**
|
|
50
|
+
* The provenance marker itself. Exported for tests and for any consumer that
|
|
51
|
+
* needs to reason about the key; prefer {@link isHtmlTierNode} for reading.
|
|
52
|
+
*/
|
|
53
|
+
export const HTML_TIER_NODE = Symbol.for(HTML_TIER_NODE_KEY);
|
|
54
|
+
/**
|
|
55
|
+
* Stamp a node as html-tier output. Called by the parser as it builds each
|
|
56
|
+
* element — the producer is the only place that knows this for certain.
|
|
57
|
+
*/
|
|
58
|
+
export function markHtmlTierNode(node) {
|
|
59
|
+
node[HTML_TIER_NODE] = true;
|
|
60
|
+
return node;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* True when this node came out of the html tier's parser.
|
|
64
|
+
*
|
|
65
|
+
* Deliberately a plain boolean read of the marker — no fallback to a
|
|
66
|
+
* string-keyed spelling, no shape heuristic ("looks like a raw tag"). Either
|
|
67
|
+
* the producer marked it or it did not; a consumer-side guess is precisely what
|
|
68
|
+
* this replaces.
|
|
69
|
+
*/
|
|
70
|
+
export function isHtmlTierNode(node) {
|
|
71
|
+
return (typeof node === 'object' &&
|
|
72
|
+
node !== null &&
|
|
73
|
+
node[HTML_TIER_NODE] === true);
|
|
74
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ObjectUI — SDUI JSX-source parser (ADR-0080)
|
|
3
3
|
*
|
|
4
4
|
* Types shared by the constrained JSX-source compiler. The parser turns a
|
|
5
|
-
* constrained JSX
|
|
5
|
+
* constrained JSX *text* into the existing SDUI `SchemaNode`
|
|
6
6
|
* tree. It PARSES — it never executes. No `import`, no `eval`, no JS.
|
|
7
7
|
*/
|
|
8
8
|
/** A node in the compiled SDUI tree. Mirrors `@object-ui/types` BaseSchema. */
|
|
@@ -39,7 +39,20 @@ export interface ParseResult {
|
|
|
39
39
|
export type ManifestInputType = 'string' | 'number' | 'boolean' | 'enum' | 'array' | 'object' | 'color' | 'date' | 'code' | 'file' | 'slot';
|
|
40
40
|
export interface ManifestInput {
|
|
41
41
|
name: string;
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The input's coarse type: ONE kind, or an ARRAY of kinds when the key's
|
|
44
|
+
* contract is a union (objectui#3832).
|
|
45
|
+
*
|
|
46
|
+
* A value passes {@link validateTree}'s coarse check when ANY arm accepts it,
|
|
47
|
+
* and is reported when none does — the array widens what is legal, it does
|
|
48
|
+
* not switch the check off.
|
|
49
|
+
*
|
|
50
|
+
* The single-kind form is unchanged and stays the canonical spelling for a
|
|
51
|
+
* one-arm key: `manifestFromConfigs` collapses a one-element array back to
|
|
52
|
+
* the bare string, so a manifest gains arrays only where a union was really
|
|
53
|
+
* declared and every already-published entry serializes byte-identically.
|
|
54
|
+
*/
|
|
55
|
+
type: ManifestInputType | ManifestInputType[];
|
|
43
56
|
required?: boolean;
|
|
44
57
|
/** allowed values for `enum` inputs */
|
|
45
58
|
enum?: Array<string | {
|
package/dist/types.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ObjectUI — SDUI JSX-source parser (ADR-0080)
|
|
3
3
|
*
|
|
4
4
|
* Types shared by the constrained JSX-source compiler. The parser turns a
|
|
5
|
-
* constrained JSX
|
|
5
|
+
* constrained JSX *text* into the existing SDUI `SchemaNode`
|
|
6
6
|
* tree. It PARSES — it never executes. No `import`, no `eval`, no JS.
|
|
7
7
|
*/
|
|
8
8
|
export {};
|
package/dist/validate.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* and binding sites the SERVER must resolve against object schema (we cannot
|
|
7
7
|
* resolve objects/fields here — that check is framework-side by design).
|
|
8
8
|
*/
|
|
9
|
+
import { inputTypeArms } from './input-type.js';
|
|
10
|
+
import { checkDashboardWidgetOptions } from './dashboard-widget-options.js';
|
|
9
11
|
/** Base props every node may carry (mirrors BaseSchema) — never "unknown prop". */
|
|
10
12
|
const BASE_PROPS = new Set([
|
|
11
13
|
'type',
|
|
@@ -82,6 +84,13 @@ export function validateTree(tree, manifest) {
|
|
|
82
84
|
tag: node.type,
|
|
83
85
|
});
|
|
84
86
|
}
|
|
87
|
+
// Dashboard widgets: an `options` key riding the spec's `.passthrough()`
|
|
88
|
+
// that no renderer consumes is legal, silent and inert — warn, naming
|
|
89
|
+
// the consumed set (objectui#5709 ruling; census + scope in
|
|
90
|
+
// `./dashboard-widget-options.ts`). Like `not-a-container`, this runs
|
|
91
|
+
// only for a component the manifest knows: an unresolved tag already
|
|
92
|
+
// drew `unknown-component`, and deep diagnostics on it would be noise.
|
|
93
|
+
diagnostics.push(...checkDashboardWidgetOptions(node));
|
|
85
94
|
}
|
|
86
95
|
if (node.children)
|
|
87
96
|
node.children.forEach(visit);
|
|
@@ -90,42 +99,98 @@ export function validateTree(tree, manifest) {
|
|
|
90
99
|
visit(tree);
|
|
91
100
|
return { diagnostics, requires: [...requires], bindings };
|
|
92
101
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
/** The values an `enum` arm admits, flattened from either declaration form. */
|
|
103
|
+
const enumValues = (input) => (input.enum ?? []).map((e) => (typeof e === 'object' ? e.value : e));
|
|
104
|
+
/**
|
|
105
|
+
* Does ONE coarse arm accept this value?
|
|
106
|
+
*
|
|
107
|
+
* An arm outside the vocabulary — and `'slot'`, which describes a child
|
|
108
|
+
* position rather than a value — accepts everything, preserving the old
|
|
109
|
+
* `default: return null` branch: those inputs never drew a diagnostic and must
|
|
110
|
+
* not start now.
|
|
111
|
+
*/
|
|
112
|
+
function armAccepts(arm, input, value) {
|
|
113
|
+
switch (arm) {
|
|
101
114
|
case 'number':
|
|
102
|
-
return typeof value === 'number'
|
|
115
|
+
return typeof value === 'number';
|
|
103
116
|
case 'boolean':
|
|
104
|
-
return typeof value === 'boolean'
|
|
117
|
+
return typeof value === 'boolean';
|
|
105
118
|
case 'string':
|
|
106
119
|
case 'color':
|
|
107
120
|
case 'date':
|
|
108
121
|
case 'code':
|
|
109
122
|
case 'file':
|
|
110
|
-
return typeof value === 'string'
|
|
123
|
+
return typeof value === 'string';
|
|
111
124
|
case 'array':
|
|
112
|
-
return Array.isArray(value)
|
|
125
|
+
return Array.isArray(value);
|
|
113
126
|
case 'object':
|
|
114
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
case 'enum': {
|
|
118
|
-
const allowed = (input.enum ?? []).map((e) => (typeof e === 'object' ? e.value : e));
|
|
119
|
-
return allowed.includes(value)
|
|
120
|
-
? null
|
|
121
|
-
: {
|
|
122
|
-
severity: 'error',
|
|
123
|
-
code: 'invalid-enum',
|
|
124
|
-
message: `<${tag}> prop "${input.name}"=${JSON.stringify(value)} is not one of ${JSON.stringify(allowed)}`,
|
|
125
|
-
tag,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
127
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
128
|
+
case 'enum':
|
|
129
|
+
return enumValues(input).includes(value);
|
|
128
130
|
default:
|
|
129
|
-
return
|
|
131
|
+
return true;
|
|
130
132
|
}
|
|
131
133
|
}
|
|
134
|
+
/** How one arm is named in a diagnostic message. */
|
|
135
|
+
function armExpectation(arm, input) {
|
|
136
|
+
switch (arm) {
|
|
137
|
+
case 'number':
|
|
138
|
+
return 'a number';
|
|
139
|
+
case 'boolean':
|
|
140
|
+
return 'a boolean';
|
|
141
|
+
case 'array':
|
|
142
|
+
return 'an array';
|
|
143
|
+
case 'object':
|
|
144
|
+
return 'an object';
|
|
145
|
+
case 'enum':
|
|
146
|
+
return `one of ${JSON.stringify(enumValues(input))}`;
|
|
147
|
+
default:
|
|
148
|
+
return 'a string';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Coarse type check, over the arms an input declares (objectui#3832).
|
|
153
|
+
*
|
|
154
|
+
* ANY arm accepting the value clears the prop — that is what lets a key whose
|
|
155
|
+
* contract is a union (`string | number`, or a string plus an inline
|
|
156
|
+
* translation map) be declared honestly instead of picking one arm and having
|
|
157
|
+
* this function report the other arm's legal values.
|
|
158
|
+
*
|
|
159
|
+
* When NO arm accepts it the prop is still reported; a union widens what counts
|
|
160
|
+
* as legal, it does not turn the check off. Two properties of the reporting are
|
|
161
|
+
* deliberate:
|
|
162
|
+
*
|
|
163
|
+
* - A single-arm input produces the byte-identical diagnostic it always did,
|
|
164
|
+
* `invalid-enum` included. This change adds a form; it does not restate the
|
|
165
|
+
* old one.
|
|
166
|
+
* - A multi-arm input produces ONE diagnostic naming every arm, at the
|
|
167
|
+
* STRICTEST arm's severity — `error` when an `enum` arm is present, because
|
|
168
|
+
* an enum's closed list is the one fact this layer can be certain about, and
|
|
169
|
+
* a value outside it should not become dismissible merely because a second
|
|
170
|
+
* arm was added next to it. Its code is `type-mismatch` (not `invalid-enum`)
|
|
171
|
+
* since the reported fact is "fits none of the declared arms", and the
|
|
172
|
+
* message carries the allowed values so the author still sees the list.
|
|
173
|
+
*/
|
|
174
|
+
function checkType(tag, input, value) {
|
|
175
|
+
const arms = inputTypeArms(input.type);
|
|
176
|
+
if (arms.length === 0)
|
|
177
|
+
return null;
|
|
178
|
+
if (arms.some((arm) => armAccepts(arm, input, value)))
|
|
179
|
+
return null;
|
|
180
|
+
if (arms.length === 1 && arms[0] === 'enum') {
|
|
181
|
+
return {
|
|
182
|
+
severity: 'error',
|
|
183
|
+
code: 'invalid-enum',
|
|
184
|
+
message: `<${tag}> prop "${input.name}"=${JSON.stringify(value)} is not one of ${JSON.stringify(enumValues(input))}`,
|
|
185
|
+
tag,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
severity: arms.includes('enum') ? 'error' : 'warning',
|
|
190
|
+
code: 'type-mismatch',
|
|
191
|
+
message: `<${tag}> prop "${input.name}" expected ${arms
|
|
192
|
+
.map((arm) => armExpectation(arm, input))
|
|
193
|
+
.join(' or ')}`,
|
|
194
|
+
tag,
|
|
195
|
+
};
|
|
196
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/sdui-parser",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@objectstack/spec": "^17.0.0
|
|
29
|
-
"@object-ui/core": "17.
|
|
30
|
-
"@object-ui/react": "17.
|
|
28
|
+
"@objectstack/spec": "^17.0.0",
|
|
29
|
+
"@object-ui/core": "17.6.0",
|
|
30
|
+
"@object-ui/react": "17.6.0"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc",
|