@objectstack/sdui-parser 17.1.0 → 17.3.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/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/parse.ts
2
2
  var EVENT_ATTR = /^on[A-Z]/;
3
3
  var FORBIDDEN_ATTRS = /* @__PURE__ */ new Set(["dangerouslySetInnerHTML", "ref", "key"]);
4
+ var DISCRIMINATOR_ATTR = "type";
4
5
  function parseJsx(source, options = {}) {
5
6
  return new Parser(source, options).parseDocument();
6
7
  }
@@ -47,7 +48,7 @@ var Parser = class {
47
48
  if (c === "" || c === ">" || c === "/") break;
48
49
  const attr = this.parseAttr(start, tag);
49
50
  if (!attr) break;
50
- props[attr.name] = attr.value;
51
+ if (!attr.drop) props[attr.name] = attr.value;
51
52
  }
52
53
  this.skipWs();
53
54
  let children;
@@ -58,7 +59,7 @@ var Parser = class {
58
59
  } else {
59
60
  this.error("unterminated-open-tag", `Unterminated <${tag}> open tag`, start, tag);
60
61
  }
61
- const node = { type: tag, ...props };
62
+ const node = { ...props, type: tag };
62
63
  if (children && children.length) node.children = children;
63
64
  return node;
64
65
  }
@@ -75,6 +76,15 @@ var Parser = class {
75
76
  this.skipWs();
76
77
  value = this.parseAttrValue(tag);
77
78
  }
79
+ if (name === DISCRIMINATOR_ATTR) {
80
+ this.error(
81
+ "forbidden-attr",
82
+ `Attribute "${DISCRIMINATOR_ATTR}" is not allowed on <${tag}> \u2014 on this tier the tag name IS the component, so <${tag}> already means type "${tag}". Delete the attribute, or write the tag of the component you meant.`,
83
+ elStart,
84
+ tag
85
+ );
86
+ return { name, value: void 0, drop: true };
87
+ }
78
88
  if (EVENT_ATTR.test(name) || FORBIDDEN_ATTRS.has(name)) {
79
89
  this.error("forbidden-attr", `Attribute "${name}" is not allowed on <${tag}>`, elStart, tag);
80
90
  return { name: `__forbidden_${name}`, value: void 0 };
@@ -132,8 +142,17 @@ var Parser = class {
132
142
  continue;
133
143
  }
134
144
  const text = this.readTextRun();
135
- const trimmed = text.replace(/\s+/g, " ").trim();
136
- if (trimmed) children.push(trimmed);
145
+ const collapsed = text.replace(/\s+/g, " ");
146
+ const core = collapsed.trim();
147
+ const afterSibling = children.length > 0;
148
+ const beforeElement = this.peek() === "<" && !this.src.startsWith("</", this.pos);
149
+ if (core) {
150
+ const lead = afterSibling && collapsed.startsWith(" ") ? " " : "";
151
+ const trail = beforeElement && collapsed.endsWith(" ") ? " " : "";
152
+ children.push(`${lead}${core}${trail}`);
153
+ } else if (collapsed && afterSibling && beforeElement) {
154
+ children.push(" ");
155
+ }
137
156
  }
138
157
  return children;
139
158
  }
@@ -217,9 +236,268 @@ function interpretBrace(raw) {
217
236
  try {
218
237
  return JSON.parse(trimmed);
219
238
  } catch {
220
- return { $expr: trimmed };
239
+ const literal = readLiteral(trimmed);
240
+ return literal === NOT_LITERAL ? { $expr: trimmed } : literal;
221
241
  }
222
242
  }
243
+ var NOT_LITERAL = /* @__PURE__ */ Symbol("not-a-literal");
244
+ var LITERAL_WS = /[ \t\n\r]/;
245
+ var IDENT_START = /[A-Za-z_$]/;
246
+ var IDENT_CHAR = /[A-Za-z0-9_$]/;
247
+ var NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/;
248
+ var SIMPLE_ESCAPE = {
249
+ '"': '"',
250
+ "\\": "\\",
251
+ "/": "/",
252
+ b: "\b",
253
+ f: "\f",
254
+ n: "\n",
255
+ r: "\r",
256
+ t: " "
257
+ };
258
+ function readLiteral(src) {
259
+ const reader = new LiteralReader(src);
260
+ const value = reader.value();
261
+ if (value === NOT_LITERAL) return NOT_LITERAL;
262
+ reader.ws();
263
+ return reader.done() ? value : NOT_LITERAL;
264
+ }
265
+ var LiteralReader = class {
266
+ constructor(src) {
267
+ this.src = src;
268
+ this.pos = 0;
269
+ }
270
+ done() {
271
+ return this.pos >= this.src.length;
272
+ }
273
+ ws() {
274
+ while (this.pos < this.src.length && LITERAL_WS.test(this.src[this.pos])) this.pos++;
275
+ }
276
+ value() {
277
+ this.ws();
278
+ const c = this.src[this.pos];
279
+ if (c === void 0) return NOT_LITERAL;
280
+ if (c === '"' || c === "'") return this.string(c);
281
+ if (c === "[") return this.array();
282
+ if (c === "{") return this.object();
283
+ if (this.keyword("true")) return true;
284
+ if (this.keyword("false")) return false;
285
+ if (this.keyword("null")) return null;
286
+ return this.number();
287
+ }
288
+ /** A keyword only when it is not the prefix of a longer identifier. */
289
+ keyword(word) {
290
+ if (!this.src.startsWith(word, this.pos)) return false;
291
+ const after = this.src[this.pos + word.length];
292
+ if (after !== void 0 && IDENT_CHAR.test(after)) return false;
293
+ this.pos += word.length;
294
+ return true;
295
+ }
296
+ number() {
297
+ const m = NUMBER.exec(this.src.slice(this.pos));
298
+ if (!m) return NOT_LITERAL;
299
+ this.pos += m[0].length;
300
+ return Number(m[0]);
301
+ }
302
+ string(quote) {
303
+ this.pos++;
304
+ let out = "";
305
+ for (; ; ) {
306
+ const c = this.src[this.pos];
307
+ if (c === void 0) return NOT_LITERAL;
308
+ if (c === quote) {
309
+ this.pos++;
310
+ return out;
311
+ }
312
+ if (c === "\\") {
313
+ const esc = this.src[this.pos + 1];
314
+ if (esc === void 0) return NOT_LITERAL;
315
+ if (esc === "u") {
316
+ const hex = this.src.slice(this.pos + 2, this.pos + 6);
317
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) return NOT_LITERAL;
318
+ out += String.fromCharCode(parseInt(hex, 16));
319
+ this.pos += 6;
320
+ continue;
321
+ }
322
+ if (esc === "'" && quote === "'") {
323
+ out += "'";
324
+ this.pos += 2;
325
+ continue;
326
+ }
327
+ const simple = SIMPLE_ESCAPE[esc];
328
+ if (simple === void 0) return NOT_LITERAL;
329
+ out += simple;
330
+ this.pos += 2;
331
+ continue;
332
+ }
333
+ if (c < " ") return NOT_LITERAL;
334
+ out += c;
335
+ this.pos++;
336
+ }
337
+ }
338
+ array() {
339
+ this.pos++;
340
+ const out = [];
341
+ this.ws();
342
+ if (this.src[this.pos] === "]") {
343
+ this.pos++;
344
+ return out;
345
+ }
346
+ for (; ; ) {
347
+ const item = this.value();
348
+ if (item === NOT_LITERAL) return NOT_LITERAL;
349
+ out.push(item);
350
+ this.ws();
351
+ const c = this.src[this.pos];
352
+ if (c === ",") {
353
+ this.pos++;
354
+ continue;
355
+ }
356
+ if (c === "]") {
357
+ this.pos++;
358
+ return out;
359
+ }
360
+ return NOT_LITERAL;
361
+ }
362
+ }
363
+ object() {
364
+ this.pos++;
365
+ const out = {};
366
+ this.ws();
367
+ if (this.src[this.pos] === "}") {
368
+ this.pos++;
369
+ return out;
370
+ }
371
+ for (; ; ) {
372
+ this.ws();
373
+ const key = this.key();
374
+ if (key === NOT_LITERAL) return NOT_LITERAL;
375
+ this.ws();
376
+ if (this.src[this.pos] !== ":") return NOT_LITERAL;
377
+ this.pos++;
378
+ const item = this.value();
379
+ if (item === NOT_LITERAL) return NOT_LITERAL;
380
+ Object.defineProperty(out, key, {
381
+ value: item,
382
+ writable: true,
383
+ enumerable: true,
384
+ configurable: true
385
+ });
386
+ this.ws();
387
+ const c = this.src[this.pos];
388
+ if (c === ",") {
389
+ this.pos++;
390
+ continue;
391
+ }
392
+ if (c === "}") {
393
+ this.pos++;
394
+ return out;
395
+ }
396
+ return NOT_LITERAL;
397
+ }
398
+ }
399
+ /** A quoted string, or a bare identifier — the second ruled widening. */
400
+ key() {
401
+ const c = this.src[this.pos];
402
+ if (c === '"' || c === "'") return this.string(c);
403
+ if (c !== void 0 && IDENT_START.test(c)) {
404
+ const start = this.pos;
405
+ this.pos++;
406
+ while (this.pos < this.src.length && IDENT_CHAR.test(this.src[this.pos])) this.pos++;
407
+ return this.src.slice(start, this.pos);
408
+ }
409
+ return NOT_LITERAL;
410
+ }
411
+ };
412
+
413
+ // src/input-type.ts
414
+ var MANIFEST_INPUT_TYPES = /* @__PURE__ */ new Set([
415
+ "string",
416
+ "number",
417
+ "boolean",
418
+ "enum",
419
+ "array",
420
+ "object",
421
+ "color",
422
+ "date",
423
+ "code",
424
+ "file",
425
+ "slot"
426
+ ]);
427
+ function inputTypeArms(type) {
428
+ if (type === void 0) return [];
429
+ return Array.isArray(type) ? type : [type];
430
+ }
431
+ function canonicalizeInputType(type) {
432
+ const arms = (Array.isArray(type) ? type : [type]).filter(
433
+ (arm) => typeof arm === "string" && MANIFEST_INPUT_TYPES.has(arm)
434
+ );
435
+ const distinct = [...new Set(arms)];
436
+ if (distinct.length === 0) return "string";
437
+ if (distinct.length === 1) return distinct[0];
438
+ return distinct;
439
+ }
440
+
441
+ // src/dashboard-widget-options.ts
442
+ var UNCONSUMED_WIDGET_OPTION = "unconsumed-widget-option";
443
+ var DASHBOARD_WIDGET_HOST_TYPES = /* @__PURE__ */ new Set([
444
+ "dashboard",
445
+ "dashboard-grid"
446
+ ]);
447
+ var CONSUMED_WIDGET_OPTION_KEYS = [
448
+ "dateGranularity",
449
+ "description",
450
+ "limit",
451
+ "sortBy",
452
+ "sortOrder",
453
+ "stageOrder"
454
+ ];
455
+ var CONSUMED = new Set(CONSUMED_WIDGET_OPTION_KEYS);
456
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
457
+ var isExpr = (v) => isPlainObject(v) && "$expr" in v;
458
+ function checkDashboardWidgetOptions(node) {
459
+ if (!DASHBOARD_WIDGET_HOST_TYPES.has(node.type)) return [];
460
+ const widgets = node.widgets;
461
+ if (!Array.isArray(widgets)) return [];
462
+ const diagnostics = [];
463
+ widgets.forEach((widget, index) => {
464
+ if (!isPlainObject(widget) || isExpr(widget)) return;
465
+ if (widget.component !== void 0) return;
466
+ if (widget.dataset === void 0 || widget.dataset === null || widget.dataset === "") return;
467
+ const options = widget.options;
468
+ if (!isPlainObject(options) || isExpr(options)) return;
469
+ if (Array.isArray(widget.suppressWarnings) && widget.suppressWarnings.includes(UNCONSUMED_WIDGET_OPTION)) {
470
+ return;
471
+ }
472
+ const label = typeof widget.id === "string" && widget.id !== "" ? widget.id : `#${index}`;
473
+ const widgetType = typeof widget.type === "string" && widget.type !== "" ? widget.type : "widget";
474
+ for (const key of Object.keys(options)) {
475
+ if (CONSUMED.has(key)) continue;
476
+ diagnostics.push({
477
+ severity: "warning",
478
+ // DIVERGENCE FROM OBJECTUI, and the only one below this file's header:
479
+ // objectui writes `code: UNCONSUMED_WIDGET_OPTION` here. This repo runs
480
+ // `check:dispatcher-error-vocabulary`, whose `objlitconst` shape reads
481
+ // the SCREAMING_SNAKE constant NAME at a `code:` position and then must
482
+ // reduce it to a literal — and its literal grammar is
483
+ // `[A-Za-z][A-Za-z0-9_]*`, which a KEBAB-case value cannot satisfy. So
484
+ // the constant form is reported as an unresolvable code constant, and
485
+ // that finding cannot be declared away. `unconsumed-widget-option` is a
486
+ // parser DIAGNOSTIC code, not an ADR-0112 wire code, and an inline
487
+ // quoted literal is the form both vocabulary gates already accept for
488
+ // the six sibling diagnostic codes in `validate.ts`
489
+ // (`unknown-component`, `unknown-prop`, `not-a-container`,
490
+ // `inert-expression`, `type-mismatch`, `invalid-enum`). The emitted
491
+ // VALUE is unchanged, and the test next door pins it equal to
492
+ // `UNCONSUMED_WIDGET_OPTION` so the two spellings cannot drift apart.
493
+ code: "unconsumed-widget-option",
494
+ message: `<${node.type}> widget "${label}" (${widgetType}): options.${key} reaches no renderer \u2014 dashboard widget renderers read only: ${CONSUMED_WIDGET_OPTION_KEYS.join(", ")}`,
495
+ tag: node.type
496
+ });
497
+ }
498
+ });
499
+ return diagnostics;
500
+ }
223
501
 
224
502
  // src/validate.ts
225
503
  var BASE_PROPS = /* @__PURE__ */ new Set([
@@ -233,7 +511,7 @@ var BASE_PROPS = /* @__PURE__ */ new Set([
233
511
  "disabledOn",
234
512
  "children"
235
513
  ]);
236
- var isExpr = (v) => typeof v === "object" && v !== null && "$expr" in v;
514
+ var isExpr2 = (v) => typeof v === "object" && v !== null && "$expr" in v;
237
515
  function validateTree(tree, manifest) {
238
516
  const diagnostics = [];
239
517
  const requires = /* @__PURE__ */ new Set();
@@ -276,7 +554,14 @@ function validateTree(tree, manifest) {
276
554
  if (input.binding) {
277
555
  bindings.push({ tag: node.type, input: key, kind: input.binding, value });
278
556
  }
279
- if (!isExpr(value)) {
557
+ if (isExpr2(value)) {
558
+ diagnostics.push({
559
+ severity: "warning",
560
+ code: "inert-expression",
561
+ message: `<${node.type}> prop "${key}" is a braced expression this tier never evaluates \u2014 the value will be silently ignored at render. This tier materializes LITERALS only (strings, numbers, booleans, null, arrays, objects; quotes may be single or double, object keys may be unquoted), e.g. columns={['name','amount']} works \u2014 columns={rows.map((r) => r.name)} cannot`,
562
+ tag: node.type
563
+ });
564
+ } else {
280
565
  const typeDiag = checkType(node.type, input, value);
281
566
  if (typeDiag) diagnostics.push(typeDiag);
282
567
  }
@@ -289,46 +574,70 @@ function validateTree(tree, manifest) {
289
574
  tag: node.type
290
575
  });
291
576
  }
577
+ diagnostics.push(...checkDashboardWidgetOptions(node));
292
578
  }
293
579
  if (node.children) node.children.forEach(visit);
294
580
  };
295
581
  if (tree) visit(tree);
296
582
  return { diagnostics, requires: [...requires], bindings };
297
583
  }
298
- function checkType(tag, input, value) {
299
- const mismatch = (expected) => ({
300
- severity: "warning",
301
- code: "type-mismatch",
302
- message: `<${tag}> prop "${input.name}" expected ${expected}`,
303
- tag
304
- });
305
- switch (input.type) {
584
+ var enumValues = (input) => (input.enum ?? []).map((e) => typeof e === "object" ? e.value : e);
585
+ function armAccepts(arm, input, value) {
586
+ switch (arm) {
306
587
  case "number":
307
- return typeof value === "number" ? null : mismatch("a number");
588
+ return typeof value === "number";
308
589
  case "boolean":
309
- return typeof value === "boolean" ? null : mismatch("a boolean");
590
+ return typeof value === "boolean";
310
591
  case "string":
311
592
  case "color":
312
593
  case "date":
313
594
  case "code":
314
595
  case "file":
315
- return typeof value === "string" ? null : mismatch("a string");
596
+ return typeof value === "string";
316
597
  case "array":
317
- return Array.isArray(value) ? null : mismatch("an array");
598
+ return Array.isArray(value);
318
599
  case "object":
319
- return typeof value === "object" && value !== null && !Array.isArray(value) ? null : mismatch("an object");
320
- case "enum": {
321
- const allowed = (input.enum ?? []).map((e) => typeof e === "object" ? e.value : e);
322
- return allowed.includes(value) ? null : {
323
- severity: "error",
324
- code: "invalid-enum",
325
- message: `<${tag}> prop "${input.name}"=${JSON.stringify(value)} is not one of ${JSON.stringify(allowed)}`,
326
- tag
327
- };
328
- }
600
+ return typeof value === "object" && value !== null && !Array.isArray(value);
601
+ case "enum":
602
+ return enumValues(input).includes(value);
329
603
  default:
330
- return null;
604
+ return true;
605
+ }
606
+ }
607
+ function armExpectation(arm, input) {
608
+ switch (arm) {
609
+ case "number":
610
+ return "a number";
611
+ case "boolean":
612
+ return "a boolean";
613
+ case "array":
614
+ return "an array";
615
+ case "object":
616
+ return "an object";
617
+ case "enum":
618
+ return `one of ${JSON.stringify(enumValues(input))}`;
619
+ default:
620
+ return "a string";
621
+ }
622
+ }
623
+ function checkType(tag, input, value) {
624
+ const arms = inputTypeArms(input.type);
625
+ if (arms.length === 0) return null;
626
+ if (arms.some((arm) => armAccepts(arm, input, value))) return null;
627
+ if (arms.length === 1 && arms[0] === "enum") {
628
+ return {
629
+ severity: "error",
630
+ code: "invalid-enum",
631
+ message: `<${tag}> prop "${input.name}"=${JSON.stringify(value)} is not one of ${JSON.stringify(enumValues(input))}`,
632
+ tag
633
+ };
331
634
  }
635
+ return {
636
+ severity: arms.includes("enum") ? "error" : "warning",
637
+ code: "type-mismatch",
638
+ message: `<${tag}> prop "${input.name}" expected ${arms.map((arm) => armExpectation(arm, input)).join(" or ")}`,
639
+ tag
640
+ };
332
641
  }
333
642
 
334
643
  // src/codegen.ts
@@ -345,7 +654,6 @@ function generateDts(manifest, options = {}) {
345
654
  interface ElementChildrenAttribute { children: object; }` : "";
346
655
  return `// AUTO-GENERATED by @object-ui/sdui-parser \u2014 DO NOT EDIT.
347
656
  // Source of truth: ComponentRegistry inputs (ADR-0080 \xA73). Regenerate via codegen.
348
- /* eslint-disable */
349
657
 
350
658
  export interface SduiBaseProps {
351
659
  id?: string;
@@ -372,17 +680,18 @@ export {};
372
680
  `;
373
681
  }
374
682
  function emitInterface(comp) {
375
- const lines = comp.inputs.filter((i) => i.type !== "slot").map((i) => ` ${propLine(i)}`).join("\n");
683
+ const lines = comp.inputs.filter((i) => valueArms(i).length > 0).map((i) => ` ${propLine(i)}`).join("\n");
376
684
  return `export interface ${propsName(comp.type)} extends SduiBaseProps {
377
685
  ${lines}
378
686
  }`;
379
687
  }
688
+ var valueArms = (input) => inputTypeArms(input.type).filter((arm) => arm !== "slot");
380
689
  function propLine(input) {
381
690
  const opt = input.required ? "" : "?";
382
691
  return `${quoteKeyIfNeeded(input.name)}${opt}: ${tsType(input)};`;
383
692
  }
384
- function tsType(input) {
385
- switch (input.type) {
693
+ function armTsType(arm, input) {
694
+ switch (arm) {
386
695
  case "number":
387
696
  return "number";
388
697
  case "boolean":
@@ -395,15 +704,16 @@ function tsType(input) {
395
704
  const vals = (input.enum ?? []).map((e) => typeof e === "object" ? e.value : e);
396
705
  return vals.length ? vals.map((v) => JSON.stringify(v)).join(" | ") : "string";
397
706
  }
398
- case "string":
399
- case "color":
400
- case "date":
401
- case "code":
402
- case "file":
403
707
  default:
404
708
  return "string";
405
709
  }
406
710
  }
711
+ function tsType(input) {
712
+ const arms = valueArms(input);
713
+ if (arms.length === 0) return "string";
714
+ const emitted = [...new Set(arms.map((arm) => armTsType(arm, input)))];
715
+ return emitted.join(" | ");
716
+ }
407
717
  var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
408
718
  var quoteKeyIfNeeded = (name) => IDENT.test(name) ? name : JSON.stringify(name);
409
719
  function propsName(type) {
@@ -442,19 +752,6 @@ function compile(source, manifest) {
442
752
  ok: !diagnostics.some((d) => d.severity === "error")
443
753
  };
444
754
  }
445
- var INPUT_TYPES = /* @__PURE__ */ new Set([
446
- "string",
447
- "number",
448
- "boolean",
449
- "enum",
450
- "array",
451
- "object",
452
- "color",
453
- "date",
454
- "code",
455
- "file",
456
- "slot"
457
- ]);
458
755
  function manifestFromConfigs(configs, opts = {}) {
459
756
  const components = {};
460
757
  for (const c of configs) {
@@ -466,7 +763,7 @@ function manifestFromConfigs(configs, opts = {}) {
466
763
  isContainer: c.isContainer,
467
764
  inputs: (c.inputs ?? []).map((i) => ({
468
765
  name: i.name,
469
- type: INPUT_TYPES.has(i.type) ? i.type : "string",
766
+ type: canonicalizeInputType(i.type),
470
767
  required: i.required,
471
768
  enum: i.enum,
472
769
  binding: i.binding,
@@ -477,9 +774,16 @@ function manifestFromConfigs(configs, opts = {}) {
477
774
  return { components };
478
775
  }
479
776
  export {
777
+ CONSUMED_WIDGET_OPTION_KEYS,
778
+ DASHBOARD_WIDGET_HOST_TYPES,
779
+ MANIFEST_INPUT_TYPES,
780
+ UNCONSUMED_WIDGET_OPTION,
781
+ canonicalizeInputType,
782
+ checkDashboardWidgetOptions,
480
783
  compile,
481
784
  generateBlockList,
482
785
  generateDts,
786
+ inputTypeArms,
483
787
  interpretBrace,
484
788
  manifestFromConfigs,
485
789
  parseJsx,