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

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
@@ -4,7 +4,7 @@ import pc from "picocolors";
4
4
  import { merge } from "merge-anything";
5
5
  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
6
  import { clampChroma, wcagContrast } from "culori";
7
- import { bundle, getObjMember, getObjMembers, isPure$ref, parseRef, replaceNode, traverseAsync } from "@terrazzo/json-schema-tools";
7
+ import { bundle, getObjMember, getObjMembers, isPure$ref, maybeRawJSON, parseRef, replaceNode, traverse } from "@terrazzo/json-schema-tools";
8
8
 
9
9
  //#region src/lib/code-frame.ts
10
10
  /**
@@ -34,10 +34,8 @@ function getMarkerLines(loc, source, opts = {}) {
34
34
  if (lineDiff) for (let i = 0; i <= lineDiff; i++) {
35
35
  const lineNumber = i + startLine;
36
36
  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];
37
+ else if (i === 0) markerLines[lineNumber] = [startColumn, source[lineNumber - 1].length - startColumn + 1];
38
+ else if (i === lineDiff) markerLines[lineNumber] = [0, endColumn];
41
39
  else markerLines[lineNumber] = [0, source[lineNumber - i].length];
42
40
  }
43
41
  else if (startColumn === endColumn) if (startColumn) markerLines[startLine] = [startColumn, 0];
@@ -55,8 +53,7 @@ function getMarkerLines(loc, source, opts = {}) {
55
53
  const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
56
54
  function codeFrameColumns(rawLines, loc, opts = {}) {
57
55
  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);
56
+ const { start, end, markerLines } = getMarkerLines(loc, rawLines.split(NEWLINE), opts);
60
57
  const hasColumns = loc.start && typeof loc.start.column === "number";
61
58
  const numberMaxWidth = String(end).length;
62
59
  let frame = rawLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
@@ -354,7 +351,7 @@ async function build(tokens, { sources, logger = new Logger(), config }) {
354
351
  //#endregion
355
352
  //#region src/lint/plugin-core/lib/docs.ts
356
353
  function docsLink(ruleName) {
357
- return `https://terrazzo.app/docs/cli/lint#${ruleName.replaceAll("/", "")}`;
354
+ return `https://terrazzo.app/docs/linting#${ruleName.replaceAll("/", "")}`;
358
355
  }
359
356
 
360
357
  //#endregion
@@ -390,9 +387,7 @@ const rule$26 = {
390
387
  if (tokens[foreground].$type !== "color") throw new Error(`Token ${foreground} isn’t a color`);
391
388
  if (!tokens[background]) throw new Error(`Token ${background} does not exist`);
392
389
  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);
390
+ const contrast = wcagContrast(tokenToCulori(tokens[foreground].$value), tokenToCulori(tokens[background].$value));
396
391
  const min = WCAG2_MIN_CONTRAST[options.level ?? "AA"][largeText ? "large" : "default"];
397
392
  if (contrast < min) report({
398
393
  messageId: ERROR_INSUFFICIENT_CONTRAST,
@@ -719,8 +714,7 @@ function isWithinGamut(color, gamut) {
719
714
  "hsl",
720
715
  "hwb"
721
716
  ].includes(parsed.mode)) return true;
722
- const clamped = clampChroma(parsed, parsed.mode, gamut === "srgb" ? "rgb" : gamut);
723
- return isWithinThreshold(parsed, clamped);
717
+ return isWithinThreshold(parsed, clampChroma(parsed, parsed.mode, gamut === "srgb" ? "rgb" : gamut));
724
718
  }
725
719
  /** is Color A close enough to Color B? */
726
720
  function isWithinThreshold(a, b, tolerance = TOLERANCE) {
@@ -807,36 +801,13 @@ const rule$20 = {
807
801
  };
808
802
  var max_gamut_default = rule$20;
809
803
 
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
804
  //#endregion
834
805
  //#region src/lint/plugin-core/rules/required-children.ts
835
806
  const REQUIRED_CHILDREN = "core/required-children";
836
807
  const ERROR_EMPTY_MATCH = "EMPTY_MATCH";
837
808
  const ERROR_MISSING_REQUIRED_TOKENS = "MISSING_REQUIRED_TOKENS";
838
809
  const ERROR_MISSING_REQUIRED_GROUP = "MISSING_REQUIRED_GROUP";
839
- const rule$18 = {
810
+ const rule$19 = {
840
811
  meta: {
841
812
  messages: {
842
813
  [ERROR_EMPTY_MATCH]: "No tokens matched {{ matcher }}",
@@ -894,12 +865,12 @@ const rule$18 = {
894
865
  }
895
866
  }
896
867
  };
897
- var required_children_default = rule$18;
868
+ var required_children_default = rule$19;
898
869
 
899
870
  //#endregion
900
871
  //#region src/lint/plugin-core/rules/required-modes.ts
901
872
  const REQUIRED_MODES = "core/required-modes";
902
- const rule$17 = {
873
+ const rule$18 = {
903
874
  meta: { docs: {
904
875
  description: "Enforce certain tokens have specific modes.",
905
876
  url: docsLink(REQUIRED_MODES)
@@ -930,7 +901,30 @@ const rule$17 = {
930
901
  }
931
902
  }
932
903
  };
933
- var required_modes_default = rule$17;
904
+ var required_modes_default = rule$18;
905
+
906
+ //#endregion
907
+ //#region src/lint/plugin-core/rules/required-type.ts
908
+ const REQUIRED_TYPE = "core/required-type";
909
+ const ERROR$10 = "ERROR";
910
+ const rule$17 = {
911
+ meta: {
912
+ messages: { [ERROR$10]: "Token missing $type." },
913
+ docs: {
914
+ description: "Requiring every token to have $type, even aliases, simplifies computation.",
915
+ url: docsLink(REQUIRED_TYPE)
916
+ }
917
+ },
918
+ defaultOptions: {},
919
+ create({ tokens, report }) {
920
+ for (const t of Object.values(tokens)) if (!t.originalValue?.$type) report({
921
+ messageId: ERROR$10,
922
+ node: t.source.node,
923
+ filename: t.source.filename
924
+ });
925
+ }
926
+ };
927
+ var required_type_default = rule$17;
934
928
 
935
929
  //#endregion
936
930
  //#region src/lint/plugin-core/rules/required-typography-properties.ts
@@ -1340,8 +1334,7 @@ const rule$11 = {
1340
1334
  break;
1341
1335
  case "strokeStyle":
1342
1336
  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");
1337
+ const dashArray = getObjMember(getObjMember(t.source.node, "$value"), "dashArray");
1345
1338
  for (let i = 0; i < t.originalValue.$value.dashArray.length; i++) {
1346
1339
  if (isAlias(t.originalValue.$value.dashArray[i])) continue;
1347
1340
  validateDimension(t.originalValue.$value.dashArray[i], {
@@ -1359,8 +1352,7 @@ const rule$11 = {
1359
1352
  filename: t.source.filename
1360
1353
  });
1361
1354
  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");
1355
+ const dashArray = getObjMember(getObjMember($valueNode, "style"), "dashArray");
1364
1356
  for (let i = 0; i < t.originalValue.$value.style.dashArray.length; i++) {
1365
1357
  if (isAlias(t.originalValue.$value.style.dashArray[i])) continue;
1366
1358
  validateDimension(t.originalValue.$value.style.dashArray[i], {
@@ -1580,8 +1572,7 @@ const rule$9 = {
1580
1572
  case "typography":
1581
1573
  if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontFamily) {
1582
1574
  if (t.partialAliasOf?.fontFamily) continue;
1583
- const $value = getObjMember(t.source.node, "$value");
1584
- const properties = getObjMembers($value);
1575
+ const properties = getObjMembers(getObjMember(t.source.node, "$value"));
1585
1576
  validateFontFamily(t.originalValue.$value.fontFamily, {
1586
1577
  node: properties.fontFamily,
1587
1578
  filename: t.source.filename
@@ -1643,8 +1634,7 @@ const rule$8 = {
1643
1634
  case "typography":
1644
1635
  if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontWeight) {
1645
1636
  if (t.partialAliasOf?.fontWeight) continue;
1646
- const $value = getObjMember(t.source.node, "$value");
1647
- const properties = getObjMembers($value);
1637
+ const properties = getObjMembers(getObjMember(t.source.node, "$value"));
1648
1638
  validateFontWeight(t.originalValue.$value.fontWeight, {
1649
1639
  node: properties.fontWeight,
1650
1640
  filename: t.source.filename
@@ -2113,9 +2103,9 @@ const ALL_RULES = {
2113
2103
  [DESCRIPTIONS]: descriptions_default,
2114
2104
  [DUPLICATE_VALUES]: duplicate_values_default,
2115
2105
  [MAX_GAMUT]: max_gamut_default,
2116
- [NO_TYPE_ON_ALIAS]: no_type_on_alias_default,
2117
2106
  [REQUIRED_CHILDREN]: required_children_default,
2118
2107
  [REQUIRED_MODES]: required_modes_default,
2108
+ [REQUIRED_TYPE]: required_type_default,
2119
2109
  [REQUIRED_TYPOGRAPHY_PROPERTIES]: required_typography_properties_default,
2120
2110
  [A11Y_MIN_CONTRAST]: a11y_min_contrast_default,
2121
2111
  [A11Y_MIN_FONT_SIZE]: a11y_min_font_size_default
@@ -2145,8 +2135,7 @@ const RECOMMENDED_CONFIG = {
2145
2135
  [VALID_SHADOW]: ["error", {}],
2146
2136
  [VALID_GRADIENT]: ["error", {}],
2147
2137
  [VALID_TYPOGRAPHY]: ["error", {}],
2148
- [CONSISTENT_NAMING]: ["warn", { format: "kebab-case" }],
2149
- [NO_TYPE_ON_ALIAS]: ["warn", {}]
2138
+ [CONSISTENT_NAMING]: ["warn", { format: "kebab-case" }]
2150
2139
  };
2151
2140
 
2152
2141
  //#endregion
@@ -2321,7 +2310,7 @@ function normalizeLint({ config, logger }) {
2321
2310
  });
2322
2311
  const value = config.lint.rules[id];
2323
2312
  let severity = "off";
2324
- let options;
2313
+ let options = {};
2325
2314
  if (typeof value === "number" || typeof value === "string") severity = value;
2326
2315
  else if (Array.isArray(value)) {
2327
2316
  severity = value[0];
@@ -2480,6 +2469,410 @@ function toMomoa(srcRaw) {
2480
2469
  });
2481
2470
  }
2482
2471
 
2472
+ //#endregion
2473
+ //#region src/lib/resolver-utils.ts
2474
+ /**
2475
+ * If tokens are found inside a resolver, strip out the resolver paths (don’t
2476
+ * include "sets"/"modifiers" in the token ID etc.)
2477
+ */
2478
+ function filterResolverPaths(path) {
2479
+ switch (path[0]) {
2480
+ case "sets": return path.slice(4);
2481
+ case "modifiers": return path.slice(5);
2482
+ case "resolutionOrder":
2483
+ switch (path[2]) {
2484
+ case "sources": return path.slice(4);
2485
+ case "contexts": return path.slice(5);
2486
+ }
2487
+ break;
2488
+ }
2489
+ return path;
2490
+ }
2491
+ /**
2492
+ * Make a deterministic string from an object
2493
+ */
2494
+ function makeInputKey(input) {
2495
+ return JSON.stringify(Object.fromEntries(Object.entries(input).sort((a, b) => a[0].localeCompare(b[0], "en-us", { numeric: true }))));
2496
+ }
2497
+
2498
+ //#endregion
2499
+ //#region src/resolver/validate.ts
2500
+ /**
2501
+ * Determine whether this is likely a resolver
2502
+ * We use terms the word “likely” because this occurs before validation. Since
2503
+ * we may be dealing with a doc _intended_ to be a resolver, but may be lacking
2504
+ * some critical information, how can we determine intent? There’s a bit of
2505
+ * guesswork here, but we try and find a reasonable edge case where we sniff out
2506
+ * invalid DTCG syntax that a resolver doc would have.
2507
+ */
2508
+ function isLikelyResolver(doc) {
2509
+ if (doc.body.type !== "Object") return false;
2510
+ for (const member of doc.body.members) {
2511
+ if (member.name.type !== "String") continue;
2512
+ switch (member.name.value) {
2513
+ case "name":
2514
+ case "description":
2515
+ case "version":
2516
+ if (member.name.type === "String") return true;
2517
+ break;
2518
+ case "sets":
2519
+ case "modifiers":
2520
+ if (member.value.type !== "Object") continue;
2521
+ if (getObjMember(member.value, "description")?.type === "String") return true;
2522
+ if (member.name.value === "sets" && getObjMember(member.value, "sources")?.type === "Array") return true;
2523
+ else if (member.name.value === "modifiers") {
2524
+ const contexts = getObjMember(member.value, "contexts");
2525
+ if (contexts?.type === "Object" && contexts.members.some((m) => m.value.type === "Array")) return true;
2526
+ }
2527
+ break;
2528
+ case "resolutionOrder":
2529
+ if (member.value.type === "Array") return true;
2530
+ break;
2531
+ }
2532
+ }
2533
+ return false;
2534
+ }
2535
+ const MESSAGE_EXPECTED = {
2536
+ STRING: "Expected string.",
2537
+ OBJECT: "Expected object.",
2538
+ ARRAY: "Expected array."
2539
+ };
2540
+ /**
2541
+ * Validate a resolver document.
2542
+ * There’s a ton of boilerplate here, only to surface detailed code frames. Is there a better abstraction?
2543
+ */
2544
+ function validateResolver(node, { logger, src }) {
2545
+ const entry = {
2546
+ group: "parser",
2547
+ label: "resolver",
2548
+ src
2549
+ };
2550
+ if (node.body.type !== "Object") logger.error({
2551
+ ...entry,
2552
+ message: MESSAGE_EXPECTED.OBJECT,
2553
+ node
2554
+ });
2555
+ const errors = [];
2556
+ let hasVersion = false;
2557
+ let hasResolutionOrder = false;
2558
+ for (const member of node.body.members) {
2559
+ if (member.name.type !== "String") continue;
2560
+ switch (member.name.value) {
2561
+ case "name":
2562
+ case "description":
2563
+ if (member.value.type !== "String") errors.push({
2564
+ ...entry,
2565
+ message: MESSAGE_EXPECTED.STRING
2566
+ });
2567
+ break;
2568
+ case "version":
2569
+ hasVersion = true;
2570
+ if (member.value.type !== "String" || member.value.value !== "2025.10") errors.push({
2571
+ ...entry,
2572
+ message: `Expected "version" to be "2025.10".`,
2573
+ node: member.value
2574
+ });
2575
+ break;
2576
+ case "sets":
2577
+ case "modifiers":
2578
+ if (member.value.type !== "Object") errors.push({
2579
+ ...entry,
2580
+ message: MESSAGE_EXPECTED.OBJECT,
2581
+ node: member.value
2582
+ });
2583
+ else for (const item of member.value.members) if (item.value.type !== "Object") errors.push({
2584
+ ...entry,
2585
+ message: MESSAGE_EXPECTED.OBJECT,
2586
+ node: item.value
2587
+ });
2588
+ else {
2589
+ const validator = member.name.value === "sets" ? validateSet : validateModifier;
2590
+ errors.push(...validator(item.value, false, {
2591
+ logger,
2592
+ src
2593
+ }));
2594
+ }
2595
+ break;
2596
+ case "resolutionOrder":
2597
+ hasResolutionOrder = true;
2598
+ if (member.value.type !== "Array") errors.push({
2599
+ ...entry,
2600
+ message: MESSAGE_EXPECTED.ARRAY,
2601
+ node: member.value
2602
+ });
2603
+ else if (member.value.elements.length === 0) errors.push({
2604
+ ...entry,
2605
+ message: `"resolutionOrder" can’t be empty array.`,
2606
+ node: member.value
2607
+ });
2608
+ else for (const item of member.value.elements) if (item.value.type !== "Object") errors.push({
2609
+ ...entry,
2610
+ message: MESSAGE_EXPECTED.OBJECT,
2611
+ node: item.value
2612
+ });
2613
+ else {
2614
+ const itemMembers = getObjMembers(item.value);
2615
+ if (itemMembers.$ref?.type === "String") continue;
2616
+ if (itemMembers.type?.type === "String") if (itemMembers.type.value === "set") validateSet(item.value, true, {
2617
+ logger,
2618
+ src
2619
+ });
2620
+ else if (itemMembers.type.value === "modifier") validateModifier(item.value, true, {
2621
+ logger,
2622
+ src
2623
+ });
2624
+ else errors.push({
2625
+ ...entry,
2626
+ message: `Unknown type ${JSON.stringify(itemMembers.type.value)}`,
2627
+ node: itemMembers.type
2628
+ });
2629
+ if (itemMembers.sources?.type === "Array") validateSet(item.value, true, {
2630
+ logger,
2631
+ src
2632
+ });
2633
+ else if (itemMembers.contexts?.type === "Object") validateModifier(item.value, true, {
2634
+ logger,
2635
+ src
2636
+ });
2637
+ else if (itemMembers.name?.type === "String" || itemMembers.description?.type === "String") validateSet(item.value, true, {
2638
+ logger,
2639
+ src
2640
+ });
2641
+ }
2642
+ break;
2643
+ case "$defs":
2644
+ case "$extensions":
2645
+ if (member.value.type !== "Object") errors.push({
2646
+ ...entry,
2647
+ message: `Expected object`,
2648
+ node: member.value
2649
+ });
2650
+ break;
2651
+ case "$ref":
2652
+ if (member.value.type !== "String") errors.push({
2653
+ ...entry,
2654
+ message: `Expected string`,
2655
+ node: member.value
2656
+ });
2657
+ break;
2658
+ default:
2659
+ errors.push({
2660
+ ...entry,
2661
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
2662
+ node: member.name,
2663
+ src
2664
+ });
2665
+ break;
2666
+ }
2667
+ }
2668
+ if (!hasVersion) errors.push({
2669
+ ...entry,
2670
+ message: `Missing "version".`,
2671
+ node,
2672
+ src
2673
+ });
2674
+ if (!hasResolutionOrder) errors.push({
2675
+ ...entry,
2676
+ message: `Missing "resolutionOrder".`,
2677
+ node,
2678
+ src
2679
+ });
2680
+ if (errors.length) logger.error(...errors);
2681
+ }
2682
+ function validateSet(node, isInline = false, { src }) {
2683
+ const entry = {
2684
+ group: "parser",
2685
+ label: "resolver",
2686
+ src
2687
+ };
2688
+ const errors = [];
2689
+ let hasName = !isInline;
2690
+ let hasType = !isInline;
2691
+ let hasSources = false;
2692
+ for (const member of node.members) {
2693
+ if (member.name.type !== "String") continue;
2694
+ switch (member.name.value) {
2695
+ case "name":
2696
+ hasName = true;
2697
+ if (member.value.type !== "String") errors.push({
2698
+ ...entry,
2699
+ message: MESSAGE_EXPECTED.STRING,
2700
+ node: member.value
2701
+ });
2702
+ break;
2703
+ case "description":
2704
+ if (member.value.type !== "String") errors.push({
2705
+ ...entry,
2706
+ message: MESSAGE_EXPECTED.STRING,
2707
+ node: member.value
2708
+ });
2709
+ break;
2710
+ case "type":
2711
+ hasType = true;
2712
+ if (member.value.type !== "String") errors.push({
2713
+ ...entry,
2714
+ message: MESSAGE_EXPECTED.STRING,
2715
+ node: member.value
2716
+ });
2717
+ else if (member.value.value !== "set") errors.push({
2718
+ ...entry,
2719
+ message: "\"type\" must be \"set\"."
2720
+ });
2721
+ break;
2722
+ case "sources":
2723
+ hasSources = true;
2724
+ if (member.value.type !== "Array") errors.push({
2725
+ ...entry,
2726
+ message: MESSAGE_EXPECTED.ARRAY,
2727
+ node: member.value
2728
+ });
2729
+ else if (member.value.elements.length === 0) errors.push({
2730
+ ...entry,
2731
+ message: `"sources" can’t be empty array.`,
2732
+ node: member.value
2733
+ });
2734
+ break;
2735
+ case "$defs":
2736
+ case "$extensions":
2737
+ if (member.value.type !== "Object") errors.push({
2738
+ ...entry,
2739
+ message: `Expected object`,
2740
+ node: member.value
2741
+ });
2742
+ break;
2743
+ case "$ref":
2744
+ if (member.value.type !== "String") errors.push({
2745
+ ...entry,
2746
+ message: `Expected string`,
2747
+ node: member.value
2748
+ });
2749
+ break;
2750
+ default:
2751
+ errors.push({
2752
+ ...entry,
2753
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
2754
+ node: member.name
2755
+ });
2756
+ break;
2757
+ }
2758
+ }
2759
+ if (!hasName) errors.push({
2760
+ ...entry,
2761
+ message: `Missing "name".`,
2762
+ node
2763
+ });
2764
+ if (!hasType) errors.push({
2765
+ ...entry,
2766
+ message: `"type": "set" missing.`,
2767
+ node
2768
+ });
2769
+ if (!hasSources) errors.push({
2770
+ ...entry,
2771
+ message: `Missing "sources".`,
2772
+ node
2773
+ });
2774
+ return errors;
2775
+ }
2776
+ function validateModifier(node, isInline = false, { src }) {
2777
+ const errors = [];
2778
+ const entry = {
2779
+ group: "parser",
2780
+ label: "resolver",
2781
+ src
2782
+ };
2783
+ let hasName = !isInline;
2784
+ let hasType = !isInline;
2785
+ let hasContexts = false;
2786
+ for (const member of node.members) {
2787
+ if (member.name.type !== "String") continue;
2788
+ switch (member.name.value) {
2789
+ case "name":
2790
+ hasName = true;
2791
+ if (member.value.type !== "String") errors.push({
2792
+ ...entry,
2793
+ message: MESSAGE_EXPECTED.STRING,
2794
+ node: member.value
2795
+ });
2796
+ break;
2797
+ case "description":
2798
+ if (member.value.type !== "String") errors.push({
2799
+ ...entry,
2800
+ message: MESSAGE_EXPECTED.STRING,
2801
+ node: member.value
2802
+ });
2803
+ break;
2804
+ case "type":
2805
+ hasType = true;
2806
+ if (member.value.type !== "String") errors.push({
2807
+ ...entry,
2808
+ message: MESSAGE_EXPECTED.STRING,
2809
+ node: member.value
2810
+ });
2811
+ else if (member.value.value !== "modifier") errors.push({
2812
+ ...entry,
2813
+ message: "\"type\" must be \"modifier\"."
2814
+ });
2815
+ break;
2816
+ case "contexts":
2817
+ hasContexts = true;
2818
+ if (member.value.type !== "Object") errors.push({
2819
+ ...entry,
2820
+ message: MESSAGE_EXPECTED.OBJECT,
2821
+ node: member.value
2822
+ });
2823
+ else if (member.value.members.length === 0) errors.push({
2824
+ ...entry,
2825
+ message: `"contexts" can’t be empty object.`,
2826
+ node: member.value
2827
+ });
2828
+ else for (const context of member.value.members) if (context.value.type !== "Array") errors.push({
2829
+ ...entry,
2830
+ message: MESSAGE_EXPECTED.ARRAY,
2831
+ node: context.value
2832
+ });
2833
+ break;
2834
+ case "$defs":
2835
+ case "$extensions":
2836
+ if (member.value.type !== "Object") errors.push({
2837
+ ...entry,
2838
+ message: `Expected object`,
2839
+ node: member.value
2840
+ });
2841
+ break;
2842
+ case "$ref":
2843
+ if (member.value.type !== "String") errors.push({
2844
+ ...entry,
2845
+ message: `Expected string`,
2846
+ node: member.value
2847
+ });
2848
+ break;
2849
+ default:
2850
+ errors.push({
2851
+ ...entry,
2852
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
2853
+ node: member.name
2854
+ });
2855
+ break;
2856
+ }
2857
+ }
2858
+ if (!hasName) errors.push({
2859
+ ...entry,
2860
+ message: `Missing "name".`,
2861
+ node
2862
+ });
2863
+ if (!hasType) errors.push({
2864
+ ...entry,
2865
+ message: `"type": "modifier" missing.`,
2866
+ node
2867
+ });
2868
+ if (!hasContexts) errors.push({
2869
+ ...entry,
2870
+ message: `Missing "contexts".`,
2871
+ node
2872
+ });
2873
+ return errors;
2874
+ }
2875
+
2483
2876
  //#endregion
2484
2877
  //#region src/parse/normalize.ts
2485
2878
  /**
@@ -2603,11 +2996,10 @@ function tokenFromNode(node, { groups, path, source, ignore }) {
2603
2996
  const jsonID = `#/${path.join("/")}`;
2604
2997
  const id = path.join(".");
2605
2998
  const originalToken = momoa.evaluate(node);
2606
- const groupID = `#/${path.slice(0, -1).join("/")}`;
2607
- const group = groups[groupID];
2999
+ const group = groups[`#/${path.slice(0, -1).join("/")}`];
2608
3000
  if (group?.tokens && !group.tokens.includes(id)) group.tokens.push(id);
2609
3001
  const nodeSource = {
2610
- filename: source.filename?.href,
3002
+ filename: source.filename.href,
2611
3003
  node
2612
3004
  };
2613
3005
  const token = {
@@ -2873,6 +3265,13 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
2873
3265
  for (const mode of Object.keys(token.mode)) {
2874
3266
  function resolveInner(alias, refChain) {
2875
3267
  const nextRef = aliasToRef(alias, mode)?.$ref;
3268
+ if (!nextRef) {
3269
+ logger.error({
3270
+ ...aliasEntry,
3271
+ message: `Internal error resolving ${JSON.stringify(refChain)}`
3272
+ });
3273
+ throw new Error("Internal error");
3274
+ }
2876
3275
  if (refChain.includes(nextRef)) logger.error({
2877
3276
  ...aliasEntry,
2878
3277
  message: "Circular alias detected."
@@ -2945,67 +3344,20 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
2945
3344
  }
2946
3345
 
2947
3346
  //#endregion
2948
- //#region src/parse/load.ts
2949
- /** Load from multiple entries, while resolving remote files */
2950
- async function loadSources(inputs, { config, logger, continueOnError, yamlToMomoa, transform }) {
3347
+ //#region src/parse/process.ts
3348
+ function processTokens(rootSource, { config, logger, sourceByFilename, refMap }) {
2951
3349
  const entry = {
2952
3350
  group: "parser",
2953
3351
  label: "init"
2954
3352
  };
2955
- const firstLoad = performance.now();
2956
- let document = {};
2957
- /** The original user inputs, in original order, with parsed ASTs */
2958
- const sources = inputs.map((input, i) => ({
2959
- ...input,
2960
- document: {},
2961
- filename: input.filename || new URL(`virtual:${i}`)
2962
- }));
2963
- /** The sources array, indexed by filename */
2964
- let sourceByFilename = {};
2965
- /** Mapping of all final $ref resolutions. This will be used to generate the graph later. */
2966
- let refMap = {};
2967
- try {
2968
- const result = await bundle(sources, {
2969
- parse: transform ? transformer(transform) : void 0,
2970
- yamlToMomoa
2971
- });
2972
- document = result.document;
2973
- sourceByFilename = result.sources;
2974
- refMap = result.refMap;
2975
- for (const [filename, source] of Object.entries(result.sources)) {
2976
- const i = sources.findIndex((s) => s.filename.href === filename);
2977
- if (i === -1) sources.push(source);
2978
- else {
2979
- sources[i].src = source.src;
2980
- sources[i].document = source.document;
2981
- }
2982
- }
2983
- } catch (err) {
2984
- let src = sources.find((s) => s.filename.href === err.filename)?.src;
2985
- if (src && typeof src !== "string") src = JSON.stringify(src, void 0, 2);
2986
- logger.error({
2987
- ...entry,
2988
- continueOnError,
2989
- message: err.message,
2990
- node: err.node,
2991
- src
2992
- });
2993
- }
2994
- logger.debug({
2995
- ...entry,
2996
- message: `JSON loaded`,
2997
- timing: performance.now() - firstLoad
2998
- });
2999
- const artificialSource = {
3000
- src: momoa.print(document, { indent: 2 }),
3001
- document
3002
- };
3003
3353
  const firstPass = performance.now();
3004
3354
  const tokens = {};
3005
3355
  const tokenIDs = [];
3006
3356
  const groups = {};
3007
- await traverseAsync(document, { async enter(node, _parent, path) {
3357
+ const isResolver = isLikelyResolver(rootSource.document);
3358
+ traverse(rootSource.document, { enter(node, _parent, rawPath) {
3008
3359
  if (node.type !== "Object") return;
3360
+ const path = isResolver ? filterResolverPaths(rawPath) : rawPath;
3009
3361
  groupFromNode(node, {
3010
3362
  path,
3011
3363
  groups
@@ -3014,10 +3366,7 @@ async function loadSources(inputs, { config, logger, continueOnError, yamlToMomo
3014
3366
  groups,
3015
3367
  ignore: config.ignore,
3016
3368
  path,
3017
- source: {
3018
- src: artificialSource,
3019
- document
3020
- }
3369
+ source: rootSource
3021
3370
  });
3022
3371
  if (token) {
3023
3372
  tokenIDs.push(token.jsonID);
@@ -3030,7 +3379,7 @@ async function loadSources(inputs, { config, logger, continueOnError, yamlToMomo
3030
3379
  timing: performance.now() - firstPass
3031
3380
  });
3032
3381
  const secondPass = performance.now();
3033
- for (const source of Object.values(sourceByFilename)) await traverseAsync(source.document, { async enter(node, _parent, path) {
3382
+ for (const source of Object.values(sourceByFilename)) traverse(source.document, { enter(node, _parent, path) {
3034
3383
  if (node.type !== "Object") return;
3035
3384
  const tokenRawValues = tokenRawValuesFromNode(node, {
3036
3385
  filename: source.filename.href,
@@ -3087,8 +3436,377 @@ async function loadSources(inputs, { config, logger, continueOnError, yamlToMomo
3087
3436
  tokensSorted[id] = tokens[path];
3088
3437
  }
3089
3438
  for (const group of Object.values(groups)) group.tokens.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3439
+ return tokensSorted;
3440
+ }
3441
+
3442
+ //#endregion
3443
+ //#region src/resolver/normalize.ts
3444
+ /** Normalize resolver (assuming it’s been validated) */
3445
+ async function normalizeResolver(node, { filename, req, src, logger, yamlToMomoa }) {
3446
+ const resolverSource = momoa.evaluate(node);
3447
+ const resolutionOrder = getObjMember(node.body, "resolutionOrder");
3448
+ return {
3449
+ name: resolverSource.name,
3450
+ version: resolverSource.version,
3451
+ description: resolverSource.description,
3452
+ sets: resolverSource.sets,
3453
+ modifiers: resolverSource.modifiers,
3454
+ resolutionOrder: await Promise.all(resolutionOrder.elements.map(async (element, i) => {
3455
+ const layer = element.value;
3456
+ const members = getObjMembers(layer);
3457
+ let item = layer;
3458
+ if (members.$ref) {
3459
+ const entry = {
3460
+ group: "parser",
3461
+ label: "init",
3462
+ node: members.$ref,
3463
+ src
3464
+ };
3465
+ const { url, subpath } = parseRef(members.$ref.value);
3466
+ if (url === ".") if (!subpath?.[0]) logger.error({
3467
+ ...entry,
3468
+ message: "$ref can’t refer to the root document."
3469
+ });
3470
+ else if (subpath[0] !== "sets" && subpath[0] !== "modifiers") logger.error({
3471
+ ...entry,
3472
+ message: "Local $ref in resolutionOrder must point to either #/sets/[set] or #/modifiers/[modifiers]."
3473
+ });
3474
+ else {
3475
+ const resolvedItem = resolverSource[subpath[0]]?.[subpath[1]];
3476
+ if (!resolvedItem) logger.error({
3477
+ ...entry,
3478
+ message: "Invalid $ref"
3479
+ });
3480
+ else item = {
3481
+ type: subpath[0] === "sets" ? "set" : "modifier",
3482
+ name: subpath[1],
3483
+ ...resolvedItem
3484
+ };
3485
+ }
3486
+ else {
3487
+ const result = await bundle([{
3488
+ filename: new URL(url, filename),
3489
+ src: resolverSource.resolutionOrder[i]
3490
+ }], {
3491
+ req,
3492
+ yamlToMomoa
3493
+ });
3494
+ if (result.document.body.type === "Object") {
3495
+ const type = getObjMember(result.document.body, "type");
3496
+ if (type?.type === "String" && type.value === "set") {
3497
+ validateSet(result.document.body, true, src);
3498
+ item = momoa.evaluate(result.document.body);
3499
+ } else if (type?.type === "String" && type.value === "modifier") {
3500
+ validateModifier(result.document.body, true, src);
3501
+ item = momoa.evaluate(result.document.body);
3502
+ }
3503
+ }
3504
+ logger.error({
3505
+ ...entry,
3506
+ message: "$ref did not resolve to a valid Set or Modifier."
3507
+ });
3508
+ }
3509
+ }
3510
+ const finalResult = await bundle([{
3511
+ filename,
3512
+ src: item
3513
+ }], {
3514
+ req,
3515
+ yamlToMomoa
3516
+ });
3517
+ return momoa.evaluate(finalResult.document.body);
3518
+ })),
3519
+ _source: {
3520
+ filename,
3521
+ node
3522
+ }
3523
+ };
3524
+ }
3525
+
3526
+ //#endregion
3527
+ //#region src/resolver/load.ts
3528
+ /** Quick-parse input sources and find a resolver */
3529
+ async function loadResolver(inputs, { config, logger, req, yamlToMomoa }) {
3530
+ let resolverDoc;
3531
+ let tokens = {};
3532
+ const entry = {
3533
+ group: "parser",
3534
+ label: "init"
3535
+ };
3536
+ for (const input of inputs) {
3537
+ let document;
3538
+ if (typeof input.src === "string") if (maybeRawJSON(input.src)) document = toMomoa(input.src);
3539
+ else if (yamlToMomoa) document = yamlToMomoa(input.src);
3540
+ else logger.error({
3541
+ ...entry,
3542
+ message: `Install yaml-to-momoa package to parse YAML, and pass in as option, e.g.:
3543
+
3544
+ import { bundle } from '@terrazzo/json-schema-tools';
3545
+ import yamlToMomoa from 'yaml-to-momoa';
3546
+
3547
+ bundle(yamlString, { yamlToMomoa });`
3548
+ });
3549
+ else if (input.src && typeof input.src === "object") document = toMomoa(JSON.stringify(input.src, void 0, 2));
3550
+ else logger.error({
3551
+ ...entry,
3552
+ message: `Could not parse ${input.filename}. Is this valid JSON or YAML?`
3553
+ });
3554
+ if (!document || !isLikelyResolver(document)) continue;
3555
+ if (inputs.length > 1) logger.error({
3556
+ ...entry,
3557
+ message: `Resolver must be the only input, found ${inputs.length} sources.`
3558
+ });
3559
+ resolverDoc = document;
3560
+ break;
3561
+ }
3562
+ let resolver;
3563
+ if (resolverDoc) {
3564
+ validateResolver(resolverDoc, {
3565
+ logger,
3566
+ src: inputs[0].src
3567
+ });
3568
+ resolver = createResolver(await normalizeResolver(resolverDoc, {
3569
+ filename: inputs[0].filename,
3570
+ logger,
3571
+ req,
3572
+ src: inputs[0].src,
3573
+ yamlToMomoa
3574
+ }), {
3575
+ config,
3576
+ logger,
3577
+ sources: [{
3578
+ ...inputs[0],
3579
+ document: resolverDoc
3580
+ }]
3581
+ });
3582
+ const firstInput = {};
3583
+ for (const m of resolver.source.resolutionOrder) {
3584
+ if (m.type !== "modifier") continue;
3585
+ firstInput[m.name] = typeof m.default === "string" ? m.default : Object.keys(m.contexts)[0];
3586
+ }
3587
+ tokens = resolver.apply(firstInput);
3588
+ }
3589
+ return {
3590
+ resolver,
3591
+ tokens,
3592
+ sources: [{
3593
+ ...inputs[0],
3594
+ document: resolverDoc
3595
+ }]
3596
+ };
3597
+ }
3598
+ /** Create an interface to resolve permutations */
3599
+ function createResolver(resolverSource, { config, logger, sources }) {
3600
+ const inputDefaults = {};
3601
+ const validContexts = {};
3602
+ const allPermutations = [];
3603
+ const resolverCache = {};
3604
+ for (const m of resolverSource.resolutionOrder) if (m.type === "modifier") {
3605
+ if (typeof m.default === "string") inputDefaults[m.name] = m.default;
3606
+ validContexts[m.name] = Object.keys(m.contexts);
3607
+ }
3090
3608
  return {
3091
- tokens: tokensSorted,
3609
+ apply(inputRaw) {
3610
+ let tokensRaw = {};
3611
+ const input = {
3612
+ ...inputDefaults,
3613
+ ...inputRaw
3614
+ };
3615
+ const inputKey = makeInputKey(input);
3616
+ if (resolverCache[inputKey]) return resolverCache[inputKey];
3617
+ for (const item of resolverSource.resolutionOrder) switch (item.type) {
3618
+ case "set":
3619
+ for (const s of item.sources) tokensRaw = merge(tokensRaw, s);
3620
+ break;
3621
+ case "modifier": {
3622
+ const context = input[item.name];
3623
+ const sources$1 = item.contexts[context];
3624
+ if (!sources$1) logger.error({
3625
+ group: "parser",
3626
+ label: "resolver",
3627
+ message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`
3628
+ });
3629
+ for (const s of sources$1 ?? []) tokensRaw = merge(tokensRaw, s);
3630
+ break;
3631
+ }
3632
+ }
3633
+ const src = JSON.stringify(tokensRaw, void 0, 2);
3634
+ const tokens = processTokens({
3635
+ filename: resolverSource._source.filename,
3636
+ document: toMomoa(src),
3637
+ src
3638
+ }, {
3639
+ config,
3640
+ logger,
3641
+ sourceByFilename: {},
3642
+ refMap: {},
3643
+ sources
3644
+ });
3645
+ resolverCache[inputKey] = tokens;
3646
+ return tokens;
3647
+ },
3648
+ source: resolverSource,
3649
+ listPermutations() {
3650
+ if (!allPermutations.length) allPermutations.push(...calculatePermutations(Object.entries(validContexts)));
3651
+ return allPermutations;
3652
+ },
3653
+ isValidInput(input) {
3654
+ if (!input || typeof input !== "object") logger.error({
3655
+ group: "parser",
3656
+ label: "resolver",
3657
+ message: `Invalid input: ${JSON.stringify(input)}.`
3658
+ });
3659
+ if (!Object.keys(input).every((k) => k in validContexts)) return false;
3660
+ for (const [name, contexts] of Object.entries(validContexts)) if (name in input) {
3661
+ if (!contexts.includes(input[name])) return false;
3662
+ } else if (!(name in inputDefaults)) return false;
3663
+ return true;
3664
+ }
3665
+ };
3666
+ }
3667
+ /** Calculate all permutations */
3668
+ function calculatePermutations(options) {
3669
+ const permutationCount = [1];
3670
+ for (const [_name, contexts] of options) permutationCount.push(contexts.length * (permutationCount.at(-1) || 1));
3671
+ const permutations = [];
3672
+ for (let i = 0; i < permutationCount.at(-1); i++) {
3673
+ const input = {};
3674
+ for (let j = 0; j < options.length; j++) {
3675
+ const [name, contexts] = options[j];
3676
+ input[name] = contexts[Math.floor(i / permutationCount[j]) % contexts.length];
3677
+ }
3678
+ permutations.push(input);
3679
+ }
3680
+ return permutations.length ? permutations : [{}];
3681
+ }
3682
+
3683
+ //#endregion
3684
+ //#region src/resolver/create-synthetic-resolver.ts
3685
+ /**
3686
+ * Interop layer upgrading legacy Terrazzo modes to resolvers
3687
+ */
3688
+ async function createSyntheticResolver(tokens, { config, logger, req, sources }) {
3689
+ const contexts = {};
3690
+ for (const token of Object.values(tokens)) for (const [mode, value] of Object.entries(token.mode)) {
3691
+ if (mode === ".") continue;
3692
+ if (!(mode in contexts)) contexts[mode] = [{}];
3693
+ addToken(contexts[mode][0], {
3694
+ ...token,
3695
+ $value: value.$value
3696
+ }, { logger });
3697
+ }
3698
+ const src = JSON.stringify({
3699
+ name: "Terrazzo",
3700
+ version: "2025.10",
3701
+ resolutionOrder: [{ $ref: "#/sets/allTokens" }, { $ref: "#/modifiers/tzMode" }],
3702
+ sets: { allTokens: { sources: [simpleFlatten(tokens, { logger })] } },
3703
+ modifiers: { tzMode: {
3704
+ description: "Automatically built from $extensions.mode",
3705
+ contexts
3706
+ } }
3707
+ }, void 0, 2);
3708
+ return createResolver(await normalizeResolver(momoa.parse(src), {
3709
+ filename: new URL("file:///virtual:resolver.json"),
3710
+ logger,
3711
+ req,
3712
+ src
3713
+ }), {
3714
+ config,
3715
+ logger,
3716
+ sources
3717
+ });
3718
+ }
3719
+ /** Add a normalized token back into an arbitrary, hierarchial structure */
3720
+ function addToken(structure, token, { logger }) {
3721
+ let node = structure;
3722
+ const parts = token.id.split(".");
3723
+ const localID = parts.pop();
3724
+ for (const part of parts) {
3725
+ if (!(part in node)) node[part] = {};
3726
+ node = node[part];
3727
+ }
3728
+ if (localID in node) logger.error({
3729
+ group: "parser",
3730
+ label: "resolver",
3731
+ message: `${localID} already exists!`
3732
+ });
3733
+ node[localID] = {
3734
+ $type: token.$type,
3735
+ $value: token.$value
3736
+ };
3737
+ }
3738
+ /** Downconvert normalized tokens back into a simplified, hierarchial shape. This is extremely lossy, and only done to build a resolver. */
3739
+ function simpleFlatten(tokens, { logger }) {
3740
+ const group = {};
3741
+ for (const token of Object.values(tokens)) addToken(group, token, { logger });
3742
+ return group;
3743
+ }
3744
+
3745
+ //#endregion
3746
+ //#region src/parse/load.ts
3747
+ /** Load from multiple entries, while resolving remote files */
3748
+ async function loadSources(inputs, { config, logger, req, continueOnError, yamlToMomoa, transform }) {
3749
+ const entry = {
3750
+ group: "parser",
3751
+ label: "init"
3752
+ };
3753
+ const firstLoad = performance.now();
3754
+ let document = {};
3755
+ /** The original user inputs, in original order, with parsed ASTs */
3756
+ const sources = inputs.map((input, i) => ({
3757
+ ...input,
3758
+ document: {},
3759
+ filename: input.filename || new URL(`virtual:${i}`)
3760
+ }));
3761
+ /** The sources array, indexed by filename */
3762
+ let sourceByFilename = {};
3763
+ /** Mapping of all final $ref resolutions. This will be used to generate the graph later. */
3764
+ let refMap = {};
3765
+ try {
3766
+ const result = await bundle(sources, {
3767
+ req,
3768
+ parse: transform ? transformer(transform) : void 0,
3769
+ yamlToMomoa
3770
+ });
3771
+ document = result.document;
3772
+ sourceByFilename = result.sources;
3773
+ refMap = result.refMap;
3774
+ for (const [filename, source] of Object.entries(result.sources)) {
3775
+ const i = sources.findIndex((s) => s.filename.href === filename);
3776
+ if (i === -1) sources.push(source);
3777
+ else {
3778
+ sources[i].src = source.src;
3779
+ sources[i].document = source.document;
3780
+ }
3781
+ }
3782
+ } catch (err) {
3783
+ let src = sources.find((s) => s.filename.href === err.filename)?.src;
3784
+ if (src && typeof src !== "string") src = JSON.stringify(src, void 0, 2);
3785
+ logger.error({
3786
+ ...entry,
3787
+ continueOnError,
3788
+ message: err.message,
3789
+ node: err.node,
3790
+ src
3791
+ });
3792
+ }
3793
+ logger.debug({
3794
+ ...entry,
3795
+ message: `JSON loaded`,
3796
+ timing: performance.now() - firstLoad
3797
+ });
3798
+ return {
3799
+ tokens: processTokens({
3800
+ filename: sources[0].filename,
3801
+ document,
3802
+ src: momoa.print(document, { indent: 2 })
3803
+ }, {
3804
+ config,
3805
+ logger,
3806
+ refMap,
3807
+ sources,
3808
+ sourceByFilename
3809
+ }),
3092
3810
  sources
3093
3811
  };
3094
3812
  }
@@ -3105,7 +3823,9 @@ function transformer(transform) {
3105
3823
  });
3106
3824
  if (result) document = result;
3107
3825
  }
3108
- await traverseAsync(document, { async enter(node, parent, path) {
3826
+ const isResolver = isLikelyResolver(document);
3827
+ traverse(document, { enter(node, parent, rawPath) {
3828
+ const path = isResolver ? filterResolverPaths(rawPath) : rawPath;
3109
3829
  if (node.type !== "Object" || !path.length) return;
3110
3830
  const ctx = {
3111
3831
  filename,
@@ -3138,17 +3858,35 @@ function transformer(transform) {
3138
3858
  //#endregion
3139
3859
  //#region src/parse/index.ts
3140
3860
  /** Parse */
3141
- async function parse(_input, { logger = new Logger(), skipLint = false, config = {}, continueOnError = false, yamlToMomoa, transform } = {}) {
3861
+ async function parse(_input, { logger = new Logger(), req = defaultReq, skipLint = false, config = {}, continueOnError = false, yamlToMomoa, transform } = {}) {
3142
3862
  const inputs = Array.isArray(_input) ? _input : [_input];
3863
+ let tokens = {};
3864
+ let resolver;
3865
+ let sources = [];
3143
3866
  const totalStart = performance.now();
3144
3867
  const initStart = performance.now();
3145
- const { tokens, sources } = await loadSources(inputs, {
3146
- logger,
3868
+ const resolverResult = await loadResolver(inputs, {
3147
3869
  config,
3148
- continueOnError,
3149
- yamlToMomoa,
3150
- transform
3870
+ logger,
3871
+ req,
3872
+ yamlToMomoa
3151
3873
  });
3874
+ if (resolverResult.resolver) {
3875
+ tokens = resolverResult.tokens;
3876
+ sources = resolverResult.sources;
3877
+ resolver = resolverResult.resolver;
3878
+ } else {
3879
+ const tokenResult = await loadSources(inputs, {
3880
+ req,
3881
+ logger,
3882
+ config,
3883
+ continueOnError,
3884
+ yamlToMomoa,
3885
+ transform
3886
+ });
3887
+ tokens = tokenResult.tokens;
3888
+ sources = tokenResult.sources;
3889
+ }
3152
3890
  logger.debug({
3153
3891
  message: "Loaded tokens",
3154
3892
  group: "parser",
@@ -3185,10 +3923,27 @@ async function parse(_input, { logger = new Logger(), skipLint = false, config =
3185
3923
  }
3186
3924
  return {
3187
3925
  tokens,
3188
- sources
3926
+ sources,
3927
+ resolver: resolver || await createSyntheticResolver(tokens, {
3928
+ config,
3929
+ logger,
3930
+ req,
3931
+ sources
3932
+ })
3189
3933
  };
3190
3934
  }
3935
+ let fs;
3936
+ /** Fallback req */
3937
+ async function defaultReq(src, _origin) {
3938
+ if (src.protocol === "file:") {
3939
+ if (!fs) fs = await import("node:fs/promises");
3940
+ return await fs.readFile(src, "utf8");
3941
+ }
3942
+ const res = await fetch(src);
3943
+ if (!res.ok) throw new Error(`${src} responded with ${res.status}\n${await res.text()}`);
3944
+ return await res.text();
3945
+ }
3191
3946
 
3192
3947
  //#endregion
3193
- export { LOG_ORDER, Logger, MULTI_VALUE, RECOMMENDED_CONFIG, SINGLE_VALUE, TokensJSONError, build, defineConfig, formatMessage, lintRunner, mergeConfigs, parse };
3948
+ 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
3949
  //# sourceMappingURL=index.js.map