@objectstack/sdui-parser 11.2.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/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +202 -0
- package/dist/index.d.mts +184 -0
- package/dist/index.d.ts +184 -0
- package/dist/index.js +523 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +489 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +41 -0
- package/src/__tests__/compile.test.ts +80 -0
- package/src/codegen.ts +138 -0
- package/src/index.ts +108 -0
- package/src/parse.ts +272 -0
- package/src/types.ts +95 -0
- package/src/validate.ts +144 -0
- package/tsconfig.json +9 -0
package/src/validate.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — SDUI tree validation against the registry manifest (ADR-0080 §3/§6)
|
|
3
|
+
*
|
|
4
|
+
* Shallow, author-time validation: unknown component, unknown/missing prop,
|
|
5
|
+
* wrong coarse type, illegal enum value. Collects `requires` (plugin provenance)
|
|
6
|
+
* and binding sites the SERVER must resolve against object schema (we cannot
|
|
7
|
+
* resolve objects/fields here — that check is framework-side by design).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
Diagnostic,
|
|
12
|
+
Manifest,
|
|
13
|
+
ManifestInput,
|
|
14
|
+
SchemaElement,
|
|
15
|
+
SchemaNode,
|
|
16
|
+
ValidationResult,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
/** Base props every node may carry (mirrors BaseSchema) — never "unknown prop". */
|
|
20
|
+
const BASE_PROPS = new Set([
|
|
21
|
+
'type',
|
|
22
|
+
'id',
|
|
23
|
+
'className',
|
|
24
|
+
'style',
|
|
25
|
+
'visible',
|
|
26
|
+
'visibleOn',
|
|
27
|
+
'disabled',
|
|
28
|
+
'disabledOn',
|
|
29
|
+
'children',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const isExpr = (v: unknown): boolean =>
|
|
33
|
+
typeof v === 'object' && v !== null && '$expr' in (v as Record<string, unknown>);
|
|
34
|
+
|
|
35
|
+
export function validateTree(tree: SchemaElement | null, manifest: Manifest): ValidationResult {
|
|
36
|
+
const diagnostics: Diagnostic[] = [];
|
|
37
|
+
const requires = new Set<string>();
|
|
38
|
+
const bindings: ValidationResult['bindings'] = [];
|
|
39
|
+
|
|
40
|
+
const visit = (node: SchemaNode): void => {
|
|
41
|
+
if (typeof node === 'string') return;
|
|
42
|
+
const comp = manifest.components[node.type];
|
|
43
|
+
if (!comp) {
|
|
44
|
+
diagnostics.push({
|
|
45
|
+
severity: 'error',
|
|
46
|
+
code: 'unknown-component',
|
|
47
|
+
message: `<${node.type}> is not a known component`,
|
|
48
|
+
tag: node.type,
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
if (comp.namespace) requires.add(comp.namespace);
|
|
52
|
+
const byName = new Map(comp.inputs.map((i) => [i.name, i]));
|
|
53
|
+
|
|
54
|
+
// required present?
|
|
55
|
+
for (const input of comp.inputs) {
|
|
56
|
+
if (input.required && !(input.name in node)) {
|
|
57
|
+
diagnostics.push({
|
|
58
|
+
severity: 'error',
|
|
59
|
+
code: 'missing-required-prop',
|
|
60
|
+
message: `<${node.type}> is missing required prop "${input.name}"`,
|
|
61
|
+
tag: node.type,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// each provided prop
|
|
67
|
+
for (const [key, value] of Object.entries(node)) {
|
|
68
|
+
if (BASE_PROPS.has(key)) continue;
|
|
69
|
+
const input = byName.get(key);
|
|
70
|
+
if (!input) {
|
|
71
|
+
diagnostics.push({
|
|
72
|
+
severity: 'warning',
|
|
73
|
+
code: 'unknown-prop',
|
|
74
|
+
message: `<${node.type}> has no prop "${key}"`,
|
|
75
|
+
tag: node.type,
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (input.binding) {
|
|
80
|
+
bindings.push({ tag: node.type, input: key, kind: input.binding, value });
|
|
81
|
+
}
|
|
82
|
+
if (!isExpr(value)) {
|
|
83
|
+
const typeDiag = checkType(node.type, input, value);
|
|
84
|
+
if (typeDiag) diagnostics.push(typeDiag);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// containment
|
|
89
|
+
if (node.children?.length && !comp.isContainer) {
|
|
90
|
+
diagnostics.push({
|
|
91
|
+
severity: 'warning',
|
|
92
|
+
code: 'not-a-container',
|
|
93
|
+
message: `<${node.type}> does not accept children`,
|
|
94
|
+
tag: node.type,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (node.children) node.children.forEach(visit);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (tree) visit(tree);
|
|
103
|
+
return { diagnostics, requires: [...requires], bindings };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function checkType(tag: string, input: ManifestInput, value: unknown): Diagnostic | null {
|
|
107
|
+
const mismatch = (expected: string): Diagnostic => ({
|
|
108
|
+
severity: 'warning',
|
|
109
|
+
code: 'type-mismatch',
|
|
110
|
+
message: `<${tag}> prop "${input.name}" expected ${expected}`,
|
|
111
|
+
tag,
|
|
112
|
+
});
|
|
113
|
+
switch (input.type) {
|
|
114
|
+
case 'number':
|
|
115
|
+
return typeof value === 'number' ? null : mismatch('a number');
|
|
116
|
+
case 'boolean':
|
|
117
|
+
return typeof value === 'boolean' ? null : mismatch('a boolean');
|
|
118
|
+
case 'string':
|
|
119
|
+
case 'color':
|
|
120
|
+
case 'date':
|
|
121
|
+
case 'code':
|
|
122
|
+
case 'file':
|
|
123
|
+
return typeof value === 'string' ? null : mismatch('a string');
|
|
124
|
+
case 'array':
|
|
125
|
+
return Array.isArray(value) ? null : mismatch('an array');
|
|
126
|
+
case 'object':
|
|
127
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
128
|
+
? null
|
|
129
|
+
: mismatch('an object');
|
|
130
|
+
case 'enum': {
|
|
131
|
+
const allowed = (input.enum ?? []).map((e) => (typeof e === 'object' ? e.value : e));
|
|
132
|
+
return allowed.includes(value as never)
|
|
133
|
+
? null
|
|
134
|
+
: {
|
|
135
|
+
severity: 'error',
|
|
136
|
+
code: 'invalid-enum',
|
|
137
|
+
message: `<${tag}> prop "${input.name}"=${JSON.stringify(value)} is not one of ${JSON.stringify(allowed)}`,
|
|
138
|
+
tag,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
default:
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|