@object-ui/sdui-parser 17.6.0 → 18.0.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/body-dialect.d.ts +52 -0
- package/dist/body-dialect.js +85 -0
- package/dist/codegen.d.ts +5 -0
- package/dist/codegen.js +91 -25
- package/dist/dashboard-widget-options.d.ts +50 -20
- package/dist/dashboard-widget-options.js +3 -3
- package/dist/index.d.ts +73 -4
- package/dist/index.js +34 -1
- package/dist/parse.d.ts +13 -4
- package/dist/parse.js +325 -7
- package/dist/provenance.d.ts +7 -4
- package/dist/provenance.js +7 -4
- package/dist/types.d.ts +130 -9
- package/dist/types.js +2 -2
- package/dist/validate.d.ts +118 -4
- package/dist/validate.js +301 -20
- package/package.json +7 -6
package/dist/parse.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A small recursive-descent parser for a CONSTRAINED JSX subset. It is
|
|
5
5
|
* deliberately not a full JS/JSX parser: a bounded grammar is the point
|
|
6
6
|
* (Markdoc model) — it shrinks the attack surface and the expressible-but-wrong
|
|
7
|
-
* space. Output is
|
|
7
|
+
* space. Output is an SDUI node tree (`ParsedSchemaNode`). Nothing is executed.
|
|
8
8
|
*
|
|
9
9
|
* Grammar (informal):
|
|
10
10
|
* document := element (exactly one root)
|
|
@@ -17,8 +17,17 @@ import type { ParseOptions, ParseResult } from './types.js';
|
|
|
17
17
|
export declare function parseJsx(source: string, options?: ParseOptions): ParseResult;
|
|
18
18
|
/**
|
|
19
19
|
* Interpret a braced attribute value `{...}`.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
*
|
|
21
|
+
* Strict-JSON values are materialized by `JSON.parse`, exactly as they always
|
|
22
|
+
* were. Beyond that, the JS **literal subset** below is materialized too
|
|
23
|
+
* (objectui#6614 Q1-A, maintainer ruling 2026-08-28). Anything left over — a
|
|
24
|
+
* genuine expression — is kept as the deferred marker `{ $expr }`: typed and
|
|
25
|
+
* validated later, drawing `inert-expression`, and NEVER evaluated here.
|
|
26
|
+
*
|
|
27
|
+
* ORDER IS LOAD-BEARING. `JSON.parse` runs FIRST and is untouched, so every
|
|
28
|
+
* input JSON accepts takes byte-identically the path it took before the literal
|
|
29
|
+
* subset existed. The reader below only ever sees strings `JSON.parse` has
|
|
30
|
+
* already thrown on, which makes strict-JSON invariance a property of the
|
|
31
|
+
* structure rather than of a test.
|
|
23
32
|
*/
|
|
24
33
|
export declare function interpretBrace(raw: string): unknown;
|
package/dist/parse.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A small recursive-descent parser for a CONSTRAINED JSX subset. It is
|
|
5
5
|
* deliberately not a full JS/JSX parser: a bounded grammar is the point
|
|
6
6
|
* (Markdoc model) — it shrinks the attack surface and the expressible-but-wrong
|
|
7
|
-
* space. Output is
|
|
7
|
+
* space. Output is an SDUI node tree (`ParsedSchemaNode`). Nothing is executed.
|
|
8
8
|
*
|
|
9
9
|
* Grammar (informal):
|
|
10
10
|
* document := element (exactly one root)
|
|
@@ -17,6 +17,47 @@ import { markHtmlTierNode } from './provenance.js';
|
|
|
17
17
|
/** Event handlers and raw-HTML injection are never allowed (parse ≠ execute). */
|
|
18
18
|
const EVENT_ATTR = /^on[A-Z]/;
|
|
19
19
|
const FORBIDDEN_ATTRS = new Set(['dangerouslySetInnerHTML', 'ref', 'key']);
|
|
20
|
+
/**
|
|
21
|
+
* The envelope's own discriminator, which on THIS tier the tag name sets.
|
|
22
|
+
*
|
|
23
|
+
* An authored `type=` attribute is a NAME COLLISION with it, and the parser
|
|
24
|
+
* refuses it at parse time (maintainer ruling 2026-09-01, recorded as an
|
|
25
|
+
* append-only amendment on ADR-0080 — 「响亮拒绝」, quoted verbatim there;
|
|
26
|
+
* objectui#7235 ports it to this copy, objectstack#13957 landed the other).
|
|
27
|
+
* One diagnostic naming BOTH the tag and the attribute replaces two bad
|
|
28
|
+
* outcomes:
|
|
29
|
+
*
|
|
30
|
+
* - the value named another REGISTERED type (`<flex type="grid">`) — the tree
|
|
31
|
+
* carried `type:'grid'`, `validateTree` found `grid` in the manifest, every
|
|
32
|
+
* check passed, and the page rendered a grid where the author wrote a flex.
|
|
33
|
+
* ZERO diagnostics. On the one tier whose whole premise is that unreviewed
|
|
34
|
+
* and AI-authored source is safe to accept.
|
|
35
|
+
* - the value named NOTHING registered (`<object-chart type="bar">`, the shape
|
|
36
|
+
* a react-tier author carries across) — loud, but `unknown-component`
|
|
37
|
+
* naming `"bar"` reads as a missing plugin, never as a bad prop.
|
|
38
|
+
*
|
|
39
|
+
* ⛔ NOT rescued as `specType` the way the react tier rescues it (objectui#2880):
|
|
40
|
+
* that is consumer-side tolerance, and it would spread an alias concept to a
|
|
41
|
+
* second tier. ⛔ NOT a warning grace period either — the same ruling declined
|
|
42
|
+
* a staged rollout. ⛔ And NOT fixable at the warning layer: `type` is in
|
|
43
|
+
* `validate.ts`'s `BASE_PROPS` deliberately (it is correct for every other
|
|
44
|
+
* member), so removing it there would make every legitimate node warn. The
|
|
45
|
+
* refusal belongs here, at parse.
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ The code is the EXISTING `forbidden-attr`, not a new one, and that is
|
|
48
|
+
* load-bearing rather than lazy: objectstack's `scripts/check-sdui-lockstep.mjs`
|
|
49
|
+
* holds ITS copy's diagnostic-code set equal to THIS one's at the pinned
|
|
50
|
+
* revision, so a code minted on one side only IS the dialect split that gate
|
|
51
|
+
* exists to catch (objectstack#12719). `forbidden-attr` already carries this
|
|
52
|
+
* shape — an attribute this tier refuses, named beside its element — and both
|
|
53
|
+
* copies stamp it.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ This copy is the RENDERER's (and the console's live edit preview); the
|
|
56
|
+
* save gate runs objectstack's. Until this landed the two differed in accepted
|
|
57
|
+
* grammar: a page objectstack refused still compiled silently while its author
|
|
58
|
+
* was typing.
|
|
59
|
+
*/
|
|
60
|
+
const DISCRIMINATOR_ATTR = 'type';
|
|
20
61
|
export function parseJsx(source, options = {}) {
|
|
21
62
|
return new Parser(source, options).parseDocument();
|
|
22
63
|
}
|
|
@@ -85,7 +126,16 @@ class Parser {
|
|
|
85
126
|
const attr = this.parseAttr(start, tag);
|
|
86
127
|
if (!attr)
|
|
87
128
|
break;
|
|
88
|
-
|
|
129
|
+
// `drop` is set only for the refused discriminator attribute, and only so
|
|
130
|
+
// that ONE diagnostic is what the author gets. The `__forbidden_<name>`
|
|
131
|
+
// sentinel the other refusals park in `props` reaches `validateTree`,
|
|
132
|
+
// which knows no such prop and adds `unknown-prop` naming a key nobody
|
|
133
|
+
// wrote — loud, and pointing at the wrong thing, which is the species of
|
|
134
|
+
// diagnostic this whole change exists to remove. The existing sentinel
|
|
135
|
+
// behaviour is left exactly as it was for the attributes that already had
|
|
136
|
+
// it (`ref`, `key`, `dangerouslySetInnerHTML`, `on*`).
|
|
137
|
+
if (!attr.drop)
|
|
138
|
+
props[attr.name] = attr.value;
|
|
89
139
|
}
|
|
90
140
|
this.skipWs();
|
|
91
141
|
let children;
|
|
@@ -107,7 +157,22 @@ class Parser {
|
|
|
107
157
|
// JSON, to the DOM, and to anything an authored document could forge — see
|
|
108
158
|
// provenance.ts. Values reached through a braced attribute are NOT marked:
|
|
109
159
|
// that JSON was written by hand, and the JSON surface's advice does apply.
|
|
110
|
-
|
|
160
|
+
// DEFENSE IN DEPTH (ruled together with the refusal above). `props` used to
|
|
161
|
+
// be spread AFTER `type: tag`, so an authored `type` attribute overwrote the
|
|
162
|
+
// discriminator the tag established and nothing downstream restored it —
|
|
163
|
+
// `compile()` returns this tree as-is and `validateTree` then looks up
|
|
164
|
+
// `manifest.components[node.type]`, i.e. the value the author wrote, not the
|
|
165
|
+
// tag they wrote. The refusal makes that overwrite unreachable; the order
|
|
166
|
+
// here makes it impossible. ⚠️ Reversing the order ALONE would have been a
|
|
167
|
+
// regression of its own — the authored value would then be dropped in
|
|
168
|
+
// silence, trading one silence for another. It is correct only BECAUSE the
|
|
169
|
+
// attribute is refused loudly one function up.
|
|
170
|
+
//
|
|
171
|
+
// ⚠️ The `markHtmlTierNode` wrapper is objectui-only and survives the
|
|
172
|
+
// reversal: it stamps the object the spread produced, so the marker is
|
|
173
|
+
// applied after every key is in place regardless of their order. Pinned in
|
|
174
|
+
// provenance.test.ts, because this is the one edit that could drop it.
|
|
175
|
+
const node = markHtmlTierNode({ ...props, type: tag });
|
|
111
176
|
if (children && children.length)
|
|
112
177
|
node.children = children;
|
|
113
178
|
return node;
|
|
@@ -126,6 +191,14 @@ class Parser {
|
|
|
126
191
|
this.skipWs();
|
|
127
192
|
value = this.parseAttrValue(tag);
|
|
128
193
|
}
|
|
194
|
+
if (name === DISCRIMINATOR_ATTR) {
|
|
195
|
+
// ONE diagnostic naming both the tag and the attribute — see
|
|
196
|
+
// DISCRIMINATOR_ATTR above for why it replaces both prior outcomes.
|
|
197
|
+
this.error('forbidden-attr', `Attribute "${DISCRIMINATOR_ATTR}" is not allowed on <${tag}> — on this tier the tag name IS the `
|
|
198
|
+
+ `component, so <${tag}> already means type "${tag}". Delete the attribute, or write the tag of the `
|
|
199
|
+
+ 'component you meant.', elStart, tag);
|
|
200
|
+
return { name, value: undefined, drop: true };
|
|
201
|
+
}
|
|
129
202
|
if (EVENT_ATTR.test(name) || FORBIDDEN_ATTRS.has(name)) {
|
|
130
203
|
this.error('forbidden-attr', `Attribute "${name}" is not allowed on <${tag}>`, elStart, tag);
|
|
131
204
|
return { name: `__forbidden_${name}`, value: undefined };
|
|
@@ -301,9 +374,18 @@ class Parser {
|
|
|
301
374
|
}
|
|
302
375
|
/**
|
|
303
376
|
* Interpret a braced attribute value `{...}`.
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
377
|
+
*
|
|
378
|
+
* Strict-JSON values are materialized by `JSON.parse`, exactly as they always
|
|
379
|
+
* were. Beyond that, the JS **literal subset** below is materialized too
|
|
380
|
+
* (objectui#6614 Q1-A, maintainer ruling 2026-08-28). Anything left over — a
|
|
381
|
+
* genuine expression — is kept as the deferred marker `{ $expr }`: typed and
|
|
382
|
+
* validated later, drawing `inert-expression`, and NEVER evaluated here.
|
|
383
|
+
*
|
|
384
|
+
* ORDER IS LOAD-BEARING. `JSON.parse` runs FIRST and is untouched, so every
|
|
385
|
+
* input JSON accepts takes byte-identically the path it took before the literal
|
|
386
|
+
* subset existed. The reader below only ever sees strings `JSON.parse` has
|
|
387
|
+
* already thrown on, which makes strict-JSON invariance a property of the
|
|
388
|
+
* structure rather than of a test.
|
|
307
389
|
*/
|
|
308
390
|
export function interpretBrace(raw) {
|
|
309
391
|
const trimmed = raw.trim();
|
|
@@ -311,6 +393,242 @@ export function interpretBrace(raw) {
|
|
|
311
393
|
return JSON.parse(trimmed);
|
|
312
394
|
}
|
|
313
395
|
catch {
|
|
314
|
-
|
|
396
|
+
const literal = readLiteral(trimmed);
|
|
397
|
+
return literal === NOT_LITERAL ? { $expr: trimmed } : literal;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/* ---------------------- the JS literal subset (#6614) ---------------------- */
|
|
401
|
+
/**
|
|
402
|
+
* EXACTLY TWO widenings over JSON, and nothing else:
|
|
403
|
+
*
|
|
404
|
+
* 1. **single-quoted strings** — `{'name'}`, `{['name','amount']}`, and in
|
|
405
|
+
* key position `{{'pageSize': 25}}`;
|
|
406
|
+
* 2. **unquoted identifier object keys** — `{{pageSize: 25}}`.
|
|
407
|
+
*
|
|
408
|
+
* Everything else JSON refuses is still refused and still becomes `{ $expr }`:
|
|
409
|
+
* trailing commas, comments, array holes, spreads, `undefined` / `NaN` /
|
|
410
|
+
* `Infinity`, `+1` / `.5` / `1.` / `0x1f`, template literals, and every genuine
|
|
411
|
+
* expression — identifiers, member access, calls, operators, ternaries.
|
|
412
|
+
*
|
|
413
|
+
* That list is deliberately short. This is a VALUE grammar, not an evaluator:
|
|
414
|
+
* it contains no identifier lookup and no operator, so there is nothing here to
|
|
415
|
+
* execute (ADR-0080 — this tier parses, never executes). The widening moves the
|
|
416
|
+
* spellings an author writes by habit onto the materialized side; it does not
|
|
417
|
+
* move the boundary between data and code.
|
|
418
|
+
*/
|
|
419
|
+
const NOT_LITERAL = Symbol('not-a-literal');
|
|
420
|
+
/** JSON's whitespace set, not JS's — narrower, and one less thing to diverge. */
|
|
421
|
+
const LITERAL_WS = /[ \t\n\r]/;
|
|
422
|
+
const IDENT_START = /[A-Za-z_$]/;
|
|
423
|
+
const IDENT_CHAR = /[A-Za-z0-9_$]/;
|
|
424
|
+
/** JSON's number grammar verbatim: no leading `+`, no `.5`, no `1.`, no hex. */
|
|
425
|
+
const NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/;
|
|
426
|
+
/** JSON's escape set. `\'` is added for single-quoted strings only. */
|
|
427
|
+
const SIMPLE_ESCAPE = {
|
|
428
|
+
'"': '"',
|
|
429
|
+
'\\': '\\',
|
|
430
|
+
'/': '/',
|
|
431
|
+
b: '\b',
|
|
432
|
+
f: '\f',
|
|
433
|
+
n: '\n',
|
|
434
|
+
r: '\r',
|
|
435
|
+
t: '\t',
|
|
436
|
+
};
|
|
437
|
+
function readLiteral(src) {
|
|
438
|
+
const reader = new LiteralReader(src);
|
|
439
|
+
const value = reader.value();
|
|
440
|
+
if (value === NOT_LITERAL)
|
|
441
|
+
return NOT_LITERAL;
|
|
442
|
+
reader.ws();
|
|
443
|
+
// Trailing anything means the input was an expression that merely STARTS with
|
|
444
|
+
// a literal (`['a'] + x`, `1 + 2`). Refuse the whole input.
|
|
445
|
+
return reader.done() ? value : NOT_LITERAL;
|
|
446
|
+
}
|
|
447
|
+
class LiteralReader {
|
|
448
|
+
constructor(src) {
|
|
449
|
+
Object.defineProperty(this, "src", {
|
|
450
|
+
enumerable: true,
|
|
451
|
+
configurable: true,
|
|
452
|
+
writable: true,
|
|
453
|
+
value: src
|
|
454
|
+
});
|
|
455
|
+
Object.defineProperty(this, "pos", {
|
|
456
|
+
enumerable: true,
|
|
457
|
+
configurable: true,
|
|
458
|
+
writable: true,
|
|
459
|
+
value: 0
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
done() {
|
|
463
|
+
return this.pos >= this.src.length;
|
|
464
|
+
}
|
|
465
|
+
ws() {
|
|
466
|
+
while (this.pos < this.src.length && LITERAL_WS.test(this.src[this.pos]))
|
|
467
|
+
this.pos++;
|
|
468
|
+
}
|
|
469
|
+
value() {
|
|
470
|
+
this.ws();
|
|
471
|
+
const c = this.src[this.pos];
|
|
472
|
+
if (c === undefined)
|
|
473
|
+
return NOT_LITERAL;
|
|
474
|
+
if (c === '"' || c === "'")
|
|
475
|
+
return this.string(c);
|
|
476
|
+
if (c === '[')
|
|
477
|
+
return this.array();
|
|
478
|
+
if (c === '{')
|
|
479
|
+
return this.object();
|
|
480
|
+
if (this.keyword('true'))
|
|
481
|
+
return true;
|
|
482
|
+
if (this.keyword('false'))
|
|
483
|
+
return false;
|
|
484
|
+
if (this.keyword('null'))
|
|
485
|
+
return null;
|
|
486
|
+
return this.number();
|
|
487
|
+
}
|
|
488
|
+
/** A keyword only when it is not the prefix of a longer identifier. */
|
|
489
|
+
keyword(word) {
|
|
490
|
+
if (!this.src.startsWith(word, this.pos))
|
|
491
|
+
return false;
|
|
492
|
+
const after = this.src[this.pos + word.length];
|
|
493
|
+
if (after !== undefined && IDENT_CHAR.test(after))
|
|
494
|
+
return false;
|
|
495
|
+
this.pos += word.length;
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
number() {
|
|
499
|
+
const m = NUMBER.exec(this.src.slice(this.pos));
|
|
500
|
+
if (!m)
|
|
501
|
+
return NOT_LITERAL;
|
|
502
|
+
this.pos += m[0].length;
|
|
503
|
+
return Number(m[0]);
|
|
504
|
+
}
|
|
505
|
+
string(quote) {
|
|
506
|
+
this.pos++; // opening quote
|
|
507
|
+
let out = '';
|
|
508
|
+
for (;;) {
|
|
509
|
+
const c = this.src[this.pos];
|
|
510
|
+
if (c === undefined)
|
|
511
|
+
return NOT_LITERAL; // unterminated
|
|
512
|
+
if (c === quote) {
|
|
513
|
+
this.pos++;
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
516
|
+
if (c === '\\') {
|
|
517
|
+
const esc = this.src[this.pos + 1];
|
|
518
|
+
if (esc === undefined)
|
|
519
|
+
return NOT_LITERAL;
|
|
520
|
+
if (esc === 'u') {
|
|
521
|
+
const hex = this.src.slice(this.pos + 2, this.pos + 6);
|
|
522
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
523
|
+
return NOT_LITERAL;
|
|
524
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
525
|
+
this.pos += 6;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
// `\'` is legal inside a single-quoted string only — JSON's set otherwise.
|
|
529
|
+
if (esc === "'" && quote === "'") {
|
|
530
|
+
out += "'";
|
|
531
|
+
this.pos += 2;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const simple = SIMPLE_ESCAPE[esc];
|
|
535
|
+
if (simple === undefined)
|
|
536
|
+
return NOT_LITERAL; // `\x41`, `\0`, line continuation
|
|
537
|
+
out += simple;
|
|
538
|
+
this.pos += 2;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
// JSON forbids raw control characters inside a string; so does this.
|
|
542
|
+
if (c < ' ')
|
|
543
|
+
return NOT_LITERAL;
|
|
544
|
+
out += c;
|
|
545
|
+
this.pos++;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
array() {
|
|
549
|
+
this.pos++; // '['
|
|
550
|
+
const out = [];
|
|
551
|
+
this.ws();
|
|
552
|
+
if (this.src[this.pos] === ']') {
|
|
553
|
+
this.pos++;
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
for (;;) {
|
|
557
|
+
const item = this.value();
|
|
558
|
+
if (item === NOT_LITERAL)
|
|
559
|
+
return NOT_LITERAL;
|
|
560
|
+
out.push(item);
|
|
561
|
+
this.ws();
|
|
562
|
+
const c = this.src[this.pos];
|
|
563
|
+
// A trailing comma leaves `value()` facing `]`, which it refuses — so
|
|
564
|
+
// `['a',]` is NOT in the subset. Only two widenings were ruled.
|
|
565
|
+
if (c === ',') {
|
|
566
|
+
this.pos++;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (c === ']') {
|
|
570
|
+
this.pos++;
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
return NOT_LITERAL;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
object() {
|
|
577
|
+
this.pos++; // '{'
|
|
578
|
+
const out = {};
|
|
579
|
+
this.ws();
|
|
580
|
+
if (this.src[this.pos] === '}') {
|
|
581
|
+
this.pos++;
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
584
|
+
for (;;) {
|
|
585
|
+
this.ws();
|
|
586
|
+
const key = this.key();
|
|
587
|
+
if (key === NOT_LITERAL)
|
|
588
|
+
return NOT_LITERAL;
|
|
589
|
+
this.ws();
|
|
590
|
+
if (this.src[this.pos] !== ':')
|
|
591
|
+
return NOT_LITERAL;
|
|
592
|
+
this.pos++;
|
|
593
|
+
const item = this.value();
|
|
594
|
+
if (item === NOT_LITERAL)
|
|
595
|
+
return NOT_LITERAL;
|
|
596
|
+
// ⚠️ Plain `out[key] = item` would hand an authored `__proto__` key the
|
|
597
|
+
// prototype SETTER. `JSON.parse` creates an ordinary own data property,
|
|
598
|
+
// and this path must too: the whole point of this tier is that untrusted
|
|
599
|
+
// source is safe to parse, so a widening must not open a
|
|
600
|
+
// prototype-pollution lever the strict-JSON path never had.
|
|
601
|
+
Object.defineProperty(out, key, {
|
|
602
|
+
value: item,
|
|
603
|
+
writable: true,
|
|
604
|
+
enumerable: true,
|
|
605
|
+
configurable: true,
|
|
606
|
+
});
|
|
607
|
+
this.ws();
|
|
608
|
+
const c = this.src[this.pos];
|
|
609
|
+
if (c === ',') {
|
|
610
|
+
this.pos++;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (c === '}') {
|
|
614
|
+
this.pos++;
|
|
615
|
+
return out;
|
|
616
|
+
}
|
|
617
|
+
return NOT_LITERAL;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/** A quoted string, or a bare identifier — the second ruled widening. */
|
|
621
|
+
key() {
|
|
622
|
+
const c = this.src[this.pos];
|
|
623
|
+
if (c === '"' || c === "'")
|
|
624
|
+
return this.string(c);
|
|
625
|
+
if (c !== undefined && IDENT_START.test(c)) {
|
|
626
|
+
const start = this.pos;
|
|
627
|
+
this.pos++;
|
|
628
|
+
while (this.pos < this.src.length && IDENT_CHAR.test(this.src[this.pos]))
|
|
629
|
+
this.pos++;
|
|
630
|
+
return this.src.slice(start, this.pos);
|
|
631
|
+
}
|
|
632
|
+
return NOT_LITERAL;
|
|
315
633
|
}
|
|
316
634
|
}
|
package/dist/provenance.d.ts
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* source, compiled tag-name-straight-through) or AUTHORED AS JSON metadata.
|
|
7
7
|
*
|
|
8
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* the JSON surface is steering authors away from can still be a supported tag
|
|
10
|
+
* in the html tier — the plain inline element (`span`) is exactly that. (The
|
|
11
|
+
* plain box element was too, until objectui#10757 deprecated `div` on the html
|
|
12
|
+
* surface as well; the console's html compile now refuses it outright, so no
|
|
13
|
+
* notice is involved there.) Advice aimed at the JSON surface, delivered to an
|
|
14
|
+
* html-tier author, is advice they cannot act on: in that tier there is no
|
|
15
|
+
* other spelling. The
|
|
13
16
|
* deprecation notice fired at them anyway (objectui#4000), which is also why the
|
|
14
17
|
* type could never be retired — the engine's own compiler keeps emitting it.
|
|
15
18
|
*
|
package/dist/provenance.js
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* source, compiled tag-name-straight-through) or AUTHORED AS JSON metadata.
|
|
7
7
|
*
|
|
8
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* the JSON surface is steering authors away from can still be a supported tag
|
|
10
|
+
* in the html tier — the plain inline element (`span`) is exactly that. (The
|
|
11
|
+
* plain box element was too, until objectui#10757 deprecated `div` on the html
|
|
12
|
+
* surface as well; the console's html compile now refuses it outright, so no
|
|
13
|
+
* notice is involved there.) Advice aimed at the JSON surface, delivered to an
|
|
14
|
+
* html-tier author, is advice they cannot act on: in that tier there is no
|
|
15
|
+
* other spelling. The
|
|
13
16
|
* deprecation notice fired at them anyway (objectui#4000), which is also why the
|
|
14
17
|
* type could never be retired — the engine's own compiler keeps emitting it.
|
|
15
18
|
*
|
package/dist/types.d.ts
CHANGED
|
@@ -2,14 +2,24 @@
|
|
|
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 *text* into
|
|
6
|
-
*
|
|
5
|
+
* constrained JSX *text* into an SDUI node tree (`ParsedSchemaNode`, below).
|
|
6
|
+
* It PARSES — it never executes. No `import`, no `eval`, no JS.
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* A node in the compiled SDUI tree, as parsed: an element or a text child.
|
|
10
|
+
*
|
|
11
|
+
* Not `SchemaNode`: `@object-ui/types` publishes that name for the closed
|
|
12
|
+
* AUTHORING face, a union discriminated on a literal `type` with no index
|
|
13
|
+
* signature and with primitive children admitted. This is the parser's open,
|
|
14
|
+
* as-parsed data (any `type` string, the keys as authored), neither is
|
|
15
|
+
* assignable to the other, and this package takes no runtime dependency on
|
|
16
|
+
* `@object-ui/types` to re-export from. One exported name stood for both until
|
|
17
|
+
* this declaration was renamed (objectui#6349, batch 11).
|
|
18
|
+
*/
|
|
19
|
+
export type ParsedSchemaNode = SchemaElement | string;
|
|
10
20
|
export interface SchemaElement {
|
|
11
21
|
type: string;
|
|
12
|
-
children?:
|
|
22
|
+
children?: ParsedSchemaNode[];
|
|
13
23
|
[prop: string]: unknown;
|
|
14
24
|
}
|
|
15
25
|
export type Severity = 'error' | 'warning';
|
|
@@ -53,22 +63,122 @@ export interface ManifestInput {
|
|
|
53
63
|
* declared and every already-published entry serializes byte-identically.
|
|
54
64
|
*/
|
|
55
65
|
type: ManifestInputType | ManifestInputType[];
|
|
66
|
+
/**
|
|
67
|
+
* The coarse kind of the input's MEMBERS — array elements, or the values of
|
|
68
|
+
* an object used as a map — as ONE kind or an ARRAY of kinds for a member
|
|
69
|
+
* contract that is a union (objectui#8067).
|
|
70
|
+
*
|
|
71
|
+
* Absent means "not declared", which is what every input published before
|
|
72
|
+
* this key existed says: {@link validateTree} checks no member and the
|
|
73
|
+
* codegen emits the unnarrowed element type, exactly as before. So a
|
|
74
|
+
* manifest gains this key only where a member kind was really declared, and
|
|
75
|
+
* every already-published entry serializes byte-identically.
|
|
76
|
+
*
|
|
77
|
+
* Read the arms through `inputTypeArms(input.of)` — the same accessor
|
|
78
|
+
* `type`'s arms go through, since the two fields carry the same shape and a
|
|
79
|
+
* reader that forgets the array form is silently inert on it.
|
|
80
|
+
*/
|
|
81
|
+
of?: ManifestInputType | ManifestInputType[];
|
|
56
82
|
required?: boolean;
|
|
57
83
|
/** allowed values for `enum` inputs */
|
|
58
84
|
enum?: Array<string | {
|
|
59
85
|
value: unknown;
|
|
60
86
|
label?: string;
|
|
61
87
|
}>;
|
|
62
|
-
/**
|
|
63
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Marks a data-binding input the server must resolve (ADR-0080 §6.3):
|
|
90
|
+
* `binding: 'object'` says the input NAMES an object, so the server-side
|
|
91
|
+
* binding check knows what to resolve it against. Unrelated to
|
|
92
|
+
* `type: 'object'`, the coarse control kind — a record-shaped value and an
|
|
93
|
+
* object-naming input are two different facts.
|
|
94
|
+
*
|
|
95
|
+
* ## The vocabulary is exactly `'object'` — `'field'` is retired on this face too
|
|
96
|
+
*
|
|
97
|
+
* `'field'` stood beside it from the first draft of ADR-0080 and was never
|
|
98
|
+
* written. It was retired on the serializer's INPUT boundary
|
|
99
|
+
* (`RegistryConfigLike` in `index.ts`) under ADR-0049 enforce-or-remove by
|
|
100
|
+
* the maintainer ruling of 2026-09-07 (decision batch #69), and on THIS
|
|
101
|
+
* face by objectui#8315 — the residue that ruling did not name, which left
|
|
102
|
+
* one published package narrow on one face and wide on the other.
|
|
103
|
+
*
|
|
104
|
+
* ⚠️ The argument for leaving this face wide was ANSWERED, not overlooked.
|
|
105
|
+
* It runs: producer → reader is a subset relation, so a reader accepting a
|
|
106
|
+
* value no producer emits is permissive rather than wrong. Three measured
|
|
107
|
+
* facts decided against it (objectui#8315):
|
|
108
|
+
*
|
|
109
|
+
* 1. **This is not a pure reader face.** `manifestFromConfigs` RETURNS a
|
|
110
|
+
* `Manifest`, so `ManifestInput` is also this package's OUTPUT type,
|
|
111
|
+
* and this key is fed straight from the already-narrowed boundary. A
|
|
112
|
+
* union wider than the producer's is imprecision on the way out, not
|
|
113
|
+
* permissiveness on the way in.
|
|
114
|
+
* 2. **Its sibling is a pure producer face.**
|
|
115
|
+
* `ManifestValidationResult.bindings[].kind` is written by
|
|
116
|
+
* `validateTree` by copying this key, so the subset relation runs the
|
|
117
|
+
* other way there — see that declaration. The two are COUPLED by that
|
|
118
|
+
* assignment: narrowing one alone needs a cast at the only conversion
|
|
119
|
+
* site, which is the lenient fallback AGENTS.md #0.1 bans. So "both
|
|
120
|
+
* narrow" and "both wide" were the only self-consistent states, and
|
|
121
|
+
* only one of them can be justified on the producer face.
|
|
122
|
+
* 3. **The permissiveness protected nothing.** `binding: 'field'` has
|
|
123
|
+
* zero writers here and zero in the objectstack copy of this package
|
|
124
|
+
* (measured 2026-09-09 on both heads, each with a firing
|
|
125
|
+
* `binding: 'object'` control), and the only manifest producer in
|
|
126
|
+
* either tree is `manifestFromConfigs`, whose input face is narrow.
|
|
127
|
+
*
|
|
128
|
+
* The reopen route is the ruling's own: a MEASURED need for field bindings
|
|
129
|
+
* is filed as a widening with the vocabulary decided then — not
|
|
130
|
+
* pre-declared here for a producer that does not exist.
|
|
131
|
+
*/
|
|
132
|
+
binding?: 'object';
|
|
64
133
|
description?: string;
|
|
65
134
|
}
|
|
66
135
|
export interface ManifestComponent {
|
|
67
136
|
type: string;
|
|
68
137
|
/** plugin namespace — provenance that drives `requires` */
|
|
69
138
|
namespace?: string;
|
|
139
|
+
/**
|
|
140
|
+
* The declared authoring surface. An input named `children` (type `slot`)
|
|
141
|
+
* is ALSO the containment declaration: `validateTree` draws
|
|
142
|
+
* `not-a-container` on a child list under any component whose `inputs`
|
|
143
|
+
* carry no such entry (objectui#9910; `acceptsChildren` in `validate.ts`).
|
|
144
|
+
*/
|
|
70
145
|
inputs: ManifestInput[];
|
|
146
|
+
/**
|
|
147
|
+
* The NODE SLOTS this component's renderer reads besides `children`
|
|
148
|
+
* (objectui#11170): positions in the key-path grammar `@object-ui/types`'
|
|
149
|
+
* `NODE_SLOT_DECLARATIONS` states — `trigger`, `items[].content`,
|
|
150
|
+
* `regions[].components`. `validateTree` descends each one the way it
|
|
151
|
+
* descends `children`, so a slot-held node is judged too. Projected from
|
|
152
|
+
* that one declaration by `manifestFromConfigs`'s `slotsFor` option; the
|
|
153
|
+
* authoring tier leaves the declaration's RETIRED positions out, as the
|
|
154
|
+
* spec's own authoring walks do. ⛔ This tier keeps no slot list of its own:
|
|
155
|
+
* a manifest built without the option carries none, and the walk then
|
|
156
|
+
* follows `children` alone.
|
|
157
|
+
*/
|
|
158
|
+
slots?: readonly string[];
|
|
159
|
+
/**
|
|
160
|
+
* LAYOUT containment (objectui#6804, objectui#9910 Q2-A): the flag the
|
|
161
|
+
* react-page JSX scope builder skips and the public layout ledger lists.
|
|
162
|
+
* ⛔ Not "accepts children" — this tier's containment check does not read it.
|
|
163
|
+
*/
|
|
71
164
|
isContainer?: boolean;
|
|
165
|
+
/**
|
|
166
|
+
* The authoring tier this entry belongs to (objectui#10735).
|
|
167
|
+
*
|
|
168
|
+
* Absent = the curated PUBLIC tier (ADR-0080 §4), the JSON-surface
|
|
169
|
+
* AI-authoring vocabulary. `'html'` = one of the intrinsic HTML elements a
|
|
170
|
+
* `kind:'html'` page may author (ADR-0081 §2), declared by the registry's
|
|
171
|
+
* `HTML_TIER_INTRINSICS` roster and carried here so the ONE flat `components`
|
|
172
|
+
* map can serve both of its readers: a whitelist reader (`compile`, the
|
|
173
|
+
* objectstack gate) takes every key and needs no change; a reader that means
|
|
174
|
+
* the curated vocabulary — the `kind:'react'` scope, the block list, a census
|
|
175
|
+
* of curated blocks — filters this key out.
|
|
176
|
+
*
|
|
177
|
+
* ⛔ Not the registration's `tier` copied through. `manifestFromConfigs`
|
|
178
|
+
* writes exactly `'html'` or nothing, so every entry published before this
|
|
179
|
+
* key existed serialises byte-identically.
|
|
180
|
+
*/
|
|
181
|
+
tier?: 'html';
|
|
72
182
|
}
|
|
73
183
|
export interface Manifest {
|
|
74
184
|
/** keyed by component `type` */
|
|
@@ -94,11 +204,22 @@ export interface ManifestValidationResult {
|
|
|
94
204
|
diagnostics: Diagnostic[];
|
|
95
205
|
/** unique plugin namespaces referenced — the page's `requires` */
|
|
96
206
|
requires: string[];
|
|
97
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Binding sites the server must resolve against object schema.
|
|
209
|
+
*
|
|
210
|
+
* `kind` is a PRODUCER face, not a reader face: `validateTree` writes it,
|
|
211
|
+
* copying {@link ManifestInput.binding} at the one site that builds this
|
|
212
|
+
* array. So the subset relation that licenses a permissive READER runs the
|
|
213
|
+
* other way here — a wider union accepts nothing extra, it obliges every
|
|
214
|
+
* consumer to handle an arm this package cannot emit. That is why the
|
|
215
|
+
* retired `'field'` arm (`9e37d9b39` on the input boundary, objectui#8315
|
|
216
|
+
* here) is gone from this end as well; the measurements are on
|
|
217
|
+
* {@link ManifestInput.binding}.
|
|
218
|
+
*/
|
|
98
219
|
bindings: Array<{
|
|
99
220
|
tag: string;
|
|
100
221
|
input: string;
|
|
101
|
-
kind: 'object'
|
|
222
|
+
kind: 'object';
|
|
102
223
|
value: unknown;
|
|
103
224
|
}>;
|
|
104
225
|
}
|
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 *text* into
|
|
6
|
-
*
|
|
5
|
+
* constrained JSX *text* into an SDUI node tree (`ParsedSchemaNode`, below).
|
|
6
|
+
* It PARSES — it never executes. No `import`, no `eval`, no JS.
|
|
7
7
|
*/
|
|
8
8
|
export {};
|