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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,42 +1,12 @@
1
1
  import wcmatch from "wildcard-match";
2
2
  import * as momoa from "@humanwhocodes/momoa";
3
- import { evaluate } from "@humanwhocodes/momoa";
4
3
  import pc from "picocolors";
5
4
  import { merge } from "merge-anything";
6
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";
7
6
  import { clampChroma, wcagContrast } from "culori";
8
- import { bundle, getObjMember, getObjMembers, isPure$ref, maybeRawJSON, parseRef, replaceNode, traverseAsync } from "@terrazzo/json-schema-tools";
7
+ import { camelCase, kebabCase, pascalCase, snakeCase } from "scule";
8
+ import { bundle, getObjMember, getObjMembers, isPure$ref, maybeRawJSON, parseRef, replaceNode, traverse } from "@terrazzo/json-schema-tools";
9
9
 
10
- //#region rolldown:runtime
11
- var __create = Object.create;
12
- var __defProp = Object.defineProperty;
13
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
14
- var __getOwnPropNames = Object.getOwnPropertyNames;
15
- var __getProtoOf = Object.getPrototypeOf;
16
- var __hasOwnProp = Object.prototype.hasOwnProperty;
17
- var __commonJS = (cb, mod) => function() {
18
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
- };
20
- var __copyProps = (to, from, except, desc) => {
21
- if (from && typeof from === "object" || typeof from === "function") {
22
- for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
23
- key = keys[i];
24
- if (!__hasOwnProp.call(to, key) && key !== except) {
25
- __defProp(to, key, {
26
- get: ((k) => from[k]).bind(null, key),
27
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
28
- });
29
- }
30
- }
31
- }
32
- return to;
33
- };
34
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
35
- value: mod,
36
- enumerable: true
37
- }) : target, mod));
38
-
39
- //#endregion
40
10
  //#region src/lib/code-frame.ts
41
11
  /**
42
12
  * Extract what lines should be marked and highlighted.
@@ -146,6 +116,7 @@ function formatMessage(entry, severity) {
146
116
  let message = entry.message;
147
117
  message = `[${entry.group}${entry.label ? `:${entry.label}` : ""}] ${message}`;
148
118
  if (severity in MESSAGE_COLOR) message = MESSAGE_COLOR[severity](message);
119
+ if (typeof entry.timing === "number") message = `${message} ${formatTiming(entry.timing)}`;
149
120
  if (entry.node) {
150
121
  const start = entry.node?.loc?.start ?? {
151
122
  line: 0,
@@ -211,12 +182,7 @@ var Logger = class {
211
182
  if (this.debugScope !== "*" && !wcmatch(this.debugScope)(debugPrefix)) return;
212
183
  message.replace(/\[config[^\]]+\]/, (match) => pc.green(match)).replace(/\[parser[^\]]+\]/, (match) => pc.magenta(match)).replace(/\[lint[^\]]+\]/, (match) => pc.yellow(match)).replace(/\[plugin[^\]]+\]/, (match) => pc.cyan(match));
213
184
  message = `${pc.dim(timeFormatter.format(performance.now()))} ${message}`;
214
- if (typeof entry.timing === "number") {
215
- let timing = "";
216
- if (entry.timing < 1e3) timing = `${Math.round(entry.timing * 100) / 100}ms`;
217
- else if (entry.timing < 6e4) timing = `${Math.round(entry.timing * 100) / 1e5}s`;
218
- message = `${message}${timing ? pc.dim(` [${timing}]`) : ""}`;
219
- }
185
+ if (typeof entry.timing === "number") message = `${message} ${formatTiming(entry.timing)}`;
220
186
  console.log(message);
221
187
  }
222
188
  }
@@ -230,6 +196,13 @@ var Logger = class {
230
196
  };
231
197
  }
232
198
  };
199
+ function formatTiming(timing) {
200
+ let output = "";
201
+ if (timing < 1e3) output = `${Math.round(timing * 100) / 100}ms`;
202
+ else if (timing < 6e4) output = `${Math.round(timing) / 1e3}s`;
203
+ else output = `${Math.round(timing / 1e3) / 60}m`;
204
+ return pc.dim(`[${output}]`);
205
+ }
233
206
  var TokensJSONError = class extends Error {
234
207
  constructor(message) {
235
208
  super(message);
@@ -258,7 +231,7 @@ function validateTransformParams({ params, logger, pluginName }) {
258
231
  });
259
232
  }
260
233
  /** Run build stage */
261
- async function build(tokens, { sources, logger = new Logger(), config }) {
234
+ async function build(tokens, { resolver, sources, logger = new Logger(), config }) {
262
235
  const formats = {};
263
236
  const result = { outputFiles: [] };
264
237
  function getTransforms(params) {
@@ -321,7 +294,8 @@ async function build(tokens, { sources, logger = new Logger(), config }) {
321
294
  formats[params.format][foundTokenI].value = cleanValue;
322
295
  formats[params.format][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
323
296
  }
324
- }
297
+ },
298
+ resolver
325
299
  });
326
300
  transformsLocked = true;
327
301
  logger.debug({
@@ -339,6 +313,7 @@ async function build(tokens, { sources, logger = new Logger(), config }) {
339
313
  tokens,
340
314
  sources,
341
315
  getTransforms,
316
+ resolver,
342
317
  outputFile(filename, contents) {
343
318
  const resolved = new URL(filename, config.outDir);
344
319
  if (result.outputFiles.some((f) => new URL(f.filename, config.outDir).href === resolved.href)) logger.error({
@@ -546,76 +521,6 @@ const rule$24 = {
546
521
  };
547
522
  var colorspace_default = rule$24;
548
523
 
549
- //#endregion
550
- //#region ../../node_modules/.pnpm/scule@1.3.0/node_modules/scule/dist/index.mjs
551
- const NUMBER_CHAR_RE = /\d/;
552
- const STR_SPLITTERS = [
553
- "-",
554
- "_",
555
- "/",
556
- "."
557
- ];
558
- function isUppercase(char = "") {
559
- if (NUMBER_CHAR_RE.test(char)) return;
560
- return char !== char.toLowerCase();
561
- }
562
- function splitByCase(str, separators) {
563
- const splitters = separators ?? STR_SPLITTERS;
564
- const parts = [];
565
- if (!str || typeof str !== "string") return parts;
566
- let buff = "";
567
- let previousUpper;
568
- let previousSplitter;
569
- for (const char of str) {
570
- const isSplitter = splitters.includes(char);
571
- if (isSplitter === true) {
572
- parts.push(buff);
573
- buff = "";
574
- previousUpper = void 0;
575
- continue;
576
- }
577
- const isUpper = isUppercase(char);
578
- if (previousSplitter === false) {
579
- if (previousUpper === false && isUpper === true) {
580
- parts.push(buff);
581
- buff = char;
582
- previousUpper = isUpper;
583
- continue;
584
- }
585
- if (previousUpper === true && isUpper === false && buff.length > 1) {
586
- const lastChar = buff.at(-1);
587
- parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
588
- buff = lastChar + char;
589
- previousUpper = isUpper;
590
- continue;
591
- }
592
- }
593
- buff += char;
594
- previousUpper = isUpper;
595
- previousSplitter = isSplitter;
596
- }
597
- parts.push(buff);
598
- return parts;
599
- }
600
- function upperFirst(str) {
601
- return str ? str[0].toUpperCase() + str.slice(1) : "";
602
- }
603
- function lowerFirst(str) {
604
- return str ? str[0].toLowerCase() + str.slice(1) : "";
605
- }
606
- function pascalCase(str, opts) {
607
- return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
608
- }
609
- function camelCase(str, opts) {
610
- return lowerFirst(pascalCase(str || "", opts));
611
- }
612
- function kebabCase(str, joiner) {
613
- return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
614
- }
615
- function snakeCase(str) {
616
- return kebabCase(str || "", "_");
617
- }
618
-
619
524
  //#endregion
620
525
  //#region src/lint/plugin-core/rules/consistent-naming.ts
621
526
  const CONSISTENT_NAMING = "core/consistent-naming";
@@ -2206,7 +2111,7 @@ function defineConfig(rawConfig, { logger = new Logger(), cwd } = {}) {
2206
2111
  config,
2207
2112
  logger
2208
2113
  });
2209
- for (const plugin of config.plugins) plugin.config?.({ ...config });
2114
+ for (const plugin of config.plugins) plugin.config?.({ ...config }, { logger });
2210
2115
  logger.debug({
2211
2116
  group: "parser",
2212
2117
  label: "config",
@@ -2375,7 +2280,6 @@ function normalizeLint({ config, logger }) {
2375
2280
  message: `Expected string or number, received ${JSON.stringify(value)}`
2376
2281
  });
2377
2282
  }
2378
- for (const [id, severity] of Object.entries(RECOMMENDED_CONFIG)) if (!(id in config.lint.rules)) config.lint.rules[id] = severity;
2379
2283
  }
2380
2284
  } else config.lint = {
2381
2285
  build: { enabled: true },
@@ -2489,40 +2393,8 @@ async function lintRunner({ tokens, filename, config = {}, sources, logger }) {
2489
2393
  });
2490
2394
  }
2491
2395
 
2492
- //#endregion
2493
- //#region ../../node_modules/.pnpm/fast-deep-equal@3.1.3/node_modules/fast-deep-equal/index.js
2494
- var require_fast_deep_equal = /* @__PURE__ */ __commonJS({ "../../node_modules/.pnpm/fast-deep-equal@3.1.3/node_modules/fast-deep-equal/index.js": ((exports, module) => {
2495
- module.exports = function equal(a, b) {
2496
- if (a === b) return true;
2497
- if (a && b && typeof a == "object" && typeof b == "object") {
2498
- if (a.constructor !== b.constructor) return false;
2499
- var length, i, keys;
2500
- if (Array.isArray(a)) {
2501
- length = a.length;
2502
- if (length != b.length) return false;
2503
- for (i = length; i-- !== 0;) if (!equal(a[i], b[i])) return false;
2504
- return true;
2505
- }
2506
- if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
2507
- if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
2508
- if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
2509
- keys = Object.keys(a);
2510
- length = keys.length;
2511
- if (length !== Object.keys(b).length) return false;
2512
- for (i = length; i-- !== 0;) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
2513
- for (i = length; i-- !== 0;) {
2514
- var key = keys[i];
2515
- if (!equal(a[key], b[key])) return false;
2516
- }
2517
- return true;
2518
- }
2519
- return a !== a && b !== b;
2520
- };
2521
- }) });
2522
-
2523
2396
  //#endregion
2524
2397
  //#region src/lib/momoa.ts
2525
- var import_fast_deep_equal = /* @__PURE__ */ __toESM(require_fast_deep_equal(), 1);
2526
2398
  /** Momoa’s default parser, with preferred settings. */
2527
2399
  function toMomoa(srcRaw) {
2528
2400
  return momoa.parse(typeof srcRaw === "string" ? srcRaw : JSON.stringify(srcRaw, void 0, 2), {
@@ -2532,6 +2404,32 @@ function toMomoa(srcRaw) {
2532
2404
  });
2533
2405
  }
2534
2406
 
2407
+ //#endregion
2408
+ //#region src/lib/resolver-utils.ts
2409
+ /**
2410
+ * If tokens are found inside a resolver, strip out the resolver paths (don’t
2411
+ * include "sets"/"modifiers" in the token ID etc.)
2412
+ */
2413
+ function filterResolverPaths(path) {
2414
+ switch (path[0]) {
2415
+ case "sets": return path.slice(4);
2416
+ case "modifiers": return path.slice(5);
2417
+ case "resolutionOrder":
2418
+ switch (path[2]) {
2419
+ case "sources": return path.slice(4);
2420
+ case "contexts": return path.slice(5);
2421
+ }
2422
+ break;
2423
+ }
2424
+ return path;
2425
+ }
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 }))));
2431
+ }
2432
+
2535
2433
  //#endregion
2536
2434
  //#region src/resolver/validate.ts
2537
2435
  /**
@@ -2550,7 +2448,7 @@ function isLikelyResolver(doc) {
2550
2448
  case "name":
2551
2449
  case "description":
2552
2450
  case "version":
2553
- if (member.name.type === "String") return true;
2451
+ if (member.value.type === "String") return true;
2554
2452
  break;
2555
2453
  case "sets":
2556
2454
  case "modifiers":
@@ -2685,6 +2583,7 @@ function validateResolver(node, { logger, src }) {
2685
2583
  node: member.value
2686
2584
  });
2687
2585
  break;
2586
+ case "$schema":
2688
2587
  case "$ref":
2689
2588
  if (member.value.type !== "String") errors.push({
2690
2589
  ...entry,
@@ -2868,6 +2767,21 @@ function validateModifier(node, isInline = false, { src }) {
2868
2767
  node: context.value
2869
2768
  });
2870
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;
2871
2785
  case "$defs":
2872
2786
  case "$extensions":
2873
2787
  if (member.value.type !== "Object") errors.push({
@@ -2910,204 +2824,6 @@ function validateModifier(node, isInline = false, { src }) {
2910
2824
  return errors;
2911
2825
  }
2912
2826
 
2913
- //#endregion
2914
- //#region src/resolver/normalize.ts
2915
- /** Normalize resolver (assuming it’s been validated) */
2916
- async function normalizeResolver(node, { filename, req, src, logger, yamlToMomoa }) {
2917
- const resolverSource = evaluate(node);
2918
- const resolutionOrder = getObjMember(node.body, "resolutionOrder");
2919
- return {
2920
- name: resolverSource.name,
2921
- version: resolverSource.version,
2922
- description: resolverSource.description,
2923
- sets: resolverSource.sets,
2924
- modifiers: resolverSource.modifiers,
2925
- resolutionOrder: await Promise.all(resolutionOrder.elements.map(async (element, i) => {
2926
- const layer = element.value;
2927
- const members = getObjMembers(layer);
2928
- let item = layer;
2929
- if (members.$ref) {
2930
- const entry = {
2931
- group: "parser",
2932
- label: "init",
2933
- node: members.$ref,
2934
- src
2935
- };
2936
- const { url, subpath } = parseRef(members.$ref.value);
2937
- if (url === ".") if (!subpath?.[0]) logger.error({
2938
- ...entry,
2939
- message: "$ref can’t refer to the root document."
2940
- });
2941
- else if (subpath[0] !== "sets" && subpath[0] !== "modifiers") logger.error({
2942
- ...entry,
2943
- message: "Local $ref in resolutionOrder must point to either #/sets/[set] or #/modifiers/[modifiers]."
2944
- });
2945
- else {
2946
- const resolvedItem = resolverSource[subpath[0]]?.[subpath[1]];
2947
- if (!resolvedItem) logger.error({
2948
- ...entry,
2949
- message: "Invalid $ref"
2950
- });
2951
- else item = {
2952
- type: subpath[0] === "sets" ? "set" : "modifier",
2953
- ...resolvedItem
2954
- };
2955
- }
2956
- else {
2957
- const result = await bundle([{
2958
- filename: new URL(url, filename),
2959
- src: resolverSource.resolutionOrder[i]
2960
- }], {
2961
- req,
2962
- yamlToMomoa
2963
- });
2964
- if (result.document.body.type === "Object") {
2965
- const type = getObjMember(result.document.body, "type");
2966
- if (type?.type === "String" && type.value === "set") {
2967
- validateSet(result.document.body, true, src);
2968
- item = evaluate(result.document.body);
2969
- } else if (type?.type === "String" && type.value === "modifier") {
2970
- validateModifier(result.document.body, true, src);
2971
- item = evaluate(result.document.body);
2972
- }
2973
- }
2974
- logger.error({
2975
- ...entry,
2976
- message: "$ref did not resolve to a valid Set or Modifier."
2977
- });
2978
- }
2979
- }
2980
- return evaluate((await bundle([{
2981
- filename,
2982
- src: item
2983
- }], {
2984
- req,
2985
- yamlToMomoa
2986
- })).document.body);
2987
- }))
2988
- };
2989
- }
2990
-
2991
- //#endregion
2992
- //#region src/resolver/load.ts
2993
- /** Quick-parse input sources and find a resolver */
2994
- async function loadResolver(inputs, { logger, req, yamlToMomoa }) {
2995
- let resolverDoc;
2996
- const entry = {
2997
- group: "parser",
2998
- label: "init"
2999
- };
3000
- for (const input of inputs) {
3001
- let document;
3002
- if (typeof input.src === "string") if (maybeRawJSON(input.src)) document = toMomoa(input.src);
3003
- else if (yamlToMomoa) document = yamlToMomoa(input.src);
3004
- else logger.error({
3005
- ...entry,
3006
- message: `Install yaml-to-momoa package to parse YAML, and pass in as option, e.g.:
3007
-
3008
- import { bundle } from '@terrazzo/json-schema-tools';
3009
- import yamlToMomoa from 'yaml-to-momoa';
3010
-
3011
- bundle(yamlString, { yamlToMomoa });`
3012
- });
3013
- else if (input.src && typeof input.src === "object") document = toMomoa(JSON.stringify(input.src, void 0, 2));
3014
- else logger.error({
3015
- ...entry,
3016
- message: `Could not parse ${input.filename}. Is this valid JSON or YAML?`
3017
- });
3018
- if (!document || !isLikelyResolver(document)) continue;
3019
- if (inputs.length > 1) logger.error({
3020
- ...entry,
3021
- message: `Resolver must be the only input, found ${inputs.length} sources.`
3022
- });
3023
- resolverDoc = document;
3024
- break;
3025
- }
3026
- if (resolverDoc) {
3027
- validateResolver(resolverDoc, {
3028
- logger,
3029
- src: inputs[0].src
3030
- });
3031
- return createResolver(await normalizeResolver(resolverDoc, {
3032
- filename: inputs[0].filename,
3033
- logger,
3034
- req,
3035
- src: inputs[0].src,
3036
- yamlToMomoa
3037
- }), { logger });
3038
- }
3039
- }
3040
- /** Create an interface to resolve permutations */
3041
- function createResolver(resolverSource, { logger }) {
3042
- const inputDefaults = {};
3043
- const modifierPermutations = [];
3044
- for (const [name, m] of Object.entries(resolverSource.modifiers ?? {})) {
3045
- if (typeof m.default === "string") inputDefaults[name] = m.default;
3046
- modifierPermutations.push([name, Object.keys(m.contexts)]);
3047
- }
3048
- for (const m of resolverSource.resolutionOrder) {
3049
- if (!("type" in m) || m.type !== "modifier") continue;
3050
- if (typeof m.default === "string") inputDefaults[m.name] = m.default;
3051
- modifierPermutations.push([m.name, Object.keys(m.contexts)]);
3052
- }
3053
- const permutations = calculatePermutations(modifierPermutations);
3054
- return {
3055
- apply(inputRaw) {
3056
- let tokens = {};
3057
- const input = {
3058
- ...inputDefaults,
3059
- ...inputRaw
3060
- };
3061
- for (const item of resolverSource.resolutionOrder) switch (item.type) {
3062
- case "set":
3063
- for (const s of item.sources) tokens = merge(tokens, s);
3064
- break;
3065
- case "modifier": {
3066
- const context = input[item.name];
3067
- const sources = item.contexts[context];
3068
- if (!sources) logger.error({
3069
- group: "parser",
3070
- label: "resolver",
3071
- message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`
3072
- });
3073
- for (const s of sources ?? []) tokens = merge(tokens, s);
3074
- break;
3075
- }
3076
- }
3077
- return tokens;
3078
- },
3079
- source: resolverSource,
3080
- permutations,
3081
- isValidInput(inputRaw) {
3082
- if (!inputRaw || typeof inputRaw !== "object") logger.error({
3083
- group: "parser",
3084
- label: "resolver",
3085
- message: `Invalid input: ${JSON.stringify(inputRaw)}.`
3086
- });
3087
- const input = {
3088
- ...inputDefaults,
3089
- ...inputRaw
3090
- };
3091
- return permutations.findIndex((p) => (0, import_fast_deep_equal.default)(input, p)) !== -1;
3092
- }
3093
- };
3094
- }
3095
- /** Calculate all permutations */
3096
- function calculatePermutations(options) {
3097
- const permutationCount = [1];
3098
- for (const [_name, contexts] of options) permutationCount.push(contexts.length * (permutationCount.at(-1) || 1));
3099
- const permutations = [];
3100
- for (let i = 0; i < permutationCount.at(-1); i++) {
3101
- const input = {};
3102
- for (let j = 0; j < options.length; j++) {
3103
- const [name, contexts] = options[j];
3104
- input[name] = contexts[Math.floor(i / permutationCount[j]) % contexts.length];
3105
- }
3106
- permutations.push(input);
3107
- }
3108
- return permutations.length ? permutations : [{}];
3109
- }
3110
-
3111
2827
  //#endregion
3112
2828
  //#region src/parse/normalize.ts
3113
2829
  /**
@@ -3227,14 +2943,14 @@ function aliasToRef(alias, mode) {
3227
2943
  }
3228
2944
  /** Generate a TokenNormalized from a Momoa node */
3229
2945
  function tokenFromNode(node, { groups, path, source, ignore }) {
3230
- if (!(node.type === "Object" && getObjMember(node, "$value") && !path.includes("$extensions"))) return;
2946
+ if (!(node.type === "Object" && !!getObjMember(node, "$value") && !path.includes("$extensions"))) return;
3231
2947
  const jsonID = `#/${path.join("/")}`;
3232
2948
  const id = path.join(".");
3233
2949
  const originalToken = momoa.evaluate(node);
3234
2950
  const group = groups[`#/${path.slice(0, -1).join("/")}`];
3235
2951
  if (group?.tokens && !group.tokens.includes(id)) group.tokens.push(id);
3236
2952
  const nodeSource = {
3237
- filename: source.filename?.href,
2953
+ filename: source.filename.href,
3238
2954
  node
3239
2955
  };
3240
2956
  const token = {
@@ -3244,6 +2960,7 @@ function tokenFromNode(node, { groups, path, source, ignore }) {
3244
2960
  $deprecated: originalToken.$deprecated ?? group.$deprecated ?? void 0,
3245
2961
  $value: originalToken.$value,
3246
2962
  $extensions: originalToken.$extensions || void 0,
2963
+ $extends: originalToken.$extends || void 0,
3247
2964
  aliasChain: void 0,
3248
2965
  aliasedBy: void 0,
3249
2966
  aliasOf: void 0,
@@ -3271,7 +2988,7 @@ function tokenFromNode(node, { groups, path, source, ignore }) {
3271
2988
  const $extensions = getObjMember(node, "$extensions");
3272
2989
  if ($extensions) {
3273
2990
  const modeNode = getObjMember($extensions, "mode");
3274
- for (const mode of Object.keys(token.$extensions.mode)) {
2991
+ for (const mode of Object.keys(token.$extensions.mode ?? {})) {
3275
2992
  const modeValue = token.$extensions.mode[mode];
3276
2993
  token.mode[mode] = {
3277
2994
  $value: modeValue,
@@ -3579,80 +3296,28 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
3579
3296
  }
3580
3297
 
3581
3298
  //#endregion
3582
- //#region src/parse/load.ts
3583
- /** Load from multiple entries, while resolving remote files */
3584
- async function loadSources(inputs, { config, logger, req, continueOnError, yamlToMomoa, transform }) {
3299
+ //#region src/parse/process.ts
3300
+ function processTokens(rootSource, { config, logger, sourceByFilename, refMap }) {
3585
3301
  const entry = {
3586
3302
  group: "parser",
3587
3303
  label: "init"
3588
3304
  };
3589
- const firstLoad = performance.now();
3590
- let document = {};
3591
- /** The original user inputs, in original order, with parsed ASTs */
3592
- const sources = inputs.map((input, i) => ({
3593
- ...input,
3594
- document: {},
3595
- filename: input.filename || new URL(`virtual:${i}`)
3596
- }));
3597
- /** The sources array, indexed by filename */
3598
- let sourceByFilename = {};
3599
- /** Mapping of all final $ref resolutions. This will be used to generate the graph later. */
3600
- let refMap = {};
3601
- try {
3602
- const result = await bundle(sources, {
3603
- req,
3604
- parse: transform ? transformer(transform) : void 0,
3605
- yamlToMomoa
3606
- });
3607
- document = result.document;
3608
- sourceByFilename = result.sources;
3609
- refMap = result.refMap;
3610
- for (const [filename, source] of Object.entries(result.sources)) {
3611
- const i = sources.findIndex((s) => s.filename.href === filename);
3612
- if (i === -1) sources.push(source);
3613
- else {
3614
- sources[i].src = source.src;
3615
- sources[i].document = source.document;
3616
- }
3617
- }
3618
- } catch (err) {
3619
- let src = sources.find((s) => s.filename.href === err.filename)?.src;
3620
- if (src && typeof src !== "string") src = JSON.stringify(src, void 0, 2);
3621
- logger.error({
3622
- ...entry,
3623
- continueOnError,
3624
- message: err.message,
3625
- node: err.node,
3626
- src
3627
- });
3628
- }
3629
- logger.debug({
3630
- ...entry,
3631
- message: `JSON loaded`,
3632
- timing: performance.now() - firstLoad
3633
- });
3634
- const artificialSource = {
3635
- src: momoa.print(document, { indent: 2 }),
3636
- document
3637
- };
3638
- const firstPass = performance.now();
3639
- const tokens = {};
3640
- const tokenIDs = [];
3641
- const groups = {};
3642
- await traverseAsync(document, { async enter(node, _parent, path) {
3643
- if (node.type !== "Object") return;
3644
- groupFromNode(node, {
3645
- path,
3646
- groups
3305
+ const firstPass = performance.now();
3306
+ const tokens = {};
3307
+ const tokenIDs = [];
3308
+ const groups = {};
3309
+ const isResolver = isLikelyResolver(rootSource.document);
3310
+ traverse(rootSource.document, { enter(node, _parent, rawPath) {
3311
+ if (node.type !== "Object") return;
3312
+ groupFromNode(node, {
3313
+ path: isResolver ? filterResolverPaths(rawPath) : rawPath,
3314
+ groups
3647
3315
  });
3648
3316
  const token = tokenFromNode(node, {
3649
3317
  groups,
3650
3318
  ignore: config.ignore,
3651
- path,
3652
- source: {
3653
- src: artificialSource,
3654
- document
3655
- }
3319
+ path: isResolver ? filterResolverPaths(rawPath) : rawPath,
3320
+ source: rootSource
3656
3321
  });
3657
3322
  if (token) {
3658
3323
  tokenIDs.push(token.jsonID);
@@ -3665,7 +3330,7 @@ async function loadSources(inputs, { config, logger, req, continueOnError, yamlT
3665
3330
  timing: performance.now() - firstPass
3666
3331
  });
3667
3332
  const secondPass = performance.now();
3668
- for (const source of Object.values(sourceByFilename)) await traverseAsync(source.document, { async enter(node, _parent, path) {
3333
+ for (const source of Object.values(sourceByFilename)) traverse(source.document, { enter(node, _parent, path) {
3669
3334
  if (node.type !== "Object") return;
3670
3335
  const tokenRawValues = tokenRawValuesFromNode(node, {
3671
3336
  filename: source.filename.href,
@@ -3722,8 +3387,378 @@ async function loadSources(inputs, { config, logger, req, continueOnError, yamlT
3722
3387
  tokensSorted[id] = tokens[path];
3723
3388
  }
3724
3389
  for (const group of Object.values(groups)) group.tokens.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3390
+ return tokensSorted;
3391
+ }
3392
+
3393
+ //#endregion
3394
+ //#region src/resolver/normalize.ts
3395
+ /** Normalize resolver (assuming it’s been validated) */
3396
+ async function normalizeResolver(node, { filename, req, src, logger, yamlToMomoa }) {
3397
+ const resolverSource = momoa.evaluate(node);
3398
+ const resolutionOrder = getObjMember(node.body, "resolutionOrder");
3725
3399
  return {
3726
- tokens: tokensSorted,
3400
+ name: resolverSource.name,
3401
+ version: resolverSource.version,
3402
+ description: resolverSource.description,
3403
+ sets: resolverSource.sets,
3404
+ modifiers: resolverSource.modifiers,
3405
+ resolutionOrder: await Promise.all(resolutionOrder.elements.map(async (element, i) => {
3406
+ const layer = element.value;
3407
+ const members = getObjMembers(layer);
3408
+ let item = layer;
3409
+ if (members.$ref) {
3410
+ const entry = {
3411
+ group: "parser",
3412
+ label: "init",
3413
+ node: members.$ref,
3414
+ src
3415
+ };
3416
+ const { url, subpath } = parseRef(members.$ref.value);
3417
+ if (url === ".") if (!subpath?.[0]) logger.error({
3418
+ ...entry,
3419
+ message: "$ref can’t refer to the root document."
3420
+ });
3421
+ else if (subpath[0] !== "sets" && subpath[0] !== "modifiers") logger.error({
3422
+ ...entry,
3423
+ message: "Local $ref in resolutionOrder must point to either #/sets/[set] or #/modifiers/[modifiers]."
3424
+ });
3425
+ else {
3426
+ const resolvedItem = resolverSource[subpath[0]]?.[subpath[1]];
3427
+ if (!resolvedItem) logger.error({
3428
+ ...entry,
3429
+ message: "Invalid $ref"
3430
+ });
3431
+ else item = {
3432
+ type: subpath[0] === "sets" ? "set" : "modifier",
3433
+ name: subpath[1],
3434
+ ...resolvedItem
3435
+ };
3436
+ }
3437
+ else {
3438
+ const result = await bundle([{
3439
+ filename: new URL(url, filename),
3440
+ src: resolverSource.resolutionOrder[i]
3441
+ }], {
3442
+ req,
3443
+ yamlToMomoa
3444
+ });
3445
+ if (result.document.body.type === "Object") {
3446
+ const type = getObjMember(result.document.body, "type");
3447
+ if (type?.type === "String" && type.value === "set") {
3448
+ validateSet(result.document.body, true, src);
3449
+ item = momoa.evaluate(result.document.body);
3450
+ } else if (type?.type === "String" && type.value === "modifier") {
3451
+ validateModifier(result.document.body, true, src);
3452
+ item = momoa.evaluate(result.document.body);
3453
+ }
3454
+ }
3455
+ logger.error({
3456
+ ...entry,
3457
+ message: "$ref did not resolve to a valid Set or Modifier."
3458
+ });
3459
+ }
3460
+ }
3461
+ const finalResult = await bundle([{
3462
+ filename,
3463
+ src: item
3464
+ }], {
3465
+ req,
3466
+ yamlToMomoa
3467
+ });
3468
+ return momoa.evaluate(finalResult.document.body);
3469
+ })),
3470
+ _source: {
3471
+ filename,
3472
+ node
3473
+ }
3474
+ };
3475
+ }
3476
+
3477
+ //#endregion
3478
+ //#region src/resolver/load.ts
3479
+ /** Quick-parse input sources and find a resolver */
3480
+ async function loadResolver(inputs, { config, logger, req, yamlToMomoa }) {
3481
+ let resolverDoc;
3482
+ let tokens = {};
3483
+ const entry = {
3484
+ group: "parser",
3485
+ label: "init"
3486
+ };
3487
+ for (const input of inputs) {
3488
+ let document;
3489
+ if (typeof input.src === "string") if (maybeRawJSON(input.src)) document = toMomoa(input.src);
3490
+ else if (yamlToMomoa) document = yamlToMomoa(input.src);
3491
+ else logger.error({
3492
+ ...entry,
3493
+ message: `Install yaml-to-momoa package to parse YAML, and pass in as option, e.g.:
3494
+
3495
+ import { bundle } from '@terrazzo/json-schema-tools';
3496
+ import yamlToMomoa from 'yaml-to-momoa';
3497
+
3498
+ bundle(yamlString, { yamlToMomoa });`
3499
+ });
3500
+ else if (input.src && typeof input.src === "object") document = toMomoa(JSON.stringify(input.src, void 0, 2));
3501
+ else logger.error({
3502
+ ...entry,
3503
+ message: `Could not parse ${input.filename}. Is this valid JSON or YAML?`
3504
+ });
3505
+ if (!document || !isLikelyResolver(document)) continue;
3506
+ if (inputs.length > 1) logger.error({
3507
+ ...entry,
3508
+ message: `Resolver must be the only input, found ${inputs.length} sources.`
3509
+ });
3510
+ resolverDoc = document;
3511
+ break;
3512
+ }
3513
+ let resolver;
3514
+ if (resolverDoc) {
3515
+ validateResolver(resolverDoc, {
3516
+ logger,
3517
+ src: inputs[0].src
3518
+ });
3519
+ resolver = createResolver(await normalizeResolver(resolverDoc, {
3520
+ filename: inputs[0].filename,
3521
+ logger,
3522
+ req,
3523
+ src: inputs[0].src,
3524
+ yamlToMomoa
3525
+ }), {
3526
+ config,
3527
+ logger,
3528
+ sources: [{
3529
+ ...inputs[0],
3530
+ document: resolverDoc
3531
+ }]
3532
+ });
3533
+ const firstInput = {};
3534
+ for (const m of resolver.source.resolutionOrder) {
3535
+ if (m.type !== "modifier") continue;
3536
+ firstInput[m.name] = typeof m.default === "string" ? m.default : Object.keys(m.contexts)[0];
3537
+ }
3538
+ tokens = resolver.apply(firstInput);
3539
+ }
3540
+ return {
3541
+ resolver,
3542
+ tokens,
3543
+ sources: [{
3544
+ ...inputs[0],
3545
+ document: resolverDoc
3546
+ }]
3547
+ };
3548
+ }
3549
+ /** Create an interface to resolve permutations */
3550
+ function createResolver(resolverSource, { config, logger, sources }) {
3551
+ const inputDefaults = {};
3552
+ const validContexts = {};
3553
+ const allPermutations = [];
3554
+ const resolverCache = {};
3555
+ for (const m of resolverSource.resolutionOrder) if (m.type === "modifier") {
3556
+ if (typeof m.default === "string") inputDefaults[m.name] = m.default;
3557
+ validContexts[m.name] = Object.keys(m.contexts);
3558
+ }
3559
+ return {
3560
+ apply(inputRaw) {
3561
+ let tokensRaw = {};
3562
+ const input = {
3563
+ ...inputDefaults,
3564
+ ...inputRaw
3565
+ };
3566
+ const inputKey = makeInputKey(input);
3567
+ if (resolverCache[inputKey]) return resolverCache[inputKey];
3568
+ for (const item of resolverSource.resolutionOrder) switch (item.type) {
3569
+ case "set":
3570
+ for (const s of item.sources) tokensRaw = merge(tokensRaw, s);
3571
+ break;
3572
+ case "modifier": {
3573
+ const context = input[item.name];
3574
+ const sources$1 = item.contexts[context];
3575
+ if (!sources$1) logger.error({
3576
+ group: "parser",
3577
+ label: "resolver",
3578
+ message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`
3579
+ });
3580
+ for (const s of sources$1 ?? []) tokensRaw = merge(tokensRaw, s);
3581
+ break;
3582
+ }
3583
+ }
3584
+ const src = JSON.stringify(tokensRaw, void 0, 2);
3585
+ const rootSource = {
3586
+ filename: resolverSource._source.filename,
3587
+ document: toMomoa(src),
3588
+ src
3589
+ };
3590
+ const tokens = processTokens(rootSource, {
3591
+ config,
3592
+ logger,
3593
+ sourceByFilename: { [resolverSource._source.filename.href]: rootSource },
3594
+ refMap: {},
3595
+ sources
3596
+ });
3597
+ resolverCache[inputKey] = tokens;
3598
+ return tokens;
3599
+ },
3600
+ source: resolverSource,
3601
+ listPermutations() {
3602
+ if (!allPermutations.length) allPermutations.push(...calculatePermutations(Object.entries(validContexts)));
3603
+ return allPermutations;
3604
+ },
3605
+ isValidInput(input) {
3606
+ if (!input || typeof input !== "object") logger.error({
3607
+ group: "parser",
3608
+ label: "resolver",
3609
+ message: `Invalid input: ${JSON.stringify(input)}.`
3610
+ });
3611
+ if (!Object.keys(input).every((k) => k in validContexts)) return false;
3612
+ for (const [name, contexts] of Object.entries(validContexts)) if (name in input) {
3613
+ if (!contexts.includes(input[name])) return false;
3614
+ } else if (!(name in inputDefaults)) return false;
3615
+ return true;
3616
+ }
3617
+ };
3618
+ }
3619
+ /** Calculate all permutations */
3620
+ function calculatePermutations(options) {
3621
+ const permutationCount = [1];
3622
+ for (const [_name, contexts] of options) permutationCount.push(contexts.length * (permutationCount.at(-1) || 1));
3623
+ const permutations = [];
3624
+ for (let i = 0; i < permutationCount.at(-1); i++) {
3625
+ const input = {};
3626
+ for (let j = 0; j < options.length; j++) {
3627
+ const [name, contexts] = options[j];
3628
+ input[name] = contexts[Math.floor(i / permutationCount[j]) % contexts.length];
3629
+ }
3630
+ permutations.push(input);
3631
+ }
3632
+ return permutations.length ? permutations : [{}];
3633
+ }
3634
+
3635
+ //#endregion
3636
+ //#region src/resolver/create-synthetic-resolver.ts
3637
+ /**
3638
+ * Interop layer upgrading legacy Terrazzo modes to resolvers
3639
+ */
3640
+ async function createSyntheticResolver(tokens, { config, logger, req, sources }) {
3641
+ const contexts = {};
3642
+ for (const token of Object.values(tokens)) for (const [mode, value] of Object.entries(token.mode)) {
3643
+ if (mode === ".") continue;
3644
+ if (!(mode in contexts)) contexts[mode] = [{}];
3645
+ addToken(contexts[mode][0], {
3646
+ ...token,
3647
+ $value: value.$value
3648
+ }, { logger });
3649
+ }
3650
+ const src = JSON.stringify({
3651
+ name: "Terrazzo",
3652
+ version: "2025.10",
3653
+ resolutionOrder: [{ $ref: "#/sets/allTokens" }, { $ref: "#/modifiers/tzMode" }],
3654
+ sets: { allTokens: { sources: [simpleFlatten(tokens, { logger })] } },
3655
+ modifiers: { tzMode: {
3656
+ description: "Automatically built from $extensions.mode",
3657
+ contexts
3658
+ } }
3659
+ }, void 0, 2);
3660
+ return createResolver(await normalizeResolver(momoa.parse(src), {
3661
+ filename: new URL("file:///virtual:resolver.json"),
3662
+ logger,
3663
+ req,
3664
+ src
3665
+ }), {
3666
+ config,
3667
+ logger,
3668
+ sources
3669
+ });
3670
+ }
3671
+ /** Add a normalized token back into an arbitrary, hierarchial structure */
3672
+ function addToken(structure, token, { logger }) {
3673
+ let node = structure;
3674
+ const parts = token.id.split(".");
3675
+ const localID = parts.pop();
3676
+ for (const part of parts) {
3677
+ if (!(part in node)) node[part] = {};
3678
+ node = node[part];
3679
+ }
3680
+ if (localID in node) logger.error({
3681
+ group: "parser",
3682
+ label: "resolver",
3683
+ message: `${localID} already exists!`
3684
+ });
3685
+ node[localID] = {
3686
+ $type: token.$type,
3687
+ $value: token.$value
3688
+ };
3689
+ }
3690
+ /** Downconvert normalized tokens back into a simplified, hierarchial shape. This is extremely lossy, and only done to build a resolver. */
3691
+ function simpleFlatten(tokens, { logger }) {
3692
+ const group = {};
3693
+ for (const token of Object.values(tokens)) addToken(group, token, { logger });
3694
+ return group;
3695
+ }
3696
+
3697
+ //#endregion
3698
+ //#region src/parse/load.ts
3699
+ /** Load from multiple entries, while resolving remote files */
3700
+ async function loadSources(inputs, { config, logger, req, continueOnError, yamlToMomoa, transform }) {
3701
+ const entry = {
3702
+ group: "parser",
3703
+ label: "init"
3704
+ };
3705
+ const firstLoad = performance.now();
3706
+ let document = {};
3707
+ /** The original user inputs, in original order, with parsed ASTs */
3708
+ const sources = inputs.map((input, i) => ({
3709
+ ...input,
3710
+ document: {},
3711
+ filename: input.filename || new URL(`virtual:${i}`)
3712
+ }));
3713
+ /** The sources array, indexed by filename */
3714
+ let sourceByFilename = {};
3715
+ /** Mapping of all final $ref resolutions. This will be used to generate the graph later. */
3716
+ let refMap = {};
3717
+ try {
3718
+ const result = await bundle(sources, {
3719
+ req,
3720
+ parse: transform ? transformer(transform) : void 0,
3721
+ yamlToMomoa
3722
+ });
3723
+ document = result.document;
3724
+ sourceByFilename = result.sources;
3725
+ refMap = result.refMap;
3726
+ for (const [filename, source] of Object.entries(result.sources)) {
3727
+ const i = sources.findIndex((s) => s.filename.href === filename);
3728
+ if (i === -1) sources.push(source);
3729
+ else {
3730
+ sources[i].src = source.src;
3731
+ sources[i].document = source.document;
3732
+ }
3733
+ }
3734
+ } catch (err) {
3735
+ let src = sources.find((s) => s.filename.href === err.filename)?.src;
3736
+ if (src && typeof src !== "string") src = JSON.stringify(src, void 0, 2);
3737
+ logger.error({
3738
+ ...entry,
3739
+ continueOnError,
3740
+ message: err.message,
3741
+ node: err.node,
3742
+ src
3743
+ });
3744
+ }
3745
+ logger.debug({
3746
+ ...entry,
3747
+ message: `JSON loaded`,
3748
+ timing: performance.now() - firstLoad
3749
+ });
3750
+ return {
3751
+ tokens: processTokens({
3752
+ filename: sources[0].filename,
3753
+ document,
3754
+ src: momoa.print(document, { indent: 2 })
3755
+ }, {
3756
+ config,
3757
+ logger,
3758
+ refMap,
3759
+ sources,
3760
+ sourceByFilename
3761
+ }),
3727
3762
  sources
3728
3763
  };
3729
3764
  }
@@ -3740,7 +3775,9 @@ function transformer(transform) {
3740
3775
  });
3741
3776
  if (result) document = result;
3742
3777
  }
3743
- await traverseAsync(document, { async enter(node, parent, path) {
3778
+ const isResolver = isLikelyResolver(document);
3779
+ traverse(document, { enter(node, parent, rawPath) {
3780
+ const path = isResolver ? filterResolverPaths(rawPath) : rawPath;
3744
3781
  if (node.type !== "Object" || !path.length) return;
3745
3782
  const ctx = {
3746
3783
  filename,
@@ -3775,21 +3812,33 @@ function transformer(transform) {
3775
3812
  /** Parse */
3776
3813
  async function parse(_input, { logger = new Logger(), req = defaultReq, skipLint = false, config = {}, continueOnError = false, yamlToMomoa, transform } = {}) {
3777
3814
  const inputs = Array.isArray(_input) ? _input : [_input];
3815
+ let tokens = {};
3816
+ let resolver;
3817
+ let sources = [];
3778
3818
  const totalStart = performance.now();
3779
- const resolver = await loadResolver(inputs, {
3819
+ const initStart = performance.now();
3820
+ const resolverResult = await loadResolver(inputs, {
3821
+ config,
3780
3822
  logger,
3781
3823
  req,
3782
3824
  yamlToMomoa
3783
3825
  });
3784
- const initStart = performance.now();
3785
- const { tokens, sources } = await loadSources(inputs, {
3786
- req,
3787
- logger,
3788
- config,
3789
- continueOnError,
3790
- yamlToMomoa,
3791
- transform
3792
- });
3826
+ if (resolverResult.resolver) {
3827
+ tokens = resolverResult.tokens;
3828
+ sources = resolverResult.sources;
3829
+ resolver = resolverResult.resolver;
3830
+ } else {
3831
+ const tokenResult = await loadSources(inputs, {
3832
+ req,
3833
+ logger,
3834
+ config,
3835
+ continueOnError,
3836
+ yamlToMomoa,
3837
+ transform
3838
+ });
3839
+ tokens = tokenResult.tokens;
3840
+ sources = tokenResult.sources;
3841
+ }
3793
3842
  logger.debug({
3794
3843
  message: "Loaded tokens",
3795
3844
  group: "parser",
@@ -3811,6 +3860,19 @@ async function parse(_input, { logger = new Logger(), req = defaultReq, skipLint
3811
3860
  timing: performance.now() - lintStart
3812
3861
  });
3813
3862
  }
3863
+ const resolverTiming = performance.now();
3864
+ const finalResolver = resolver || await createSyntheticResolver(tokens, {
3865
+ config,
3866
+ logger,
3867
+ req,
3868
+ sources
3869
+ });
3870
+ logger.debug({
3871
+ message: "Resolver finalized",
3872
+ group: "parser",
3873
+ label: "core",
3874
+ timing: performance.now() - resolverTiming
3875
+ });
3814
3876
  logger.debug({
3815
3877
  message: "Finish all parser tasks",
3816
3878
  group: "parser",
@@ -3827,7 +3889,7 @@ async function parse(_input, { logger = new Logger(), req = defaultReq, skipLint
3827
3889
  return {
3828
3890
  tokens,
3829
3891
  sources,
3830
- resolver
3892
+ resolver: finalResolver
3831
3893
  };
3832
3894
  }
3833
3895
  let fs;