@terrazzo/parser 2.0.0-alpha.7 → 2.0.0-beta.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.
Files changed (53) hide show
  1. package/dist/index.d.ts +39 -6
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +578 -512
  4. package/dist/index.js.map +1 -1
  5. package/package.json +3 -3
  6. package/src/build/index.ts +0 -209
  7. package/src/config.ts +0 -304
  8. package/src/index.ts +0 -95
  9. package/src/lib/code-frame.ts +0 -177
  10. package/src/lib/momoa.ts +0 -10
  11. package/src/lib/resolver-utils.ts +0 -35
  12. package/src/lint/index.ts +0 -142
  13. package/src/lint/plugin-core/index.ts +0 -103
  14. package/src/lint/plugin-core/lib/docs.ts +0 -3
  15. package/src/lint/plugin-core/rules/a11y-min-contrast.ts +0 -91
  16. package/src/lint/plugin-core/rules/a11y-min-font-size.ts +0 -66
  17. package/src/lint/plugin-core/rules/colorspace.ts +0 -108
  18. package/src/lint/plugin-core/rules/consistent-naming.ts +0 -65
  19. package/src/lint/plugin-core/rules/descriptions.ts +0 -43
  20. package/src/lint/plugin-core/rules/duplicate-values.ts +0 -85
  21. package/src/lint/plugin-core/rules/max-gamut.ts +0 -144
  22. package/src/lint/plugin-core/rules/required-children.ts +0 -106
  23. package/src/lint/plugin-core/rules/required-modes.ts +0 -75
  24. package/src/lint/plugin-core/rules/required-type.ts +0 -28
  25. package/src/lint/plugin-core/rules/required-typography-properties.ts +0 -65
  26. package/src/lint/plugin-core/rules/valid-boolean.ts +0 -41
  27. package/src/lint/plugin-core/rules/valid-border.ts +0 -57
  28. package/src/lint/plugin-core/rules/valid-color.ts +0 -265
  29. package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +0 -83
  30. package/src/lint/plugin-core/rules/valid-dimension.ts +0 -199
  31. package/src/lint/plugin-core/rules/valid-duration.ts +0 -123
  32. package/src/lint/plugin-core/rules/valid-font-family.ts +0 -68
  33. package/src/lint/plugin-core/rules/valid-font-weight.ts +0 -89
  34. package/src/lint/plugin-core/rules/valid-gradient.ts +0 -79
  35. package/src/lint/plugin-core/rules/valid-link.ts +0 -41
  36. package/src/lint/plugin-core/rules/valid-number.ts +0 -63
  37. package/src/lint/plugin-core/rules/valid-shadow.ts +0 -67
  38. package/src/lint/plugin-core/rules/valid-string.ts +0 -41
  39. package/src/lint/plugin-core/rules/valid-stroke-style.ts +0 -104
  40. package/src/lint/plugin-core/rules/valid-transition.ts +0 -61
  41. package/src/lint/plugin-core/rules/valid-typography.ts +0 -67
  42. package/src/logger.ts +0 -213
  43. package/src/parse/index.ts +0 -124
  44. package/src/parse/load.ts +0 -172
  45. package/src/parse/normalize.ts +0 -163
  46. package/src/parse/process.ts +0 -251
  47. package/src/parse/token.ts +0 -553
  48. package/src/resolver/create-synthetic-resolver.ts +0 -86
  49. package/src/resolver/index.ts +0 -7
  50. package/src/resolver/load.ts +0 -215
  51. package/src/resolver/normalize.ts +0 -133
  52. package/src/resolver/validate.ts +0 -375
  53. package/src/types.ts +0 -468
package/dist/index.js CHANGED
@@ -230,29 +230,35 @@ function validateTransformParams({ params, logger, pluginName }) {
230
230
  message: "setTransform() value expected object of strings, received some non-string values"
231
231
  });
232
232
  }
233
+ const FALLBACK_PERMUTATION_ID = JSON.stringify({ tzMode: "*" });
233
234
  /** Run build stage */
234
235
  async function build(tokens, { resolver, sources, logger = new Logger(), config }) {
235
236
  const formats = {};
236
237
  const result = { outputFiles: [] };
237
- function getTransforms(params) {
238
- if (!params?.format) {
239
- logger.warn({
240
- group: "plugin",
241
- message: "\"format\" missing from getTransforms(), no tokens returned."
242
- });
243
- return [];
244
- }
245
- const tokenMatcher = params.id ? wcmatch(Array.isArray(params.id) ? params.id : [params.id]) : null;
246
- const modeMatcher = params.mode ? wcmatch(params.mode) : null;
247
- return (formats[params.format] ?? []).filter((token) => {
248
- if (params.$type) {
249
- if (typeof params.$type === "string" && token.token.$type !== params.$type) return false;
250
- else if (Array.isArray(params.$type) && !params.$type.some(($type) => token.token.$type === $type)) return false;
238
+ function getTransforms(plugin) {
239
+ return function getTransforms$1(params) {
240
+ if (!params?.format) {
241
+ logger.warn({
242
+ group: "plugin",
243
+ label: plugin,
244
+ message: "\"format\" missing from getTransforms(), no tokens returned."
245
+ });
246
+ return [];
251
247
  }
252
- if (params.id && params.id !== "*" && tokenMatcher && !tokenMatcher(token.token.id)) return false;
253
- if (modeMatcher && !modeMatcher(token.mode)) return false;
254
- return true;
255
- });
248
+ const tokenMatcher = params.id && params.id !== "*" ? wcmatch(params.id) : null;
249
+ const modeMatcher = params.mode ? wcmatch(params.mode) : null;
250
+ const permutationID = params.input ? resolver.getPermutationID(params.input) : JSON.stringify({ tzMode: "*" });
251
+ return (formats[params.format]?.[permutationID] ?? []).filter((token) => {
252
+ if (params.$type) {
253
+ if (typeof params.$type === "string" && token.token.$type !== params.$type) return false;
254
+ else if (Array.isArray(params.$type) && !params.$type.some(($type) => token.token.$type === $type)) return false;
255
+ }
256
+ if (tokenMatcher && !tokenMatcher(token.token.id)) return false;
257
+ if (params.input && token.permutationID !== resolver.getPermutationID(params.input)) return false;
258
+ if (modeMatcher && !modeMatcher(token.mode)) return false;
259
+ return true;
260
+ });
261
+ };
256
262
  }
257
263
  let transformsLocked = false;
258
264
  const startTransform = performance.now();
@@ -260,7 +266,7 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
260
266
  context: { logger },
261
267
  tokens,
262
268
  sources,
263
- getTransforms,
269
+ getTransforms: getTransforms(plugin.name),
264
270
  setTransform(id, params) {
265
271
  if (transformsLocked) {
266
272
  logger.warn({
@@ -271,6 +277,7 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
271
277
  return;
272
278
  }
273
279
  const token = tokens[id];
280
+ const permutationID = params.input ? resolver.getPermutationID(params.input) : FALLBACK_PERMUTATION_ID;
274
281
  const cleanValue = typeof params.value === "string" ? params.value : { ...params.value };
275
282
  validateTransformParams({
276
283
  logger,
@@ -280,19 +287,27 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
280
287
  },
281
288
  pluginName: plugin.name
282
289
  });
283
- if (!formats[params.format]) formats[params.format] = [];
284
- const foundTokenI = formats[params.format].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && (!params.mode || params.mode === t.mode));
285
- if (foundTokenI === -1) formats[params.format].push({
290
+ if (!formats[params.format]) formats[params.format] = {};
291
+ if (!formats[params.format][permutationID]) formats[params.format][permutationID] = [];
292
+ let foundTokenI = -1;
293
+ if (params.mode) foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && params.mode === t.mode);
294
+ else if (params.input) {
295
+ if (!formats[params.format][permutationID]) formats[params.format][permutationID] = [];
296
+ foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && permutationID === t.permutationID);
297
+ } else foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID));
298
+ if (foundTokenI === -1) formats[params.format][permutationID].push({
286
299
  ...params,
287
300
  id,
288
301
  value: cleanValue,
289
302
  type: typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE,
290
303
  mode: params.mode || ".",
291
- token: structuredClone(token)
304
+ token: structuredClone(token),
305
+ permutationID,
306
+ input: JSON.parse(permutationID)
292
307
  });
293
308
  else {
294
- formats[params.format][foundTokenI].value = cleanValue;
295
- formats[params.format][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
309
+ formats[params.format][permutationID][foundTokenI].value = cleanValue;
310
+ formats[params.format][permutationID][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
296
311
  }
297
312
  },
298
313
  resolver
@@ -312,7 +327,7 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
312
327
  context: { logger },
313
328
  tokens,
314
329
  sources,
315
- getTransforms,
330
+ getTransforms: getTransforms(plugin.name),
316
331
  resolver,
317
332
  outputFile(filename, contents) {
318
333
  const resolved = new URL(filename, config.outDir);
@@ -341,7 +356,7 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
341
356
  await Promise.all(config.plugins.map(async (plugin) => plugin.buildEnd?.({
342
357
  context: { logger },
343
358
  tokens,
344
- getTransforms,
359
+ getTransforms: getTransforms(plugin.name),
345
360
  sources,
346
361
  outputFiles: structuredClone(result.outputFiles)
347
362
  })));
@@ -2150,6 +2165,7 @@ function normalizeTokens({ rawConfig, config, logger, cwd }) {
2150
2165
  });
2151
2166
  }
2152
2167
  }
2168
+ config.alphabetize = rawConfig.alphabetize ?? true;
2153
2169
  }
2154
2170
  /** Normalize config.outDir */
2155
2171
  function normalizeOutDir({ config, cwd, logger }) {
@@ -2423,449 +2439,66 @@ function filterResolverPaths(path) {
2423
2439
  }
2424
2440
  return path;
2425
2441
  }
2426
- /**
2427
- * Make a deterministic string from an object
2428
- */
2429
- function makeInputKey(input) {
2430
- return JSON.stringify(Object.fromEntries(Object.entries(input).sort((a, b) => a[0].localeCompare(b[0], "en-us", { numeric: true }))));
2442
+ /** Make a deterministic string from an object */
2443
+ function getPermutationID(input) {
2444
+ const keys = Object.keys(input).sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2445
+ return JSON.stringify(Object.fromEntries(keys.map((k) => [k, input[k]])));
2431
2446
  }
2432
2447
 
2433
2448
  //#endregion
2434
- //#region src/resolver/validate.ts
2435
- /**
2436
- * Determine whether this is likely a resolver
2437
- * We use terms the word “likely” because this occurs before validation. Since
2438
- * we may be dealing with a doc _intended_ to be a resolver, but may be lacking
2439
- * some critical information, how can we determine intent? There’s a bit of
2440
- * guesswork here, but we try and find a reasonable edge case where we sniff out
2441
- * invalid DTCG syntax that a resolver doc would have.
2442
- */
2443
- function isLikelyResolver(doc) {
2444
- if (doc.body.type !== "Object") return false;
2445
- for (const member of doc.body.members) {
2446
- if (member.name.type !== "String") continue;
2447
- switch (member.name.value) {
2448
- case "name":
2449
- case "description":
2450
- case "version":
2451
- if (member.value.type === "String") return true;
2452
- break;
2453
- case "sets":
2454
- case "modifiers":
2455
- if (member.value.type !== "Object") continue;
2456
- if (getObjMember(member.value, "description")?.type === "String") return true;
2457
- if (member.name.value === "sets" && getObjMember(member.value, "sources")?.type === "Array") return true;
2458
- else if (member.name.value === "modifiers") {
2459
- const contexts = getObjMember(member.value, "contexts");
2460
- if (contexts?.type === "Object" && contexts.members.some((m) => m.value.type === "Array")) return true;
2461
- }
2462
- break;
2463
- case "resolutionOrder":
2464
- if (member.value.type === "Array") return true;
2465
- break;
2466
- }
2467
- }
2468
- return false;
2449
+ //#region src/parse/assert.ts
2450
+ function assert(value, logger, entry) {
2451
+ if (!value) logger.error(entry);
2469
2452
  }
2470
- const MESSAGE_EXPECTED = {
2471
- STRING: "Expected string.",
2472
- OBJECT: "Expected object.",
2473
- ARRAY: "Expected array."
2474
- };
2453
+ function assertStringNode(value, logger, entry) {
2454
+ assert(value?.type === "String", logger, entry);
2455
+ }
2456
+ function assertObjectNode(value, logger, entry) {
2457
+ assert(value?.type === "Object", logger, entry);
2458
+ }
2459
+
2460
+ //#endregion
2461
+ //#region src/parse/normalize.ts
2475
2462
  /**
2476
- * Validate a resolver document.
2477
- * There’s a ton of boilerplate here, only to surface detailed code frames. Is there a better abstraction?
2463
+ * Normalize token value.
2464
+ * The reason for the “any” typing is this aligns various user-provided inputs to the type
2478
2465
  */
2479
- function validateResolver(node, { logger, src }) {
2466
+ function normalize(token, { logger, src }) {
2480
2467
  const entry = {
2481
2468
  group: "parser",
2482
- label: "resolver",
2469
+ label: "init",
2483
2470
  src
2484
2471
  };
2485
- if (node.body.type !== "Object") logger.error({
2486
- ...entry,
2487
- message: MESSAGE_EXPECTED.OBJECT,
2488
- node
2489
- });
2490
- const errors = [];
2491
- let hasVersion = false;
2492
- let hasResolutionOrder = false;
2493
- for (const member of node.body.members) {
2494
- if (member.name.type !== "String") continue;
2495
- switch (member.name.value) {
2496
- case "name":
2497
- case "description":
2498
- if (member.value.type !== "String") errors.push({
2499
- ...entry,
2500
- message: MESSAGE_EXPECTED.STRING
2501
- });
2502
- break;
2503
- case "version":
2504
- hasVersion = true;
2505
- if (member.value.type !== "String" || member.value.value !== "2025.10") errors.push({
2506
- ...entry,
2507
- message: `Expected "version" to be "2025.10".`,
2508
- node: member.value
2509
- });
2510
- break;
2511
- case "sets":
2512
- case "modifiers":
2513
- if (member.value.type !== "Object") errors.push({
2514
- ...entry,
2515
- message: MESSAGE_EXPECTED.OBJECT,
2516
- node: member.value
2517
- });
2518
- else for (const item of member.value.members) if (item.value.type !== "Object") errors.push({
2519
- ...entry,
2520
- message: MESSAGE_EXPECTED.OBJECT,
2521
- node: item.value
2522
- });
2523
- else {
2524
- const validator = member.name.value === "sets" ? validateSet : validateModifier;
2525
- errors.push(...validator(item.value, false, {
2526
- logger,
2527
- src
2528
- }));
2529
- }
2530
- break;
2531
- case "resolutionOrder":
2532
- hasResolutionOrder = true;
2533
- if (member.value.type !== "Array") errors.push({
2534
- ...entry,
2535
- message: MESSAGE_EXPECTED.ARRAY,
2536
- node: member.value
2537
- });
2538
- else if (member.value.elements.length === 0) errors.push({
2539
- ...entry,
2540
- message: `"resolutionOrder" can’t be empty array.`,
2541
- node: member.value
2542
- });
2543
- else for (const item of member.value.elements) if (item.value.type !== "Object") errors.push({
2544
- ...entry,
2545
- message: MESSAGE_EXPECTED.OBJECT,
2546
- node: item.value
2547
- });
2548
- else {
2549
- const itemMembers = getObjMembers(item.value);
2550
- if (itemMembers.$ref?.type === "String") continue;
2551
- if (itemMembers.type?.type === "String") if (itemMembers.type.value === "set") validateSet(item.value, true, {
2552
- logger,
2553
- src
2554
- });
2555
- else if (itemMembers.type.value === "modifier") validateModifier(item.value, true, {
2556
- logger,
2557
- src
2558
- });
2559
- else errors.push({
2560
- ...entry,
2561
- message: `Unknown type ${JSON.stringify(itemMembers.type.value)}`,
2562
- node: itemMembers.type
2563
- });
2564
- if (itemMembers.sources?.type === "Array") validateSet(item.value, true, {
2565
- logger,
2566
- src
2567
- });
2568
- else if (itemMembers.contexts?.type === "Object") validateModifier(item.value, true, {
2569
- logger,
2570
- src
2571
- });
2572
- else if (itemMembers.name?.type === "String" || itemMembers.description?.type === "String") validateSet(item.value, true, {
2573
- logger,
2574
- src
2575
- });
2576
- }
2577
- break;
2578
- case "$defs":
2579
- case "$extensions":
2580
- if (member.value.type !== "Object") errors.push({
2581
- ...entry,
2582
- message: `Expected object`,
2583
- node: member.value
2584
- });
2585
- break;
2586
- case "$schema":
2587
- case "$ref":
2588
- if (member.value.type !== "String") errors.push({
2589
- ...entry,
2590
- message: `Expected string`,
2591
- node: member.value
2592
- });
2593
- break;
2594
- default:
2595
- errors.push({
2596
- ...entry,
2597
- message: `Unknown key ${JSON.stringify(member.name.value)}`,
2598
- node: member.name,
2599
- src
2600
- });
2601
- break;
2472
+ function normalizeFontFamily(value) {
2473
+ return typeof value === "string" ? [value] : value;
2474
+ }
2475
+ function normalizeFontWeight(value) {
2476
+ return typeof value === "string" && FONT_WEIGHTS[value] || value;
2477
+ }
2478
+ function normalizeColor(value, node) {
2479
+ if (typeof value === "string" && !isAlias(value)) {
2480
+ logger.warn({
2481
+ ...entry,
2482
+ node,
2483
+ message: `${token.id}: string colors will be deprecated in a future version. Please update to object notation`
2484
+ });
2485
+ try {
2486
+ return parseColor(value);
2487
+ } catch {
2488
+ return {
2489
+ colorSpace: "srgb",
2490
+ components: [
2491
+ 0,
2492
+ 0,
2493
+ 0
2494
+ ],
2495
+ alpha: 1
2496
+ };
2497
+ }
2498
+ } else if (value && typeof value === "object") {
2499
+ if (value.alpha === void 0) value.alpha = 1;
2602
2500
  }
2603
- }
2604
- if (!hasVersion) errors.push({
2605
- ...entry,
2606
- message: `Missing "version".`,
2607
- node,
2608
- src
2609
- });
2610
- if (!hasResolutionOrder) errors.push({
2611
- ...entry,
2612
- message: `Missing "resolutionOrder".`,
2613
- node,
2614
- src
2615
- });
2616
- if (errors.length) logger.error(...errors);
2617
- }
2618
- function validateSet(node, isInline = false, { src }) {
2619
- const entry = {
2620
- group: "parser",
2621
- label: "resolver",
2622
- src
2623
- };
2624
- const errors = [];
2625
- let hasName = !isInline;
2626
- let hasType = !isInline;
2627
- let hasSources = false;
2628
- for (const member of node.members) {
2629
- if (member.name.type !== "String") continue;
2630
- switch (member.name.value) {
2631
- case "name":
2632
- hasName = true;
2633
- if (member.value.type !== "String") errors.push({
2634
- ...entry,
2635
- message: MESSAGE_EXPECTED.STRING,
2636
- node: member.value
2637
- });
2638
- break;
2639
- case "description":
2640
- if (member.value.type !== "String") errors.push({
2641
- ...entry,
2642
- message: MESSAGE_EXPECTED.STRING,
2643
- node: member.value
2644
- });
2645
- break;
2646
- case "type":
2647
- hasType = true;
2648
- if (member.value.type !== "String") errors.push({
2649
- ...entry,
2650
- message: MESSAGE_EXPECTED.STRING,
2651
- node: member.value
2652
- });
2653
- else if (member.value.value !== "set") errors.push({
2654
- ...entry,
2655
- message: "\"type\" must be \"set\"."
2656
- });
2657
- break;
2658
- case "sources":
2659
- hasSources = true;
2660
- if (member.value.type !== "Array") errors.push({
2661
- ...entry,
2662
- message: MESSAGE_EXPECTED.ARRAY,
2663
- node: member.value
2664
- });
2665
- else if (member.value.elements.length === 0) errors.push({
2666
- ...entry,
2667
- message: `"sources" can’t be empty array.`,
2668
- node: member.value
2669
- });
2670
- break;
2671
- case "$defs":
2672
- case "$extensions":
2673
- if (member.value.type !== "Object") errors.push({
2674
- ...entry,
2675
- message: `Expected object`,
2676
- node: member.value
2677
- });
2678
- break;
2679
- case "$ref":
2680
- if (member.value.type !== "String") errors.push({
2681
- ...entry,
2682
- message: `Expected string`,
2683
- node: member.value
2684
- });
2685
- break;
2686
- default:
2687
- errors.push({
2688
- ...entry,
2689
- message: `Unknown key ${JSON.stringify(member.name.value)}`,
2690
- node: member.name
2691
- });
2692
- break;
2693
- }
2694
- }
2695
- if (!hasName) errors.push({
2696
- ...entry,
2697
- message: `Missing "name".`,
2698
- node
2699
- });
2700
- if (!hasType) errors.push({
2701
- ...entry,
2702
- message: `"type": "set" missing.`,
2703
- node
2704
- });
2705
- if (!hasSources) errors.push({
2706
- ...entry,
2707
- message: `Missing "sources".`,
2708
- node
2709
- });
2710
- return errors;
2711
- }
2712
- function validateModifier(node, isInline = false, { src }) {
2713
- const errors = [];
2714
- const entry = {
2715
- group: "parser",
2716
- label: "resolver",
2717
- src
2718
- };
2719
- let hasName = !isInline;
2720
- let hasType = !isInline;
2721
- let hasContexts = false;
2722
- for (const member of node.members) {
2723
- if (member.name.type !== "String") continue;
2724
- switch (member.name.value) {
2725
- case "name":
2726
- hasName = true;
2727
- if (member.value.type !== "String") errors.push({
2728
- ...entry,
2729
- message: MESSAGE_EXPECTED.STRING,
2730
- node: member.value
2731
- });
2732
- break;
2733
- case "description":
2734
- if (member.value.type !== "String") errors.push({
2735
- ...entry,
2736
- message: MESSAGE_EXPECTED.STRING,
2737
- node: member.value
2738
- });
2739
- break;
2740
- case "type":
2741
- hasType = true;
2742
- if (member.value.type !== "String") errors.push({
2743
- ...entry,
2744
- message: MESSAGE_EXPECTED.STRING,
2745
- node: member.value
2746
- });
2747
- else if (member.value.value !== "modifier") errors.push({
2748
- ...entry,
2749
- message: "\"type\" must be \"modifier\"."
2750
- });
2751
- break;
2752
- case "contexts":
2753
- hasContexts = true;
2754
- if (member.value.type !== "Object") errors.push({
2755
- ...entry,
2756
- message: MESSAGE_EXPECTED.OBJECT,
2757
- node: member.value
2758
- });
2759
- else if (member.value.members.length === 0) errors.push({
2760
- ...entry,
2761
- message: `"contexts" can’t be empty object.`,
2762
- node: member.value
2763
- });
2764
- else for (const context of member.value.members) if (context.value.type !== "Array") errors.push({
2765
- ...entry,
2766
- message: MESSAGE_EXPECTED.ARRAY,
2767
- node: context.value
2768
- });
2769
- break;
2770
- case "default":
2771
- if (member.value.type !== "String") errors.push({
2772
- ...entry,
2773
- message: `Expected string`,
2774
- node: member.value
2775
- });
2776
- else {
2777
- const contexts = getObjMember(node, "contexts");
2778
- if (!contexts || !getObjMember(contexts, member.value.value)) errors.push({
2779
- ...entry,
2780
- message: "Invalid default context",
2781
- node: member.value
2782
- });
2783
- }
2784
- break;
2785
- case "$defs":
2786
- case "$extensions":
2787
- if (member.value.type !== "Object") errors.push({
2788
- ...entry,
2789
- message: `Expected object`,
2790
- node: member.value
2791
- });
2792
- break;
2793
- case "$ref":
2794
- if (member.value.type !== "String") errors.push({
2795
- ...entry,
2796
- message: `Expected string`,
2797
- node: member.value
2798
- });
2799
- break;
2800
- default:
2801
- errors.push({
2802
- ...entry,
2803
- message: `Unknown key ${JSON.stringify(member.name.value)}`,
2804
- node: member.name
2805
- });
2806
- break;
2807
- }
2808
- }
2809
- if (!hasName) errors.push({
2810
- ...entry,
2811
- message: `Missing "name".`,
2812
- node
2813
- });
2814
- if (!hasType) errors.push({
2815
- ...entry,
2816
- message: `"type": "modifier" missing.`,
2817
- node
2818
- });
2819
- if (!hasContexts) errors.push({
2820
- ...entry,
2821
- message: `Missing "contexts".`,
2822
- node
2823
- });
2824
- return errors;
2825
- }
2826
-
2827
- //#endregion
2828
- //#region src/parse/normalize.ts
2829
- /**
2830
- * Normalize token value.
2831
- * The reason for the “any” typing is this aligns various user-provided inputs to the type
2832
- */
2833
- function normalize(token, { logger, src }) {
2834
- const entry = {
2835
- group: "parser",
2836
- label: "init",
2837
- src
2838
- };
2839
- function normalizeFontFamily(value) {
2840
- return typeof value === "string" ? [value] : value;
2841
- }
2842
- function normalizeFontWeight(value) {
2843
- return typeof value === "string" && FONT_WEIGHTS[value] || value;
2844
- }
2845
- function normalizeColor(value, node) {
2846
- if (typeof value === "string" && !isAlias(value)) {
2847
- logger.warn({
2848
- ...entry,
2849
- node,
2850
- message: `${token.id}: string colors will be deprecated in a future version. Please update to object notation`
2851
- });
2852
- try {
2853
- return parseColor(value);
2854
- } catch {
2855
- return {
2856
- colorSpace: "srgb",
2857
- components: [
2858
- 0,
2859
- 0,
2860
- 0
2861
- ],
2862
- alpha: 1
2863
- };
2864
- }
2865
- } else if (value && typeof value === "object") {
2866
- if (value.alpha === void 0) value.alpha = 1;
2867
- }
2868
- return value;
2501
+ return value;
2869
2502
  }
2870
2503
  switch (token.$type) {
2871
2504
  case "color":
@@ -2951,7 +2584,7 @@ function aliasToTokenRef(alias, mode) {
2951
2584
  function tokenFromNode(node, { groups, path, source, ignore }) {
2952
2585
  if (!(node.type === "Object" && !!getObjMember(node, "$value") && !path.includes("$extensions"))) return;
2953
2586
  const jsonID = encodeFragment(path);
2954
- const id = path.join(".");
2587
+ const id = path.join(".").replace(/\.\$root$/, "");
2955
2588
  const originalToken = momoa.evaluate(node);
2956
2589
  const group = groups[encodeFragment(path.slice(0, -1))];
2957
2590
  if (group?.tokens && !group.tokens.includes(id)) group.tokens.push(id);
@@ -3176,7 +2809,7 @@ function refToTokenID($ref) {
3176
2809
  if (typeof path !== "string") return;
3177
2810
  const { subpath } = parseRef(path);
3178
2811
  if (subpath?.[0] === "$defs") subpath.splice(0, 2);
3179
- return subpath?.length && subpath.join(".").replace(/\.(\$value|\$extensions).*$/, "") || void 0;
2812
+ return subpath?.length && subpath.join(".").replace(/\.(\$root|\$value|\$extensions).*$/, "") || void 0;
3180
2813
  }
3181
2814
  const EXPECTED_NESTED_ALIAS = {
3182
2815
  border: {
@@ -3304,7 +2937,7 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
3304
2937
 
3305
2938
  //#endregion
3306
2939
  //#region src/parse/process.ts
3307
- function processTokens(rootSource, { config, logger, sourceByFilename }) {
2940
+ function processTokens(rootSource, { config, logger, sourceByFilename, isResolver }) {
3308
2941
  const entry = {
3309
2942
  group: "parser",
3310
2943
  label: "init"
@@ -3312,13 +2945,19 @@ function processTokens(rootSource, { config, logger, sourceByFilename }) {
3312
2945
  const refMap = {};
3313
2946
  function resolveRef(node, chain) {
3314
2947
  const { subpath } = parseRef(node.value);
3315
- if (!subpath) logger.error({
2948
+ assert(subpath, logger, {
3316
2949
  ...entry,
3317
2950
  message: "Can’t resolve $ref",
3318
2951
  node,
3319
2952
  src: rootSource.src
3320
2953
  });
3321
2954
  const next = findNode(rootSource.document, subpath);
2955
+ assert(next, logger, {
2956
+ ...entry,
2957
+ message: "Can't find $ref",
2958
+ node,
2959
+ src: rootSource.src
2960
+ });
3322
2961
  if (next?.type === "Object") {
3323
2962
  const next$ref = getObjMember(next, "$ref");
3324
2963
  if (next$ref && next$ref.type === "String") {
@@ -3339,7 +2978,7 @@ function processTokens(rootSource, { config, logger, sourceByFilename }) {
3339
2978
  if (rawPath.includes("$extensions") || node.type !== "Object") return;
3340
2979
  const $ref = node.type === "Object" ? getObjMember(node, "$ref") : void 0;
3341
2980
  if (!$ref) return;
3342
- if ($ref.type !== "String") logger.error({
2981
+ assertStringNode($ref, logger, {
3343
2982
  ...entry,
3344
2983
  message: "Invalid $ref. Expected string.",
3345
2984
  node: $ref,
@@ -3363,10 +3002,9 @@ function processTokens(rootSource, { config, logger, sourceByFilename }) {
3363
3002
  });
3364
3003
  function flatten$extends(node, chain) {
3365
3004
  const memberKeys = node.members.map((m) => m.name.type === "String" && m.name.value).filter(Boolean);
3366
- let extended;
3367
3005
  if (memberKeys.includes("$extends")) {
3368
3006
  const $extends = getObjMember(node, "$extends");
3369
- if ($extends.type !== "String") logger.error({
3007
+ assertStringNode($extends, logger, {
3370
3008
  ...entry,
3371
3009
  message: "$extends must be a string",
3372
3010
  node: $extends,
@@ -3379,7 +3017,7 @@ function processTokens(rootSource, { config, logger, sourceByFilename }) {
3379
3017
  src: rootSource.src
3380
3018
  });
3381
3019
  const next = isAlias($extends.value) ? aliasToGroupRef($extends.value) : void 0;
3382
- if (!next) logger.error({
3020
+ assert(next, logger, {
3383
3021
  ...entry,
3384
3022
  message: "$extends must be a valid alias",
3385
3023
  node: $extends,
@@ -3392,14 +3030,14 @@ function processTokens(rootSource, { config, logger, sourceByFilename }) {
3392
3030
  src: rootSource.src
3393
3031
  });
3394
3032
  chain.push(next.$ref);
3395
- extended = findNode(rootSource.document, parseRef(next.$ref).subpath ?? []);
3396
- if (!extended) logger.error({
3033
+ const extended = findNode(rootSource.document, parseRef(next.$ref).subpath ?? []);
3034
+ assert(extended, logger, {
3397
3035
  ...entry,
3398
3036
  message: "Could not resolve $extends",
3399
3037
  node: $extends,
3400
3038
  src: rootSource.src
3401
3039
  });
3402
- if (extended.type !== "Object") logger.error({
3040
+ assertObjectNode(extended, logger, {
3403
3041
  ...entry,
3404
3042
  message: "$extends must resolve to a group of tokens",
3405
3043
  node
@@ -3422,7 +3060,6 @@ function processTokens(rootSource, { config, logger, sourceByFilename }) {
3422
3060
  const tokens = {};
3423
3061
  const tokenIDs = [];
3424
3062
  const groups = {};
3425
- const isResolver = isLikelyResolver(rootSource.document);
3426
3063
  traverse(rootSource.document, { enter(node, _parent, rawPath) {
3427
3064
  if (node.type !== "Object") return;
3428
3065
  groupFromNode(node, {
@@ -3495,6 +3132,7 @@ function processTokens(rootSource, { config, logger, sourceByFilename }) {
3495
3132
  message: "Normalized values",
3496
3133
  timing: performance.now() - normalizeStart
3497
3134
  });
3135
+ if (config.alphabetize === false) return tokens;
3498
3136
  const sortStart = performance.now();
3499
3137
  const tokensSorted = {};
3500
3138
  tokenIDs.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
@@ -3599,36 +3237,440 @@ function findObject(dict, path, logger) {
3599
3237
  }
3600
3238
 
3601
3239
  //#endregion
3602
- //#region src/resolver/load.ts
3603
- /** Quick-parse input sources and find a resolver */
3604
- async function loadResolver(inputs, { config, logger, req, yamlToMomoa }) {
3605
- let resolverDoc;
3606
- let tokens = {};
3607
- const entry = {
3608
- group: "parser",
3609
- label: "init"
3610
- };
3611
- for (const input of inputs) {
3612
- let document;
3613
- if (typeof input.src === "string") if (maybeRawJSON(input.src)) document = toMomoa(input.src);
3614
- else if (yamlToMomoa) document = yamlToMomoa(input.src);
3615
- else logger.error({
3616
- ...entry,
3617
- message: `Install yaml-to-momoa package to parse YAML, and pass in as option, e.g.:
3618
-
3619
- import { bundle } from '@terrazzo/json-schema-tools';
3620
- import yamlToMomoa from 'yaml-to-momoa';
3621
-
3622
- bundle(yamlString, { yamlToMomoa });`
3623
- });
3624
- else if (input.src && typeof input.src === "object") document = toMomoa(JSON.stringify(input.src, void 0, 2));
3625
- else logger.error({
3626
- ...entry,
3627
- message: `Could not parse ${input.filename}. Is this valid JSON or YAML?`
3628
- });
3629
- if (!document || !isLikelyResolver(document)) continue;
3630
- if (inputs.length > 1) logger.error({
3631
- ...entry,
3240
+ //#region src/resolver/validate.ts
3241
+ /**
3242
+ * Determine whether this is likely a resolver
3243
+ * We use terms the word “likely” because this occurs before validation. Since
3244
+ * we may be dealing with a doc _intended_ to be a resolver, but may be lacking
3245
+ * some critical information, how can we determine intent? There’s a bit of
3246
+ * guesswork here, but we try and find a reasonable edge case where we sniff out
3247
+ * invalid DTCG syntax that a resolver doc would have.
3248
+ */
3249
+ function isLikelyResolver(doc) {
3250
+ if (doc.body.type !== "Object") return false;
3251
+ for (const member of doc.body.members) {
3252
+ if (member.name.type !== "String") continue;
3253
+ switch (member.name.value) {
3254
+ case "name":
3255
+ case "description":
3256
+ case "version":
3257
+ if (member.value.type === "String") return true;
3258
+ break;
3259
+ case "sets":
3260
+ case "modifiers":
3261
+ if (member.value.type !== "Object") continue;
3262
+ if (getObjMember(member.value, "description")?.type === "String") return true;
3263
+ if (member.name.value === "sets" && getObjMember(member.value, "sources")?.type === "Array") return true;
3264
+ else if (member.name.value === "modifiers") {
3265
+ const contexts = getObjMember(member.value, "contexts");
3266
+ if (contexts?.type === "Object" && contexts.members.some((m) => m.value.type === "Array")) return true;
3267
+ }
3268
+ break;
3269
+ case "resolutionOrder":
3270
+ if (member.value.type === "Array") return true;
3271
+ break;
3272
+ }
3273
+ }
3274
+ return false;
3275
+ }
3276
+ const MESSAGE_EXPECTED = {
3277
+ STRING: "Expected string.",
3278
+ OBJECT: "Expected object.",
3279
+ ARRAY: "Expected array."
3280
+ };
3281
+ /**
3282
+ * Validate a resolver document.
3283
+ * There’s a ton of boilerplate here, only to surface detailed code frames. Is there a better abstraction?
3284
+ */
3285
+ function validateResolver(node, { logger, src }) {
3286
+ const entry = {
3287
+ group: "parser",
3288
+ label: "resolver",
3289
+ src
3290
+ };
3291
+ if (node.body.type !== "Object") logger.error({
3292
+ ...entry,
3293
+ message: MESSAGE_EXPECTED.OBJECT,
3294
+ node
3295
+ });
3296
+ const errors = [];
3297
+ let hasVersion = false;
3298
+ let hasResolutionOrder = false;
3299
+ for (const member of node.body.members) {
3300
+ if (member.name.type !== "String") continue;
3301
+ switch (member.name.value) {
3302
+ case "name":
3303
+ case "description":
3304
+ if (member.value.type !== "String") errors.push({
3305
+ ...entry,
3306
+ message: MESSAGE_EXPECTED.STRING
3307
+ });
3308
+ break;
3309
+ case "version":
3310
+ hasVersion = true;
3311
+ if (member.value.type !== "String" || member.value.value !== "2025.10") errors.push({
3312
+ ...entry,
3313
+ message: `Expected "version" to be "2025.10".`,
3314
+ node: member.value
3315
+ });
3316
+ break;
3317
+ case "sets":
3318
+ case "modifiers":
3319
+ if (member.value.type !== "Object") errors.push({
3320
+ ...entry,
3321
+ message: MESSAGE_EXPECTED.OBJECT,
3322
+ node: member.value
3323
+ });
3324
+ else for (const item of member.value.members) if (item.value.type !== "Object") errors.push({
3325
+ ...entry,
3326
+ message: MESSAGE_EXPECTED.OBJECT,
3327
+ node: item.value
3328
+ });
3329
+ else {
3330
+ const validator = member.name.value === "sets" ? validateSet : validateModifier;
3331
+ errors.push(...validator(item.value, false, {
3332
+ logger,
3333
+ src
3334
+ }));
3335
+ }
3336
+ break;
3337
+ case "resolutionOrder":
3338
+ hasResolutionOrder = true;
3339
+ if (member.value.type !== "Array") errors.push({
3340
+ ...entry,
3341
+ message: MESSAGE_EXPECTED.ARRAY,
3342
+ node: member.value
3343
+ });
3344
+ else if (member.value.elements.length === 0) errors.push({
3345
+ ...entry,
3346
+ message: `"resolutionOrder" can’t be empty array.`,
3347
+ node: member.value
3348
+ });
3349
+ else for (const item of member.value.elements) if (item.value.type !== "Object") errors.push({
3350
+ ...entry,
3351
+ message: MESSAGE_EXPECTED.OBJECT,
3352
+ node: item.value
3353
+ });
3354
+ else {
3355
+ const itemMembers = getObjMembers(item.value);
3356
+ if (itemMembers.$ref?.type === "String") continue;
3357
+ if (itemMembers.type?.type === "String") if (itemMembers.type.value === "set") validateSet(item.value, true, {
3358
+ logger,
3359
+ src
3360
+ });
3361
+ else if (itemMembers.type.value === "modifier") validateModifier(item.value, true, {
3362
+ logger,
3363
+ src
3364
+ });
3365
+ else errors.push({
3366
+ ...entry,
3367
+ message: `Unknown type ${JSON.stringify(itemMembers.type.value)}`,
3368
+ node: itemMembers.type
3369
+ });
3370
+ if (itemMembers.sources?.type === "Array") validateSet(item.value, true, {
3371
+ logger,
3372
+ src
3373
+ });
3374
+ else if (itemMembers.contexts?.type === "Object") validateModifier(item.value, true, {
3375
+ logger,
3376
+ src
3377
+ });
3378
+ else if (itemMembers.name?.type === "String" || itemMembers.description?.type === "String") validateSet(item.value, true, {
3379
+ logger,
3380
+ src
3381
+ });
3382
+ }
3383
+ break;
3384
+ case "$defs":
3385
+ case "$extensions":
3386
+ if (member.value.type !== "Object") errors.push({
3387
+ ...entry,
3388
+ message: `Expected object`,
3389
+ node: member.value
3390
+ });
3391
+ break;
3392
+ case "$schema":
3393
+ case "$ref":
3394
+ if (member.value.type !== "String") errors.push({
3395
+ ...entry,
3396
+ message: `Expected string`,
3397
+ node: member.value
3398
+ });
3399
+ break;
3400
+ default:
3401
+ errors.push({
3402
+ ...entry,
3403
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
3404
+ node: member.name,
3405
+ src
3406
+ });
3407
+ break;
3408
+ }
3409
+ }
3410
+ if (!hasVersion) errors.push({
3411
+ ...entry,
3412
+ message: `Missing "version".`,
3413
+ node,
3414
+ src
3415
+ });
3416
+ if (!hasResolutionOrder) errors.push({
3417
+ ...entry,
3418
+ message: `Missing "resolutionOrder".`,
3419
+ node,
3420
+ src
3421
+ });
3422
+ if (errors.length) logger.error(...errors);
3423
+ }
3424
+ function validateSet(node, isInline = false, { src }) {
3425
+ const entry = {
3426
+ group: "parser",
3427
+ label: "resolver",
3428
+ src
3429
+ };
3430
+ const errors = [];
3431
+ let hasName = !isInline;
3432
+ let hasType = !isInline;
3433
+ let hasSources = false;
3434
+ for (const member of node.members) {
3435
+ if (member.name.type !== "String") continue;
3436
+ switch (member.name.value) {
3437
+ case "name":
3438
+ hasName = true;
3439
+ if (member.value.type !== "String") errors.push({
3440
+ ...entry,
3441
+ message: MESSAGE_EXPECTED.STRING,
3442
+ node: member.value
3443
+ });
3444
+ break;
3445
+ case "description":
3446
+ if (member.value.type !== "String") errors.push({
3447
+ ...entry,
3448
+ message: MESSAGE_EXPECTED.STRING,
3449
+ node: member.value
3450
+ });
3451
+ break;
3452
+ case "type":
3453
+ hasType = true;
3454
+ if (member.value.type !== "String") errors.push({
3455
+ ...entry,
3456
+ message: MESSAGE_EXPECTED.STRING,
3457
+ node: member.value
3458
+ });
3459
+ else if (member.value.value !== "set") errors.push({
3460
+ ...entry,
3461
+ message: "\"type\" must be \"set\"."
3462
+ });
3463
+ break;
3464
+ case "sources":
3465
+ hasSources = true;
3466
+ if (member.value.type !== "Array") errors.push({
3467
+ ...entry,
3468
+ message: MESSAGE_EXPECTED.ARRAY,
3469
+ node: member.value
3470
+ });
3471
+ else if (member.value.elements.length === 0) errors.push({
3472
+ ...entry,
3473
+ message: `"sources" can’t be empty array.`,
3474
+ node: member.value
3475
+ });
3476
+ else for (const source of member.value.elements) if (source.value.type !== "Object") errors.push({
3477
+ ...entry,
3478
+ message: MESSAGE_EXPECTED.OBJECT,
3479
+ node: source.value
3480
+ });
3481
+ break;
3482
+ case "$defs":
3483
+ case "$extensions":
3484
+ if (member.value.type !== "Object") errors.push({
3485
+ ...entry,
3486
+ message: `Expected object`,
3487
+ node: member.value
3488
+ });
3489
+ break;
3490
+ case "$ref":
3491
+ if (member.value.type !== "String") errors.push({
3492
+ ...entry,
3493
+ message: `Expected string`,
3494
+ node: member.value
3495
+ });
3496
+ break;
3497
+ default:
3498
+ errors.push({
3499
+ ...entry,
3500
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
3501
+ node: member.name
3502
+ });
3503
+ break;
3504
+ }
3505
+ }
3506
+ if (!hasName) errors.push({
3507
+ ...entry,
3508
+ message: `Missing "name".`,
3509
+ node
3510
+ });
3511
+ if (!hasType) errors.push({
3512
+ ...entry,
3513
+ message: `"type": "set" missing.`,
3514
+ node
3515
+ });
3516
+ if (!hasSources) errors.push({
3517
+ ...entry,
3518
+ message: `Missing "sources".`,
3519
+ node
3520
+ });
3521
+ return errors;
3522
+ }
3523
+ function validateModifier(node, isInline = false, { src }) {
3524
+ const errors = [];
3525
+ const entry = {
3526
+ group: "parser",
3527
+ label: "resolver",
3528
+ src
3529
+ };
3530
+ let hasName = !isInline;
3531
+ let hasType = !isInline;
3532
+ let hasContexts = false;
3533
+ for (const member of node.members) {
3534
+ if (member.name.type !== "String") continue;
3535
+ switch (member.name.value) {
3536
+ case "name":
3537
+ hasName = true;
3538
+ if (member.value.type !== "String") errors.push({
3539
+ ...entry,
3540
+ message: MESSAGE_EXPECTED.STRING,
3541
+ node: member.value
3542
+ });
3543
+ break;
3544
+ case "description":
3545
+ if (member.value.type !== "String") errors.push({
3546
+ ...entry,
3547
+ message: MESSAGE_EXPECTED.STRING,
3548
+ node: member.value
3549
+ });
3550
+ break;
3551
+ case "type":
3552
+ hasType = true;
3553
+ if (member.value.type !== "String") errors.push({
3554
+ ...entry,
3555
+ message: MESSAGE_EXPECTED.STRING,
3556
+ node: member.value
3557
+ });
3558
+ else if (member.value.value !== "modifier") errors.push({
3559
+ ...entry,
3560
+ message: "\"type\" must be \"modifier\"."
3561
+ });
3562
+ break;
3563
+ case "contexts":
3564
+ hasContexts = true;
3565
+ if (member.value.type !== "Object") errors.push({
3566
+ ...entry,
3567
+ message: MESSAGE_EXPECTED.OBJECT,
3568
+ node: member.value
3569
+ });
3570
+ else if (member.value.members.length === 0) errors.push({
3571
+ ...entry,
3572
+ message: `"contexts" can’t be empty object.`,
3573
+ node: member.value
3574
+ });
3575
+ else for (const context of member.value.members) if (context.value.type !== "Array") errors.push({
3576
+ ...entry,
3577
+ message: MESSAGE_EXPECTED.ARRAY,
3578
+ node: context.value
3579
+ });
3580
+ else for (const source of context.value.elements) if (source.value.type !== "Object") errors.push({
3581
+ ...entry,
3582
+ message: MESSAGE_EXPECTED.OBJECT,
3583
+ node: source.value
3584
+ });
3585
+ break;
3586
+ case "default":
3587
+ if (member.value.type !== "String") errors.push({
3588
+ ...entry,
3589
+ message: `Expected string`,
3590
+ node: member.value
3591
+ });
3592
+ else {
3593
+ const contexts = getObjMember(node, "contexts");
3594
+ if (!contexts || !getObjMember(contexts, member.value.value)) errors.push({
3595
+ ...entry,
3596
+ message: "Invalid default context",
3597
+ node: member.value
3598
+ });
3599
+ }
3600
+ break;
3601
+ case "$defs":
3602
+ case "$extensions":
3603
+ if (member.value.type !== "Object") errors.push({
3604
+ ...entry,
3605
+ message: `Expected object`,
3606
+ node: member.value
3607
+ });
3608
+ break;
3609
+ case "$ref":
3610
+ if (member.value.type !== "String") errors.push({
3611
+ ...entry,
3612
+ message: `Expected string`,
3613
+ node: member.value
3614
+ });
3615
+ break;
3616
+ default:
3617
+ errors.push({
3618
+ ...entry,
3619
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
3620
+ node: member.name
3621
+ });
3622
+ break;
3623
+ }
3624
+ }
3625
+ if (!hasName) errors.push({
3626
+ ...entry,
3627
+ message: `Missing "name".`,
3628
+ node
3629
+ });
3630
+ if (!hasType) errors.push({
3631
+ ...entry,
3632
+ message: `"type": "modifier" missing.`,
3633
+ node
3634
+ });
3635
+ if (!hasContexts) errors.push({
3636
+ ...entry,
3637
+ message: `Missing "contexts".`,
3638
+ node
3639
+ });
3640
+ return errors;
3641
+ }
3642
+
3643
+ //#endregion
3644
+ //#region src/resolver/load.ts
3645
+ /** Quick-parse input sources and find a resolver */
3646
+ async function loadResolver(inputs, { config, logger, req, yamlToMomoa }) {
3647
+ let resolverDoc;
3648
+ let tokens = {};
3649
+ const entry = {
3650
+ group: "parser",
3651
+ label: "init"
3652
+ };
3653
+ for (const input of inputs) {
3654
+ let document;
3655
+ if (typeof input.src === "string") if (maybeRawJSON(input.src)) document = toMomoa(input.src);
3656
+ else if (yamlToMomoa) document = yamlToMomoa(input.src);
3657
+ else logger.error({
3658
+ ...entry,
3659
+ message: `Install yaml-to-momoa package to parse YAML, and pass in as option, e.g.:
3660
+
3661
+ import { bundle } from '@terrazzo/json-schema-tools';
3662
+ import yamlToMomoa from 'yaml-to-momoa';
3663
+
3664
+ bundle(yamlString, { yamlToMomoa });`
3665
+ });
3666
+ else if (input.src && typeof input.src === "object") document = toMomoa(JSON.stringify(input.src, void 0, 2));
3667
+ else logger.error({
3668
+ ...entry,
3669
+ message: `Could not parse ${input.filename}. Is this valid JSON or YAML?`
3670
+ });
3671
+ if (!document || !isLikelyResolver(document)) continue;
3672
+ if (inputs.length > 1) logger.error({
3673
+ ...entry,
3632
3674
  message: `Resolver must be the only input, found ${inputs.length} sources.`
3633
3675
  });
3634
3676
  resolverDoc = document;
@@ -3687,8 +3729,8 @@ function createResolver(resolverSource, { config, logger, sources }) {
3687
3729
  ...inputDefaults,
3688
3730
  ...inputRaw
3689
3731
  };
3690
- const inputKey = makeInputKey(input);
3691
- if (resolverCache[inputKey]) return resolverCache[inputKey];
3732
+ const permutationID = getPermutationID(input);
3733
+ if (resolverCache[permutationID]) return resolverCache[permutationID];
3692
3734
  for (const item of resolverSource.resolutionOrder) switch (item.type) {
3693
3735
  case "set":
3694
3736
  for (const s of item.sources) tokensRaw = merge(tokensRaw, s);
@@ -3697,8 +3739,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3697
3739
  const context = input[item.name];
3698
3740
  const sources$1 = item.contexts[context];
3699
3741
  if (!sources$1) logger.error({
3700
- group: "parser",
3701
- label: "resolver",
3742
+ group: "resolver",
3702
3743
  message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`
3703
3744
  });
3704
3745
  for (const s of sources$1 ?? []) tokensRaw = merge(tokensRaw, s);
@@ -3715,9 +3756,10 @@ function createResolver(resolverSource, { config, logger, sources }) {
3715
3756
  config,
3716
3757
  logger,
3717
3758
  sourceByFilename: { [resolverSource._source.filename.href]: rootSource },
3759
+ isResolver: true,
3718
3760
  sources
3719
3761
  });
3720
- resolverCache[inputKey] = tokens;
3762
+ resolverCache[permutationID] = tokens;
3721
3763
  return tokens;
3722
3764
  },
3723
3765
  source: resolverSource,
@@ -3725,17 +3767,41 @@ function createResolver(resolverSource, { config, logger, sources }) {
3725
3767
  if (!allPermutations.length) allPermutations.push(...calculatePermutations(Object.entries(validContexts)));
3726
3768
  return allPermutations;
3727
3769
  },
3728
- isValidInput(input) {
3770
+ isValidInput(input, throwError = false) {
3729
3771
  if (!input || typeof input !== "object") logger.error({
3730
- group: "parser",
3731
- label: "resolver",
3772
+ group: "resolver",
3732
3773
  message: `Invalid input: ${JSON.stringify(input)}.`
3733
3774
  });
3734
- if (!Object.keys(input).every((k) => k in validContexts)) return false;
3775
+ for (const k of Object.keys(input)) if (!(k in validContexts)) {
3776
+ if (throwError) logger.error({
3777
+ group: "resolver",
3778
+ message: `No such modifier ${JSON.stringify(k)}`
3779
+ });
3780
+ return false;
3781
+ }
3735
3782
  for (const [name, contexts] of Object.entries(validContexts)) if (name in input) {
3736
- if (!contexts.includes(input[name])) return false;
3737
- } else if (!(name in inputDefaults)) return false;
3783
+ if (!contexts.includes(input[name])) {
3784
+ if (throwError) logger.error({
3785
+ group: "resolver",
3786
+ message: `Modifier "${name}" has no context ${JSON.stringify(input[name])}.`
3787
+ });
3788
+ return false;
3789
+ }
3790
+ } else if (!(name in inputDefaults)) {
3791
+ if (throwError) logger.error({
3792
+ group: "resolver",
3793
+ message: `Modifier "${name}" missing value (no default set).`
3794
+ });
3795
+ return false;
3796
+ }
3738
3797
  return true;
3798
+ },
3799
+ getPermutationID(input) {
3800
+ this.isValidInput(input, true);
3801
+ return getPermutationID({
3802
+ ...inputDefaults,
3803
+ ...input
3804
+ });
3739
3805
  }
3740
3806
  };
3741
3807
  }