@terrazzo/parser 2.0.0-alpha.2 → 2.0.0-alpha.3

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.js CHANGED
@@ -1,11 +1,42 @@
1
1
  import wcmatch from "wildcard-match";
2
2
  import * as momoa from "@humanwhocodes/momoa";
3
+ import { evaluate } from "@humanwhocodes/momoa";
3
4
  import pc from "picocolors";
4
5
  import { merge } from "merge-anything";
5
6
  import { BORDER_REQUIRED_PROPERTIES, COLORSPACE, FONT_WEIGHTS, GRADIENT_REQUIRED_STOP_PROPERTIES, SHADOW_REQUIRED_PROPERTIES, STROKE_STYLE_LINE_CAP_VALUES, STROKE_STYLE_OBJECT_REQUIRED_PROPERTIES, STROKE_STYLE_STRING_VALUES, TRANSITION_REQUIRED_PROPERTIES, isAlias, parseAlias, parseColor, pluralize, tokenToCulori } from "@terrazzo/token-tools";
6
7
  import { clampChroma, wcagContrast } from "culori";
7
- import { bundle, getObjMember, getObjMembers, isPure$ref, parseRef, replaceNode, traverseAsync } from "@terrazzo/json-schema-tools";
8
+ import { bundle, getObjMember, getObjMembers, isPure$ref, maybeRawJSON, parseRef, replaceNode, traverseAsync } from "@terrazzo/json-schema-tools";
8
9
 
10
+ //#region rolldown:runtime
11
+ var __create = Object.create;
12
+ var __defProp = Object.defineProperty;
13
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
14
+ var __getOwnPropNames = Object.getOwnPropertyNames;
15
+ var __getProtoOf = Object.getPrototypeOf;
16
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
17
+ var __commonJS = (cb, mod) => function() {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ };
20
+ var __copyProps = (to, from, except, desc) => {
21
+ if (from && typeof from === "object" || typeof from === "function") {
22
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
23
+ key = keys[i];
24
+ if (!__hasOwnProp.call(to, key) && key !== except) {
25
+ __defProp(to, key, {
26
+ get: ((k) => from[k]).bind(null, key),
27
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
28
+ });
29
+ }
30
+ }
31
+ }
32
+ return to;
33
+ };
34
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
35
+ value: mod,
36
+ enumerable: true
37
+ }) : target, mod));
38
+
39
+ //#endregion
9
40
  //#region src/lib/code-frame.ts
10
41
  /**
11
42
  * Extract what lines should be marked and highlighted.
@@ -34,10 +65,8 @@ function getMarkerLines(loc, source, opts = {}) {
34
65
  if (lineDiff) for (let i = 0; i <= lineDiff; i++) {
35
66
  const lineNumber = i + startLine;
36
67
  if (!startColumn) markerLines[lineNumber] = true;
37
- else if (i === 0) {
38
- const sourceLength = source[lineNumber - 1].length;
39
- markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
40
- } else if (i === lineDiff) markerLines[lineNumber] = [0, endColumn];
68
+ else if (i === 0) markerLines[lineNumber] = [startColumn, source[lineNumber - 1].length - startColumn + 1];
69
+ else if (i === lineDiff) markerLines[lineNumber] = [0, endColumn];
41
70
  else markerLines[lineNumber] = [0, source[lineNumber - i].length];
42
71
  }
43
72
  else if (startColumn === endColumn) if (startColumn) markerLines[startLine] = [startColumn, 0];
@@ -55,8 +84,7 @@ function getMarkerLines(loc, source, opts = {}) {
55
84
  const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
56
85
  function codeFrameColumns(rawLines, loc, opts = {}) {
57
86
  if (typeof rawLines !== "string") throw new Error(`Expected string, got ${rawLines}`);
58
- const lines = rawLines.split(NEWLINE);
59
- const { start, end, markerLines } = getMarkerLines(loc, lines, opts);
87
+ const { start, end, markerLines } = getMarkerLines(loc, rawLines.split(NEWLINE), opts);
60
88
  const hasColumns = loc.start && typeof loc.start.column === "number";
61
89
  const numberMaxWidth = String(end).length;
62
90
  let frame = rawLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
@@ -354,7 +382,7 @@ async function build(tokens, { sources, logger = new Logger(), config }) {
354
382
  //#endregion
355
383
  //#region src/lint/plugin-core/lib/docs.ts
356
384
  function docsLink(ruleName) {
357
- return `https://terrazzo.app/docs/cli/lint#${ruleName.replaceAll("/", "")}`;
385
+ return `https://terrazzo.app/docs/linting#${ruleName.replaceAll("/", "")}`;
358
386
  }
359
387
 
360
388
  //#endregion
@@ -390,9 +418,7 @@ const rule$26 = {
390
418
  if (tokens[foreground].$type !== "color") throw new Error(`Token ${foreground} isn’t a color`);
391
419
  if (!tokens[background]) throw new Error(`Token ${background} does not exist`);
392
420
  if (tokens[background].$type !== "color") throw new Error(`Token ${background} isn’t a color`);
393
- const a = tokenToCulori(tokens[foreground].$value);
394
- const b = tokenToCulori(tokens[background].$value);
395
- const contrast = wcagContrast(a, b);
421
+ const contrast = wcagContrast(tokenToCulori(tokens[foreground].$value), tokenToCulori(tokens[background].$value));
396
422
  const min = WCAG2_MIN_CONTRAST[options.level ?? "AA"][largeText ? "large" : "default"];
397
423
  if (contrast < min) report({
398
424
  messageId: ERROR_INSUFFICIENT_CONTRAST,
@@ -719,8 +745,7 @@ function isWithinGamut(color, gamut) {
719
745
  "hsl",
720
746
  "hwb"
721
747
  ].includes(parsed.mode)) return true;
722
- const clamped = clampChroma(parsed, parsed.mode, gamut === "srgb" ? "rgb" : gamut);
723
- return isWithinThreshold(parsed, clamped);
748
+ return isWithinThreshold(parsed, clampChroma(parsed, parsed.mode, gamut === "srgb" ? "rgb" : gamut));
724
749
  }
725
750
  /** is Color A close enough to Color B? */
726
751
  function isWithinThreshold(a, b, tolerance = TOLERANCE) {
@@ -807,36 +832,13 @@ const rule$20 = {
807
832
  };
808
833
  var max_gamut_default = rule$20;
809
834
 
810
- //#endregion
811
- //#region src/lint/plugin-core/rules/no-type-on-alias.ts
812
- const NO_TYPE_ON_ALIAS = "core/no-type-on-alias";
813
- const ERROR$10 = "ERROR";
814
- const rule$19 = {
815
- meta: {
816
- messages: { [ERROR$10]: "Remove $type from aliased value." },
817
- docs: {
818
- description: "If a $value is aliased it already has a $type defined.",
819
- url: docsLink(NO_TYPE_ON_ALIAS)
820
- }
821
- },
822
- defaultOptions: {},
823
- create({ tokens, report }) {
824
- for (const t of Object.values(tokens)) if (isAlias(t.originalValue.$value) && t.originalValue?.$type) report({
825
- messageId: ERROR$10,
826
- node: t.source.node,
827
- filename: t.source.filename
828
- });
829
- }
830
- };
831
- var no_type_on_alias_default = rule$19;
832
-
833
835
  //#endregion
834
836
  //#region src/lint/plugin-core/rules/required-children.ts
835
837
  const REQUIRED_CHILDREN = "core/required-children";
836
838
  const ERROR_EMPTY_MATCH = "EMPTY_MATCH";
837
839
  const ERROR_MISSING_REQUIRED_TOKENS = "MISSING_REQUIRED_TOKENS";
838
840
  const ERROR_MISSING_REQUIRED_GROUP = "MISSING_REQUIRED_GROUP";
839
- const rule$18 = {
841
+ const rule$19 = {
840
842
  meta: {
841
843
  messages: {
842
844
  [ERROR_EMPTY_MATCH]: "No tokens matched {{ matcher }}",
@@ -894,12 +896,12 @@ const rule$18 = {
894
896
  }
895
897
  }
896
898
  };
897
- var required_children_default = rule$18;
899
+ var required_children_default = rule$19;
898
900
 
899
901
  //#endregion
900
902
  //#region src/lint/plugin-core/rules/required-modes.ts
901
903
  const REQUIRED_MODES = "core/required-modes";
902
- const rule$17 = {
904
+ const rule$18 = {
903
905
  meta: { docs: {
904
906
  description: "Enforce certain tokens have specific modes.",
905
907
  url: docsLink(REQUIRED_MODES)
@@ -930,7 +932,30 @@ const rule$17 = {
930
932
  }
931
933
  }
932
934
  };
933
- var required_modes_default = rule$17;
935
+ var required_modes_default = rule$18;
936
+
937
+ //#endregion
938
+ //#region src/lint/plugin-core/rules/required-type.ts
939
+ const REQUIRED_TYPE = "core/required-type";
940
+ const ERROR$10 = "ERROR";
941
+ const rule$17 = {
942
+ meta: {
943
+ messages: { [ERROR$10]: "Token missing $type." },
944
+ docs: {
945
+ description: "Requiring every token to have $type, even aliases, simplifies computation.",
946
+ url: docsLink(REQUIRED_TYPE)
947
+ }
948
+ },
949
+ defaultOptions: {},
950
+ create({ tokens, report }) {
951
+ for (const t of Object.values(tokens)) if (!t.originalValue?.$type) report({
952
+ messageId: ERROR$10,
953
+ node: t.source.node,
954
+ filename: t.source.filename
955
+ });
956
+ }
957
+ };
958
+ var required_type_default = rule$17;
934
959
 
935
960
  //#endregion
936
961
  //#region src/lint/plugin-core/rules/required-typography-properties.ts
@@ -1340,8 +1365,7 @@ const rule$11 = {
1340
1365
  break;
1341
1366
  case "strokeStyle":
1342
1367
  if (typeof t.originalValue.$value === "object" && Array.isArray(t.originalValue.$value.dashArray)) {
1343
- const $valueNode = getObjMember(t.source.node, "$value");
1344
- const dashArray = getObjMember($valueNode, "dashArray");
1368
+ const dashArray = getObjMember(getObjMember(t.source.node, "$value"), "dashArray");
1345
1369
  for (let i = 0; i < t.originalValue.$value.dashArray.length; i++) {
1346
1370
  if (isAlias(t.originalValue.$value.dashArray[i])) continue;
1347
1371
  validateDimension(t.originalValue.$value.dashArray[i], {
@@ -1359,8 +1383,7 @@ const rule$11 = {
1359
1383
  filename: t.source.filename
1360
1384
  });
1361
1385
  if (typeof t.originalValue.$value.style === "object" && Array.isArray(t.originalValue.$value.style.dashArray)) {
1362
- const style = getObjMember($valueNode, "style");
1363
- const dashArray = getObjMember(style, "dashArray");
1386
+ const dashArray = getObjMember(getObjMember($valueNode, "style"), "dashArray");
1364
1387
  for (let i = 0; i < t.originalValue.$value.style.dashArray.length; i++) {
1365
1388
  if (isAlias(t.originalValue.$value.style.dashArray[i])) continue;
1366
1389
  validateDimension(t.originalValue.$value.style.dashArray[i], {
@@ -1580,8 +1603,7 @@ const rule$9 = {
1580
1603
  case "typography":
1581
1604
  if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontFamily) {
1582
1605
  if (t.partialAliasOf?.fontFamily) continue;
1583
- const $value = getObjMember(t.source.node, "$value");
1584
- const properties = getObjMembers($value);
1606
+ const properties = getObjMembers(getObjMember(t.source.node, "$value"));
1585
1607
  validateFontFamily(t.originalValue.$value.fontFamily, {
1586
1608
  node: properties.fontFamily,
1587
1609
  filename: t.source.filename
@@ -1643,8 +1665,7 @@ const rule$8 = {
1643
1665
  case "typography":
1644
1666
  if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontWeight) {
1645
1667
  if (t.partialAliasOf?.fontWeight) continue;
1646
- const $value = getObjMember(t.source.node, "$value");
1647
- const properties = getObjMembers($value);
1668
+ const properties = getObjMembers(getObjMember(t.source.node, "$value"));
1648
1669
  validateFontWeight(t.originalValue.$value.fontWeight, {
1649
1670
  node: properties.fontWeight,
1650
1671
  filename: t.source.filename
@@ -2113,9 +2134,9 @@ const ALL_RULES = {
2113
2134
  [DESCRIPTIONS]: descriptions_default,
2114
2135
  [DUPLICATE_VALUES]: duplicate_values_default,
2115
2136
  [MAX_GAMUT]: max_gamut_default,
2116
- [NO_TYPE_ON_ALIAS]: no_type_on_alias_default,
2117
2137
  [REQUIRED_CHILDREN]: required_children_default,
2118
2138
  [REQUIRED_MODES]: required_modes_default,
2139
+ [REQUIRED_TYPE]: required_type_default,
2119
2140
  [REQUIRED_TYPOGRAPHY_PROPERTIES]: required_typography_properties_default,
2120
2141
  [A11Y_MIN_CONTRAST]: a11y_min_contrast_default,
2121
2142
  [A11Y_MIN_FONT_SIZE]: a11y_min_font_size_default
@@ -2145,8 +2166,7 @@ const RECOMMENDED_CONFIG = {
2145
2166
  [VALID_SHADOW]: ["error", {}],
2146
2167
  [VALID_GRADIENT]: ["error", {}],
2147
2168
  [VALID_TYPOGRAPHY]: ["error", {}],
2148
- [CONSISTENT_NAMING]: ["warn", { format: "kebab-case" }],
2149
- [NO_TYPE_ON_ALIAS]: ["warn", {}]
2169
+ [CONSISTENT_NAMING]: ["warn", { format: "kebab-case" }]
2150
2170
  };
2151
2171
 
2152
2172
  //#endregion
@@ -2321,7 +2341,7 @@ function normalizeLint({ config, logger }) {
2321
2341
  });
2322
2342
  const value = config.lint.rules[id];
2323
2343
  let severity = "off";
2324
- let options;
2344
+ let options = {};
2325
2345
  if (typeof value === "number" || typeof value === "string") severity = value;
2326
2346
  else if (Array.isArray(value)) {
2327
2347
  severity = value[0];
@@ -2469,8 +2489,40 @@ async function lintRunner({ tokens, filename, config = {}, sources, logger }) {
2469
2489
  });
2470
2490
  }
2471
2491
 
2492
+ //#endregion
2493
+ //#region ../../node_modules/.pnpm/fast-deep-equal@3.1.3/node_modules/fast-deep-equal/index.js
2494
+ var require_fast_deep_equal = /* @__PURE__ */ __commonJS({ "../../node_modules/.pnpm/fast-deep-equal@3.1.3/node_modules/fast-deep-equal/index.js": ((exports, module) => {
2495
+ module.exports = function equal(a, b) {
2496
+ if (a === b) return true;
2497
+ if (a && b && typeof a == "object" && typeof b == "object") {
2498
+ if (a.constructor !== b.constructor) return false;
2499
+ var length, i, keys;
2500
+ if (Array.isArray(a)) {
2501
+ length = a.length;
2502
+ if (length != b.length) return false;
2503
+ for (i = length; i-- !== 0;) if (!equal(a[i], b[i])) return false;
2504
+ return true;
2505
+ }
2506
+ if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
2507
+ if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
2508
+ if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
2509
+ keys = Object.keys(a);
2510
+ length = keys.length;
2511
+ if (length !== Object.keys(b).length) return false;
2512
+ for (i = length; i-- !== 0;) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
2513
+ for (i = length; i-- !== 0;) {
2514
+ var key = keys[i];
2515
+ if (!equal(a[key], b[key])) return false;
2516
+ }
2517
+ return true;
2518
+ }
2519
+ return a !== a && b !== b;
2520
+ };
2521
+ }) });
2522
+
2472
2523
  //#endregion
2473
2524
  //#region src/lib/momoa.ts
2525
+ var import_fast_deep_equal = /* @__PURE__ */ __toESM(require_fast_deep_equal(), 1);
2474
2526
  /** Momoa’s default parser, with preferred settings. */
2475
2527
  function toMomoa(srcRaw) {
2476
2528
  return momoa.parse(typeof srcRaw === "string" ? srcRaw : JSON.stringify(srcRaw, void 0, 2), {
@@ -2480,6 +2532,582 @@ function toMomoa(srcRaw) {
2480
2532
  });
2481
2533
  }
2482
2534
 
2535
+ //#endregion
2536
+ //#region src/resolver/validate.ts
2537
+ /**
2538
+ * Determine whether this is likely a resolver
2539
+ * We use terms the word “likely” because this occurs before validation. Since
2540
+ * we may be dealing with a doc _intended_ to be a resolver, but may be lacking
2541
+ * some critical information, how can we determine intent? There’s a bit of
2542
+ * guesswork here, but we try and find a reasonable edge case where we sniff out
2543
+ * invalid DTCG syntax that a resolver doc would have.
2544
+ */
2545
+ function isLikelyResolver(doc) {
2546
+ if (doc.body.type !== "Object") return false;
2547
+ for (const member of doc.body.members) {
2548
+ if (member.name.type !== "String") continue;
2549
+ switch (member.name.value) {
2550
+ case "name":
2551
+ case "description":
2552
+ case "version":
2553
+ if (member.name.type === "String") return true;
2554
+ break;
2555
+ case "sets":
2556
+ case "modifiers":
2557
+ if (member.value.type !== "Object") continue;
2558
+ if (getObjMember(member.value, "description")?.type === "String") return true;
2559
+ if (member.name.value === "sets" && getObjMember(member.value, "sources")?.type === "Array") return true;
2560
+ else if (member.name.value === "modifiers") {
2561
+ const contexts = getObjMember(member.value, "contexts");
2562
+ if (contexts?.type === "Object" && contexts.members.some((m) => m.value.type === "Array")) return true;
2563
+ }
2564
+ break;
2565
+ case "resolutionOrder":
2566
+ if (member.value.type === "Array") return true;
2567
+ break;
2568
+ }
2569
+ }
2570
+ return false;
2571
+ }
2572
+ const MESSAGE_EXPECTED = {
2573
+ STRING: "Expected string.",
2574
+ OBJECT: "Expected object.",
2575
+ ARRAY: "Expected array."
2576
+ };
2577
+ /**
2578
+ * Validate a resolver document.
2579
+ * There’s a ton of boilerplate here, only to surface detailed code frames. Is there a better abstraction?
2580
+ */
2581
+ function validateResolver(node, { logger, src }) {
2582
+ const entry = {
2583
+ group: "parser",
2584
+ label: "resolver",
2585
+ src
2586
+ };
2587
+ if (node.body.type !== "Object") logger.error({
2588
+ ...entry,
2589
+ message: MESSAGE_EXPECTED.OBJECT,
2590
+ node
2591
+ });
2592
+ const errors = [];
2593
+ let hasVersion = false;
2594
+ let hasResolutionOrder = false;
2595
+ for (const member of node.body.members) {
2596
+ if (member.name.type !== "String") continue;
2597
+ switch (member.name.value) {
2598
+ case "name":
2599
+ case "description":
2600
+ if (member.value.type !== "String") errors.push({
2601
+ ...entry,
2602
+ message: MESSAGE_EXPECTED.STRING
2603
+ });
2604
+ break;
2605
+ case "version":
2606
+ hasVersion = true;
2607
+ if (member.value.type !== "String" || member.value.value !== "2025.10") errors.push({
2608
+ ...entry,
2609
+ message: `Expected "version" to be "2025.10".`,
2610
+ node: member.value
2611
+ });
2612
+ break;
2613
+ case "sets":
2614
+ case "modifiers":
2615
+ if (member.value.type !== "Object") errors.push({
2616
+ ...entry,
2617
+ message: MESSAGE_EXPECTED.OBJECT,
2618
+ node: member.value
2619
+ });
2620
+ else for (const item of member.value.members) if (item.value.type !== "Object") errors.push({
2621
+ ...entry,
2622
+ message: MESSAGE_EXPECTED.OBJECT,
2623
+ node: item.value
2624
+ });
2625
+ else {
2626
+ const validator = member.name.value === "sets" ? validateSet : validateModifier;
2627
+ errors.push(...validator(item.value, false, {
2628
+ logger,
2629
+ src
2630
+ }));
2631
+ }
2632
+ break;
2633
+ case "resolutionOrder":
2634
+ hasResolutionOrder = true;
2635
+ if (member.value.type !== "Array") errors.push({
2636
+ ...entry,
2637
+ message: MESSAGE_EXPECTED.ARRAY,
2638
+ node: member.value
2639
+ });
2640
+ else if (member.value.elements.length === 0) errors.push({
2641
+ ...entry,
2642
+ message: `"resolutionOrder" can’t be empty array.`,
2643
+ node: member.value
2644
+ });
2645
+ else for (const item of member.value.elements) if (item.value.type !== "Object") errors.push({
2646
+ ...entry,
2647
+ message: MESSAGE_EXPECTED.OBJECT,
2648
+ node: item.value
2649
+ });
2650
+ else {
2651
+ const itemMembers = getObjMembers(item.value);
2652
+ if (itemMembers.$ref?.type === "String") continue;
2653
+ if (itemMembers.type?.type === "String") if (itemMembers.type.value === "set") validateSet(item.value, true, {
2654
+ logger,
2655
+ src
2656
+ });
2657
+ else if (itemMembers.type.value === "modifier") validateModifier(item.value, true, {
2658
+ logger,
2659
+ src
2660
+ });
2661
+ else errors.push({
2662
+ ...entry,
2663
+ message: `Unknown type ${JSON.stringify(itemMembers.type.value)}`,
2664
+ node: itemMembers.type
2665
+ });
2666
+ if (itemMembers.sources?.type === "Array") validateSet(item.value, true, {
2667
+ logger,
2668
+ src
2669
+ });
2670
+ else if (itemMembers.contexts?.type === "Object") validateModifier(item.value, true, {
2671
+ logger,
2672
+ src
2673
+ });
2674
+ else if (itemMembers.name?.type === "String" || itemMembers.description?.type === "String") validateSet(item.value, true, {
2675
+ logger,
2676
+ src
2677
+ });
2678
+ }
2679
+ break;
2680
+ case "$defs":
2681
+ case "$extensions":
2682
+ if (member.value.type !== "Object") errors.push({
2683
+ ...entry,
2684
+ message: `Expected object`,
2685
+ node: member.value
2686
+ });
2687
+ break;
2688
+ case "$ref":
2689
+ if (member.value.type !== "String") errors.push({
2690
+ ...entry,
2691
+ message: `Expected string`,
2692
+ node: member.value
2693
+ });
2694
+ break;
2695
+ default:
2696
+ errors.push({
2697
+ ...entry,
2698
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
2699
+ node: member.name,
2700
+ src
2701
+ });
2702
+ break;
2703
+ }
2704
+ }
2705
+ if (!hasVersion) errors.push({
2706
+ ...entry,
2707
+ message: `Missing "version".`,
2708
+ node,
2709
+ src
2710
+ });
2711
+ if (!hasResolutionOrder) errors.push({
2712
+ ...entry,
2713
+ message: `Missing "resolutionOrder".`,
2714
+ node,
2715
+ src
2716
+ });
2717
+ if (errors.length) logger.error(...errors);
2718
+ }
2719
+ function validateSet(node, isInline = false, { src }) {
2720
+ const entry = {
2721
+ group: "parser",
2722
+ label: "resolver",
2723
+ src
2724
+ };
2725
+ const errors = [];
2726
+ let hasName = !isInline;
2727
+ let hasType = !isInline;
2728
+ let hasSources = false;
2729
+ for (const member of node.members) {
2730
+ if (member.name.type !== "String") continue;
2731
+ switch (member.name.value) {
2732
+ case "name":
2733
+ hasName = true;
2734
+ if (member.value.type !== "String") errors.push({
2735
+ ...entry,
2736
+ message: MESSAGE_EXPECTED.STRING,
2737
+ node: member.value
2738
+ });
2739
+ break;
2740
+ case "description":
2741
+ if (member.value.type !== "String") errors.push({
2742
+ ...entry,
2743
+ message: MESSAGE_EXPECTED.STRING,
2744
+ node: member.value
2745
+ });
2746
+ break;
2747
+ case "type":
2748
+ hasType = true;
2749
+ if (member.value.type !== "String") errors.push({
2750
+ ...entry,
2751
+ message: MESSAGE_EXPECTED.STRING,
2752
+ node: member.value
2753
+ });
2754
+ else if (member.value.value !== "set") errors.push({
2755
+ ...entry,
2756
+ message: "\"type\" must be \"set\"."
2757
+ });
2758
+ break;
2759
+ case "sources":
2760
+ hasSources = true;
2761
+ if (member.value.type !== "Array") errors.push({
2762
+ ...entry,
2763
+ message: MESSAGE_EXPECTED.ARRAY,
2764
+ node: member.value
2765
+ });
2766
+ else if (member.value.elements.length === 0) errors.push({
2767
+ ...entry,
2768
+ message: `"sources" can’t be empty array.`,
2769
+ node: member.value
2770
+ });
2771
+ break;
2772
+ case "$defs":
2773
+ case "$extensions":
2774
+ if (member.value.type !== "Object") errors.push({
2775
+ ...entry,
2776
+ message: `Expected object`,
2777
+ node: member.value
2778
+ });
2779
+ break;
2780
+ case "$ref":
2781
+ if (member.value.type !== "String") errors.push({
2782
+ ...entry,
2783
+ message: `Expected string`,
2784
+ node: member.value
2785
+ });
2786
+ break;
2787
+ default:
2788
+ errors.push({
2789
+ ...entry,
2790
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
2791
+ node: member.name
2792
+ });
2793
+ break;
2794
+ }
2795
+ }
2796
+ if (!hasName) errors.push({
2797
+ ...entry,
2798
+ message: `Missing "name".`,
2799
+ node
2800
+ });
2801
+ if (!hasType) errors.push({
2802
+ ...entry,
2803
+ message: `"type": "set" missing.`,
2804
+ node
2805
+ });
2806
+ if (!hasSources) errors.push({
2807
+ ...entry,
2808
+ message: `Missing "sources".`,
2809
+ node
2810
+ });
2811
+ return errors;
2812
+ }
2813
+ function validateModifier(node, isInline = false, { src }) {
2814
+ const errors = [];
2815
+ const entry = {
2816
+ group: "parser",
2817
+ label: "resolver",
2818
+ src
2819
+ };
2820
+ let hasName = !isInline;
2821
+ let hasType = !isInline;
2822
+ let hasContexts = false;
2823
+ for (const member of node.members) {
2824
+ if (member.name.type !== "String") continue;
2825
+ switch (member.name.value) {
2826
+ case "name":
2827
+ hasName = true;
2828
+ if (member.value.type !== "String") errors.push({
2829
+ ...entry,
2830
+ message: MESSAGE_EXPECTED.STRING,
2831
+ node: member.value
2832
+ });
2833
+ break;
2834
+ case "description":
2835
+ if (member.value.type !== "String") errors.push({
2836
+ ...entry,
2837
+ message: MESSAGE_EXPECTED.STRING,
2838
+ node: member.value
2839
+ });
2840
+ break;
2841
+ case "type":
2842
+ hasType = true;
2843
+ if (member.value.type !== "String") errors.push({
2844
+ ...entry,
2845
+ message: MESSAGE_EXPECTED.STRING,
2846
+ node: member.value
2847
+ });
2848
+ else if (member.value.value !== "modifier") errors.push({
2849
+ ...entry,
2850
+ message: "\"type\" must be \"modifier\"."
2851
+ });
2852
+ break;
2853
+ case "contexts":
2854
+ hasContexts = true;
2855
+ if (member.value.type !== "Object") errors.push({
2856
+ ...entry,
2857
+ message: MESSAGE_EXPECTED.OBJECT,
2858
+ node: member.value
2859
+ });
2860
+ else if (member.value.members.length === 0) errors.push({
2861
+ ...entry,
2862
+ message: `"contexts" can’t be empty object.`,
2863
+ node: member.value
2864
+ });
2865
+ else for (const context of member.value.members) if (context.value.type !== "Array") errors.push({
2866
+ ...entry,
2867
+ message: MESSAGE_EXPECTED.ARRAY,
2868
+ node: context.value
2869
+ });
2870
+ break;
2871
+ case "$defs":
2872
+ case "$extensions":
2873
+ if (member.value.type !== "Object") errors.push({
2874
+ ...entry,
2875
+ message: `Expected object`,
2876
+ node: member.value
2877
+ });
2878
+ break;
2879
+ case "$ref":
2880
+ if (member.value.type !== "String") errors.push({
2881
+ ...entry,
2882
+ message: `Expected string`,
2883
+ node: member.value
2884
+ });
2885
+ break;
2886
+ default:
2887
+ errors.push({
2888
+ ...entry,
2889
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
2890
+ node: member.name
2891
+ });
2892
+ break;
2893
+ }
2894
+ }
2895
+ if (!hasName) errors.push({
2896
+ ...entry,
2897
+ message: `Missing "name".`,
2898
+ node
2899
+ });
2900
+ if (!hasType) errors.push({
2901
+ ...entry,
2902
+ message: `"type": "modifier" missing.`,
2903
+ node
2904
+ });
2905
+ if (!hasContexts) errors.push({
2906
+ ...entry,
2907
+ message: `Missing "contexts".`,
2908
+ node
2909
+ });
2910
+ return errors;
2911
+ }
2912
+
2913
+ //#endregion
2914
+ //#region src/resolver/normalize.ts
2915
+ /** Normalize resolver (assuming it’s been validated) */
2916
+ async function normalizeResolver(node, { filename, req, src, logger, yamlToMomoa }) {
2917
+ const resolverSource = evaluate(node);
2918
+ const resolutionOrder = getObjMember(node.body, "resolutionOrder");
2919
+ return {
2920
+ name: resolverSource.name,
2921
+ version: resolverSource.version,
2922
+ description: resolverSource.description,
2923
+ sets: resolverSource.sets,
2924
+ modifiers: resolverSource.modifiers,
2925
+ resolutionOrder: await Promise.all(resolutionOrder.elements.map(async (element, i) => {
2926
+ const layer = element.value;
2927
+ const members = getObjMembers(layer);
2928
+ let item = layer;
2929
+ if (members.$ref) {
2930
+ const entry = {
2931
+ group: "parser",
2932
+ label: "init",
2933
+ node: members.$ref,
2934
+ src
2935
+ };
2936
+ const { url, subpath } = parseRef(members.$ref.value);
2937
+ if (url === ".") if (!subpath?.[0]) logger.error({
2938
+ ...entry,
2939
+ message: "$ref can’t refer to the root document."
2940
+ });
2941
+ else if (subpath[0] !== "sets" && subpath[0] !== "modifiers") logger.error({
2942
+ ...entry,
2943
+ message: "Local $ref in resolutionOrder must point to either #/sets/[set] or #/modifiers/[modifiers]."
2944
+ });
2945
+ else {
2946
+ const resolvedItem = resolverSource[subpath[0]]?.[subpath[1]];
2947
+ if (!resolvedItem) logger.error({
2948
+ ...entry,
2949
+ message: "Invalid $ref"
2950
+ });
2951
+ else item = {
2952
+ type: subpath[0] === "sets" ? "set" : "modifier",
2953
+ ...resolvedItem
2954
+ };
2955
+ }
2956
+ else {
2957
+ const result = await bundle([{
2958
+ filename: new URL(url, filename),
2959
+ src: resolverSource.resolutionOrder[i]
2960
+ }], {
2961
+ req,
2962
+ yamlToMomoa
2963
+ });
2964
+ if (result.document.body.type === "Object") {
2965
+ const type = getObjMember(result.document.body, "type");
2966
+ if (type?.type === "String" && type.value === "set") {
2967
+ validateSet(result.document.body, true, src);
2968
+ item = evaluate(result.document.body);
2969
+ } else if (type?.type === "String" && type.value === "modifier") {
2970
+ validateModifier(result.document.body, true, src);
2971
+ item = evaluate(result.document.body);
2972
+ }
2973
+ }
2974
+ logger.error({
2975
+ ...entry,
2976
+ message: "$ref did not resolve to a valid Set or Modifier."
2977
+ });
2978
+ }
2979
+ }
2980
+ return evaluate((await bundle([{
2981
+ filename,
2982
+ src: item
2983
+ }], {
2984
+ req,
2985
+ yamlToMomoa
2986
+ })).document.body);
2987
+ }))
2988
+ };
2989
+ }
2990
+
2991
+ //#endregion
2992
+ //#region src/resolver/load.ts
2993
+ /** Quick-parse input sources and find a resolver */
2994
+ async function loadResolver(inputs, { logger, req, yamlToMomoa }) {
2995
+ let resolverDoc;
2996
+ const entry = {
2997
+ group: "parser",
2998
+ label: "init"
2999
+ };
3000
+ for (const input of inputs) {
3001
+ let document;
3002
+ if (typeof input.src === "string") if (maybeRawJSON(input.src)) document = toMomoa(input.src);
3003
+ else if (yamlToMomoa) document = yamlToMomoa(input.src);
3004
+ else logger.error({
3005
+ ...entry,
3006
+ message: `Install yaml-to-momoa package to parse YAML, and pass in as option, e.g.:
3007
+
3008
+ import { bundle } from '@terrazzo/json-schema-tools';
3009
+ import yamlToMomoa from 'yaml-to-momoa';
3010
+
3011
+ bundle(yamlString, { yamlToMomoa });`
3012
+ });
3013
+ else if (input.src && typeof input.src === "object") document = toMomoa(JSON.stringify(input.src, void 0, 2));
3014
+ else logger.error({
3015
+ ...entry,
3016
+ message: `Could not parse ${input.filename}. Is this valid JSON or YAML?`
3017
+ });
3018
+ if (!document || !isLikelyResolver(document)) continue;
3019
+ if (inputs.length > 1) logger.error({
3020
+ ...entry,
3021
+ message: `Resolver must be the only input, found ${inputs.length} sources.`
3022
+ });
3023
+ resolverDoc = document;
3024
+ break;
3025
+ }
3026
+ if (resolverDoc) {
3027
+ validateResolver(resolverDoc, {
3028
+ logger,
3029
+ src: inputs[0].src
3030
+ });
3031
+ return createResolver(await normalizeResolver(resolverDoc, {
3032
+ filename: inputs[0].filename,
3033
+ logger,
3034
+ req,
3035
+ src: inputs[0].src,
3036
+ yamlToMomoa
3037
+ }), { logger });
3038
+ }
3039
+ }
3040
+ /** Create an interface to resolve permutations */
3041
+ function createResolver(resolverSource, { logger }) {
3042
+ const inputDefaults = {};
3043
+ const modifierPermutations = [];
3044
+ for (const [name, m] of Object.entries(resolverSource.modifiers ?? {})) {
3045
+ if (typeof m.default === "string") inputDefaults[name] = m.default;
3046
+ modifierPermutations.push([name, Object.keys(m.contexts)]);
3047
+ }
3048
+ for (const m of resolverSource.resolutionOrder) {
3049
+ if (!("type" in m) || m.type !== "modifier") continue;
3050
+ if (typeof m.default === "string") inputDefaults[m.name] = m.default;
3051
+ modifierPermutations.push([m.name, Object.keys(m.contexts)]);
3052
+ }
3053
+ const permutations = calculatePermutations(modifierPermutations);
3054
+ return {
3055
+ apply(inputRaw) {
3056
+ let tokens = {};
3057
+ const input = {
3058
+ ...inputDefaults,
3059
+ ...inputRaw
3060
+ };
3061
+ for (const item of resolverSource.resolutionOrder) switch (item.type) {
3062
+ case "set":
3063
+ for (const s of item.sources) tokens = merge(tokens, s);
3064
+ break;
3065
+ case "modifier": {
3066
+ const context = input[item.name];
3067
+ const sources = item.contexts[context];
3068
+ if (!sources) logger.error({
3069
+ group: "parser",
3070
+ label: "resolver",
3071
+ message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`
3072
+ });
3073
+ for (const s of sources ?? []) tokens = merge(tokens, s);
3074
+ break;
3075
+ }
3076
+ }
3077
+ return tokens;
3078
+ },
3079
+ source: resolverSource,
3080
+ permutations,
3081
+ isValidInput(inputRaw) {
3082
+ if (!inputRaw || typeof inputRaw !== "object") logger.error({
3083
+ group: "parser",
3084
+ label: "resolver",
3085
+ message: `Invalid input: ${JSON.stringify(inputRaw)}.`
3086
+ });
3087
+ const input = {
3088
+ ...inputDefaults,
3089
+ ...inputRaw
3090
+ };
3091
+ return permutations.findIndex((p) => (0, import_fast_deep_equal.default)(input, p)) !== -1;
3092
+ }
3093
+ };
3094
+ }
3095
+ /** Calculate all permutations */
3096
+ function calculatePermutations(options) {
3097
+ const permutationCount = [1];
3098
+ for (const [_name, contexts] of options) permutationCount.push(contexts.length * (permutationCount.at(-1) || 1));
3099
+ const permutations = [];
3100
+ for (let i = 0; i < permutationCount.at(-1); i++) {
3101
+ const input = {};
3102
+ for (let j = 0; j < options.length; j++) {
3103
+ const [name, contexts] = options[j];
3104
+ input[name] = contexts[Math.floor(i / permutationCount[j]) % contexts.length];
3105
+ }
3106
+ permutations.push(input);
3107
+ }
3108
+ return permutations.length ? permutations : [{}];
3109
+ }
3110
+
2483
3111
  //#endregion
2484
3112
  //#region src/parse/normalize.ts
2485
3113
  /**
@@ -2603,8 +3231,7 @@ function tokenFromNode(node, { groups, path, source, ignore }) {
2603
3231
  const jsonID = `#/${path.join("/")}`;
2604
3232
  const id = path.join(".");
2605
3233
  const originalToken = momoa.evaluate(node);
2606
- const groupID = `#/${path.slice(0, -1).join("/")}`;
2607
- const group = groups[groupID];
3234
+ const group = groups[`#/${path.slice(0, -1).join("/")}`];
2608
3235
  if (group?.tokens && !group.tokens.includes(id)) group.tokens.push(id);
2609
3236
  const nodeSource = {
2610
3237
  filename: source.filename?.href,
@@ -2873,6 +3500,13 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
2873
3500
  for (const mode of Object.keys(token.mode)) {
2874
3501
  function resolveInner(alias, refChain) {
2875
3502
  const nextRef = aliasToRef(alias, mode)?.$ref;
3503
+ if (!nextRef) {
3504
+ logger.error({
3505
+ ...aliasEntry,
3506
+ message: `Internal error resolving ${JSON.stringify(refChain)}`
3507
+ });
3508
+ throw new Error("Internal error");
3509
+ }
2876
3510
  if (refChain.includes(nextRef)) logger.error({
2877
3511
  ...aliasEntry,
2878
3512
  message: "Circular alias detected."
@@ -2947,7 +3581,7 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
2947
3581
  //#endregion
2948
3582
  //#region src/parse/load.ts
2949
3583
  /** Load from multiple entries, while resolving remote files */
2950
- async function loadSources(inputs, { config, logger, continueOnError, yamlToMomoa, transform }) {
3584
+ async function loadSources(inputs, { config, logger, req, continueOnError, yamlToMomoa, transform }) {
2951
3585
  const entry = {
2952
3586
  group: "parser",
2953
3587
  label: "init"
@@ -2966,6 +3600,7 @@ async function loadSources(inputs, { config, logger, continueOnError, yamlToMomo
2966
3600
  let refMap = {};
2967
3601
  try {
2968
3602
  const result = await bundle(sources, {
3603
+ req,
2969
3604
  parse: transform ? transformer(transform) : void 0,
2970
3605
  yamlToMomoa
2971
3606
  });
@@ -3138,11 +3773,17 @@ function transformer(transform) {
3138
3773
  //#endregion
3139
3774
  //#region src/parse/index.ts
3140
3775
  /** Parse */
3141
- async function parse(_input, { logger = new Logger(), skipLint = false, config = {}, continueOnError = false, yamlToMomoa, transform } = {}) {
3776
+ async function parse(_input, { logger = new Logger(), req = defaultReq, skipLint = false, config = {}, continueOnError = false, yamlToMomoa, transform } = {}) {
3142
3777
  const inputs = Array.isArray(_input) ? _input : [_input];
3143
3778
  const totalStart = performance.now();
3779
+ const resolver = await loadResolver(inputs, {
3780
+ logger,
3781
+ req,
3782
+ yamlToMomoa
3783
+ });
3144
3784
  const initStart = performance.now();
3145
3785
  const { tokens, sources } = await loadSources(inputs, {
3786
+ req,
3146
3787
  logger,
3147
3788
  config,
3148
3789
  continueOnError,
@@ -3185,10 +3826,22 @@ async function parse(_input, { logger = new Logger(), skipLint = false, config =
3185
3826
  }
3186
3827
  return {
3187
3828
  tokens,
3188
- sources
3829
+ sources,
3830
+ resolver
3189
3831
  };
3190
3832
  }
3833
+ let fs;
3834
+ /** Fallback req */
3835
+ async function defaultReq(src, _origin) {
3836
+ if (src.protocol === "file:") {
3837
+ if (!fs) fs = await import("node:fs/promises");
3838
+ return await fs.readFile(src, "utf8");
3839
+ }
3840
+ const res = await fetch(src);
3841
+ if (!res.ok) throw new Error(`${src} responded with ${res.status}\n${await res.text()}`);
3842
+ return await res.text();
3843
+ }
3191
3844
 
3192
3845
  //#endregion
3193
- export { LOG_ORDER, Logger, MULTI_VALUE, RECOMMENDED_CONFIG, SINGLE_VALUE, TokensJSONError, build, defineConfig, formatMessage, lintRunner, mergeConfigs, parse };
3846
+ export { LOG_ORDER, Logger, MULTI_VALUE, RECOMMENDED_CONFIG, SINGLE_VALUE, TokensJSONError, build, calculatePermutations, createResolver, defineConfig, formatMessage, isLikelyResolver, lintRunner, loadResolver, mergeConfigs, normalizeResolver, parse, validateModifier, validateResolver, validateSet };
3194
3847
  //# sourceMappingURL=index.js.map