@terrazzo/parser 2.0.0-beta.4 → 2.0.0-beta.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,5 +1,4 @@
1
- import { BORDER_REQUIRED_PROPERTIES, COLOR_SPACE, 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, getTokenMatcher, isAlias, parseAlias, parseColor, pluralize, tokenToColor } from "@terrazzo/token-tools";
2
- import wcmatch from "wildcard-match";
1
+ import { BORDER_REQUIRED_PROPERTIES, COLOR_SPACE, CachedWildcardMatcher, 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, tokenToColor } from "@terrazzo/token-tools";
3
2
  import * as momoa from "@humanwhocodes/momoa";
4
3
  import pc from "picocolors";
5
4
  import { merge } from "merge-anything";
@@ -140,6 +139,7 @@ function formatMessage(entry, severity) {
140
139
  }
141
140
  return message;
142
141
  }
142
+ const debugMatch = new CachedWildcardMatcher();
143
143
  var Logger = class {
144
144
  level = "info";
145
145
  debugScope = "*";
@@ -191,7 +191,7 @@ var Logger = class {
191
191
  this.debugCount++;
192
192
  let message = formatMessage(entry, "debug");
193
193
  const debugPrefix = entry.label ? `${entry.group}:${entry.label}` : entry.group;
194
- if (this.debugScope !== "*" && !wcmatch(this.debugScope)(debugPrefix)) return;
194
+ if (this.debugScope !== "*" && !debugMatch.match(this.debugScope)(debugPrefix)) return;
195
195
  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));
196
196
  message = `${pc.dim(timeFormatter.format(performance.now()))} ${message}`;
197
197
  if (typeof entry.timing === "number") message = `${message} ${formatTiming(entry.timing)}`;
@@ -243,6 +243,7 @@ function validateTransformParams({ params, logger, pluginName }) {
243
243
  });
244
244
  }
245
245
  const FALLBACK_PERMUTATION_ID = JSON.stringify({ tzMode: "*" });
246
+ const cachedMatcher$1 = new CachedWildcardMatcher();
246
247
  /** Run build stage */
247
248
  async function build(tokens, { resolver, sources, logger = new Logger(), config }) {
248
249
  const formats = {};
@@ -257,84 +258,95 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
257
258
  });
258
259
  return [];
259
260
  }
260
- const tokenMatcher = params.id && params.id !== "*" ? getTokenMatcher(params.id) : null;
261
- const modeMatcher = params.mode ? wcmatch(params.mode) : null;
262
- const permutationID = params.input ? resolver.getPermutationID(params.input) : JSON.stringify({ tzMode: "*" });
261
+ const isLegacyModes = params.input && Object.keys(params.input).length === 1 && "tzMode" in params.input;
262
+ const permutationID = params.input && !isLegacyModes ? resolver.getPermutationID(params.input) : FALLBACK_PERMUTATION_ID;
263
+ const mode = params.mode || isLegacyModes && params.input.tzMode || void 0;
264
+ const singleTokenID = typeof params.id === "string" && tokens[params.id]?.id || Array.isArray(params.id) && params.id.length === 1 && tokens[params.id[0]]?.id || void 0;
265
+ const $type = typeof params.$type === "string" && [params.$type] || Array.isArray(params.$type) && params.$type || void 0;
266
+ const idMatcher = params.id && !singleTokenID && !isFullWildcard(params.id) ? cachedMatcher$1.tokenIDMatch(params.id) : null;
267
+ const modeMatcher = mode && mode !== "." && !isFullWildcard(mode) ? cachedMatcher$1.match(mode) : null;
263
268
  return (formats[params.format]?.[permutationID] ?? []).filter((token) => {
264
- if (params.$type) {
265
- if (typeof params.$type === "string" && token.token.$type !== params.$type) return false;
266
- else if (Array.isArray(params.$type) && !params.$type.some(($type) => token.token.$type === $type)) return false;
267
- }
268
- if (tokenMatcher && !tokenMatcher(token.token.id)) return false;
269
- if (params.input && token.permutationID !== resolver.getPermutationID(params.input)) return false;
270
- if (modeMatcher && !modeMatcher(token.mode)) return false;
269
+ if (singleTokenID && token.id !== singleTokenID || idMatcher && !idMatcher(token.id)) return false;
270
+ if (params.$type && !$type?.some((value) => token.token.$type === value)) return false;
271
+ if (mode === "." && token.mode !== "." || modeMatcher && !modeMatcher(token.mode)) return false;
271
272
  return true;
272
273
  });
273
274
  };
274
275
  }
275
276
  let transformsLocked = false;
276
277
  const startTransform = performance.now();
277
- for (const plugin of config.plugins) if (typeof plugin.transform === "function") await plugin.transform({
278
- context: { logger },
279
- tokens,
280
- sources,
281
- getTransforms: getTransforms(plugin.name),
282
- setTransform(id, params) {
283
- if (transformsLocked) {
284
- logger.warn({
285
- message: "Attempted to call setTransform() after transform step has completed.",
278
+ for (const plugin of config.plugins) if (typeof plugin.transform === "function") {
279
+ const pt = performance.now();
280
+ await plugin.transform({
281
+ context: { logger },
282
+ tokens,
283
+ sources,
284
+ getTransforms: getTransforms(plugin.name),
285
+ setTransform(id, params) {
286
+ if (transformsLocked) {
287
+ logger.warn({
288
+ message: "Attempted to call setTransform() after transform step has completed.",
289
+ group: "plugin",
290
+ label: plugin.name
291
+ });
292
+ return;
293
+ }
294
+ const token = tokens[id];
295
+ if (!token) logger.error({
286
296
  group: "plugin",
287
- label: plugin.name
297
+ label: plugin.name,
298
+ message: `No token "${id}"`
288
299
  });
289
- return;
290
- }
291
- const token = tokens[id];
292
- const permutationID = params.input ? resolver.getPermutationID(params.input) : FALLBACK_PERMUTATION_ID;
293
- const cleanValue = typeof params.value === "string" ? params.value : { ...params.value };
294
- validateTransformParams({
295
- logger,
296
- params: {
297
- ...params,
298
- value: cleanValue
299
- },
300
- pluginName: plugin.name
301
- });
302
- if (!formats[params.format]) formats[params.format] = {};
303
- if (!formats[params.format][permutationID]) formats[params.format][permutationID] = [];
304
- let foundTokenI = -1;
305
- if (params.mode) foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && params.mode === t.mode);
306
- else if (params.input) {
300
+ const isLegacyModes = params.input && Object.keys(params.input).length === 1 && "tzMode" in params.input;
301
+ const permutationID = params.input && !isLegacyModes ? resolver.getPermutationID(params.input) : FALLBACK_PERMUTATION_ID;
302
+ const mode = params.mode || isLegacyModes && params.input.tzMode || void 0;
303
+ const cleanValue = typeof params.value === "string" ? params.value : { ...params.value };
304
+ validateTransformParams({
305
+ logger,
306
+ params: {
307
+ ...params,
308
+ value: cleanValue
309
+ },
310
+ pluginName: plugin.name
311
+ });
312
+ if (!formats[params.format]) formats[params.format] = {};
307
313
  if (!formats[params.format][permutationID]) formats[params.format][permutationID] = [];
308
- foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && permutationID === t.permutationID);
309
- } else foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID));
310
- if (foundTokenI === -1) formats[params.format][permutationID].push({
311
- ...params,
312
- id,
313
- value: cleanValue,
314
- type: typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE,
315
- mode: params.mode || ".",
316
- token: structuredClone(token),
317
- permutationID,
318
- input: JSON.parse(permutationID)
319
- });
320
- else {
321
- formats[params.format][permutationID][foundTokenI].value = cleanValue;
322
- formats[params.format][permutationID][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
323
- }
324
- },
325
- resolver
326
- });
314
+ const foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && (!mode || t.mode === mode));
315
+ if (foundTokenI === -1) formats[params.format][permutationID].push({
316
+ ...params,
317
+ id,
318
+ value: cleanValue,
319
+ type: typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE,
320
+ mode: mode || ".",
321
+ token: makeReadOnlyToken(token),
322
+ permutationID,
323
+ input: JSON.parse(permutationID)
324
+ });
325
+ else {
326
+ formats[params.format][permutationID][foundTokenI].value = cleanValue;
327
+ formats[params.format][permutationID][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
328
+ }
329
+ },
330
+ resolver
331
+ });
332
+ logger.debug({
333
+ group: "plugin",
334
+ label: plugin.name,
335
+ message: "transform()",
336
+ timing: performance.now() - pt
337
+ });
338
+ }
327
339
  transformsLocked = true;
328
340
  logger.debug({
329
341
  group: "parser",
330
342
  label: "transform",
331
- message: "transform() step",
343
+ message: "All plugins finished transform()",
332
344
  timing: performance.now() - startTransform
333
345
  });
334
346
  const startBuild = performance.now();
335
347
  await Promise.all(config.plugins.map(async (plugin) => {
336
348
  if (typeof plugin.build === "function") {
337
- const pluginBuildStart = performance.now();
349
+ const pb = performance.now();
338
350
  await plugin.build({
339
351
  context: { logger },
340
352
  tokens,
@@ -352,18 +364,25 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
352
364
  filename,
353
365
  contents,
354
366
  plugin: plugin.name,
355
- time: performance.now() - pluginBuildStart
367
+ time: performance.now() - pb
356
368
  });
357
369
  }
358
370
  });
371
+ logger.debug({
372
+ group: "plugin",
373
+ label: plugin.name,
374
+ message: "build()",
375
+ timing: performance.now() - pb
376
+ });
359
377
  }
360
378
  }));
361
379
  logger.debug({
362
380
  group: "parser",
363
381
  label: "build",
364
- message: "build() step",
382
+ message: "All plugins finished build()",
365
383
  timing: performance.now() - startBuild
366
384
  });
385
+ cachedMatcher$1.reset();
367
386
  const startBuildEnd = performance.now();
368
387
  await Promise.all(config.plugins.map(async (plugin) => plugin.buildEnd?.({
369
388
  context: { logger },
@@ -380,6 +399,65 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
380
399
  });
381
400
  return result;
382
401
  }
402
+ function isFullWildcard(value) {
403
+ return typeof value === "string" && (value === "*" || value === "**") || Array.isArray(value) && value.some((v) => v === "*" || v === "**");
404
+ }
405
+ /** Generate getters for transformed tokens. Reduces memory usage while improving accuracy. Provides some safety for read-only values. */
406
+ function makeReadOnlyToken(token) {
407
+ return {
408
+ get id() {
409
+ return token.id;
410
+ },
411
+ get $value() {
412
+ return token.$value;
413
+ },
414
+ get $type() {
415
+ return token.$type;
416
+ },
417
+ get $description() {
418
+ return token.$description;
419
+ },
420
+ get $deprecated() {
421
+ return token.$deprecated;
422
+ },
423
+ get $extends() {
424
+ return token.$extends;
425
+ },
426
+ get $extensions() {
427
+ return token.$extensions;
428
+ },
429
+ get mode() {
430
+ return token.mode;
431
+ },
432
+ get originalValue() {
433
+ return token.originalValue;
434
+ },
435
+ get aliasChain() {
436
+ return token.aliasChain;
437
+ },
438
+ get aliasOf() {
439
+ return token.aliasOf;
440
+ },
441
+ get partialAliasOf() {
442
+ return token.partialAliasOf;
443
+ },
444
+ get aliasedBy() {
445
+ return token.aliasedBy;
446
+ },
447
+ get group() {
448
+ return token.group;
449
+ },
450
+ get source() {
451
+ return token.source;
452
+ },
453
+ get jsonID() {
454
+ return token.jsonID;
455
+ },
456
+ get dependencies() {
457
+ return token.dependencies;
458
+ }
459
+ };
460
+ }
383
461
 
384
462
  //#endregion
385
463
  //#region src/lint/plugin-core/lib/docs.ts
@@ -435,6 +513,22 @@ const rule$26 = {
435
513
  }
436
514
  };
437
515
 
516
+ //#endregion
517
+ //#region src/lint/plugin-core/lib/matchers.ts
518
+ /**
519
+ * Share one cached matcher factory for all lint plugins.
520
+ *
521
+ * Creating matchers is CPU-intensive, however, if we made one matcher for very
522
+ * getTransform plugin query, we could end up with tens of thousands of
523
+ * matchers, all taking up space in memory, but without providing any caching
524
+ * benefits if a matcher is used only once. So a reasonable balance is we
525
+ * maintain one cache per task category, and we garbage-collect everything after
526
+ * it’s done. Lint tasks are likely to have frequently-occurring patterns. So
527
+ * we’d expect for most use cases a shared lint cache has benefits, but only
528
+ * so long as this doesn’t spread to other plugins and other task categories.
529
+ */
530
+ const cachedLintMatcher = new CachedWildcardMatcher();
531
+
438
532
  //#endregion
439
533
  //#region src/lint/plugin-core/rules/a11y-min-font-size.ts
440
534
  const A11Y_MIN_FONT_SIZE = "a11y/min-font-size";
@@ -450,7 +544,7 @@ const rule$25 = {
450
544
  defaultOptions: {},
451
545
  create({ tokens, options, report }) {
452
546
  if (!options.minSizePx && !options.minSizeRem) throw new Error("Must specify at least one of minSizePx or minSizeRem");
453
- const shouldIgnore = options.ignore ? getTokenMatcher(options.ignore) : null;
547
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
454
548
  for (const t of Object.values(tokens)) {
455
549
  if (shouldIgnore?.(t.id)) continue;
456
550
  if (t.aliasOf) continue;
@@ -491,7 +585,7 @@ const rule$24 = {
491
585
  defaultOptions: { colorSpace: "srgb" },
492
586
  create({ tokens, options, report }) {
493
587
  if (!options.colorSpace) return;
494
- const shouldIgnore = options.ignore ? getTokenMatcher(options.ignore) : null;
588
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
495
589
  for (const t of Object.values(tokens)) {
496
590
  if (shouldIgnore?.(t.id)) continue;
497
591
  if (t.aliasOf) continue;
@@ -602,7 +696,7 @@ const rule$22 = {
602
696
  },
603
697
  defaultOptions: {},
604
698
  create({ tokens, options, report }) {
605
- const shouldIgnore = options.ignore ? getTokenMatcher(options.ignore) : null;
699
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
606
700
  for (const t of Object.values(tokens)) {
607
701
  if (shouldIgnore?.(t.id)) continue;
608
702
  if (!t.$description) report({
@@ -629,7 +723,7 @@ const rule$21 = {
629
723
  defaultOptions: {},
630
724
  create({ report, tokens, options }) {
631
725
  const values = {};
632
- const shouldIgnore = options.ignore ? getTokenMatcher(options.ignore) : null;
726
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
633
727
  for (const t of Object.values(tokens)) {
634
728
  if (shouldIgnore?.(t.id)) continue;
635
729
  if (!values[t.$type]) values[t.$type] = /* @__PURE__ */ new Set();
@@ -682,7 +776,7 @@ const rule$20 = {
682
776
  create({ tokens, options, report }) {
683
777
  if (!options?.gamut) return;
684
778
  if (options.gamut !== "srgb" && options.gamut !== "p3" && options.gamut !== "rec2020") throw new Error(`Unknown gamut "${options.gamut}". Options are "srgb", "p3", or "rec2020"`);
685
- const shouldIgnore = options.ignore ? getTokenMatcher(options.ignore) : null;
779
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
686
780
  for (const t of Object.values(tokens)) {
687
781
  if (shouldIgnore?.(t.id)) continue;
688
782
  if (t.aliasOf) continue;
@@ -761,7 +855,7 @@ const rule$19 = {
761
855
  const { match, requiredTokens, requiredGroups } = options.matches[matchI];
762
856
  if (!match.length) throw new Error(`Match ${matchI}: must declare \`match: […]\``);
763
857
  if (!requiredTokens?.length && !requiredGroups?.length) throw new Error(`Match ${matchI}: must declare either \`requiredTokens: […]\` or \`requiredGroups: […]\``);
764
- const matcher = getTokenMatcher(match);
858
+ const matcher = cachedLintMatcher.tokenIDMatch(match);
765
859
  const matchGroups = [];
766
860
  const matchTokens = [];
767
861
  let tokensMatched = false;
@@ -816,7 +910,7 @@ const rule$18 = {
816
910
  const { match, modes } = options.matches[matchI];
817
911
  if (!match.length) throw new Error(`Match ${matchI}: must declare \`match: […]\``);
818
912
  if (!modes?.length) throw new Error(`Match ${matchI}: must declare \`modes: […]\``);
819
- const matcher = getTokenMatcher(match);
913
+ const matcher = cachedLintMatcher.tokenIDMatch(match);
820
914
  let tokensMatched = false;
821
915
  for (const t of Object.values(tokens)) {
822
916
  if (!matcher(t.id)) continue;
@@ -877,7 +971,7 @@ const rule$16 = {
877
971
  create({ tokens, options, report }) {
878
972
  if (!options) return;
879
973
  if (!options.properties.length) throw new Error(`"properties" can’t be empty`);
880
- const shouldIgnore = options.ignore ? getTokenMatcher(options.ignore) : null;
974
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
881
975
  for (const t of Object.values(tokens)) {
882
976
  if (shouldIgnore?.(t.id)) continue;
883
977
  if (t.$type !== "typography") continue;
@@ -1388,7 +1482,7 @@ const rule$6 = {
1388
1482
  "lineHeight"
1389
1483
  ] },
1390
1484
  create({ tokens, options, report }) {
1391
- const isIgnored = options.ignore ? getTokenMatcher(options.ignore) : () => false;
1485
+ const isIgnored = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : () => false;
1392
1486
  for (const t of Object.values(tokens)) {
1393
1487
  if (t.aliasOf || !t.originalValue || t.$type !== "typography" || isIgnored(t.id)) continue;
1394
1488
  validateTypography(t.originalValue.$value, {
@@ -2341,6 +2435,7 @@ async function lintRunner({ tokens, filename, config = {}, sources, logger }) {
2341
2435
  message: "Finished",
2342
2436
  timing: performance.now() - s
2343
2437
  });
2438
+ cachedLintMatcher.reset();
2344
2439
  }
2345
2440
  const errCount = errors.length ? `${errors.length} ${pluralize(errors.length, "error", "errors")}` : "";
2346
2441
  const warnCount = warnings.length ? `${warnings.length} ${pluralize(warnings.length, "warning", "warnings")}` : "";
@@ -2372,6 +2467,13 @@ function toMomoa(srcRaw) {
2372
2467
  });
2373
2468
  }
2374
2469
 
2470
+ //#endregion
2471
+ //#region src/lib/array.ts
2472
+ /** JS compiler-optimizable comparator */
2473
+ function alphaComparator(a, b) {
2474
+ return a.localeCompare(b, "en-us", { numeric: true });
2475
+ }
2476
+
2375
2477
  //#endregion
2376
2478
  //#region src/lib/resolver-utils.ts
2377
2479
  /**
@@ -2393,8 +2495,32 @@ function filterResolverPaths(path) {
2393
2495
  }
2394
2496
  /** Make a deterministic string from an object */
2395
2497
  function getPermutationID(input) {
2396
- const keys = Object.keys(input).sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2397
- return JSON.stringify(Object.fromEntries(keys.map((k) => [k, input[k]])));
2498
+ const keys = Object.keys(input).sort(alphaComparator);
2499
+ const sortedInput = {};
2500
+ for (const k of keys) sortedInput[k] = input[k];
2501
+ return JSON.stringify(sortedInput);
2502
+ }
2503
+ /**
2504
+ * Destructively merge B into A, with B overwriting A
2505
+ *
2506
+ * This is needed for resolvers because we need a really performant way to merge
2507
+ * token sets. merge-anything is a package we use for merging more complex
2508
+ * configurations like terrazzo.config.ts files, but that’s too slow for tokens.
2509
+ */
2510
+ function destructiveMerge(a, b) {
2511
+ if (!a || !b || typeof b !== "object") return;
2512
+ for (const k in b) {
2513
+ if (!Object.hasOwn(b, k)) continue;
2514
+ const b2 = b[k];
2515
+ if (b2 != null && typeof b2 === "object") if (Array.isArray(b2)) {
2516
+ a[k] = [];
2517
+ destructiveMerge(a[k], [...b2]);
2518
+ } else {
2519
+ if (!(k in a)) a[k] = {};
2520
+ destructiveMerge(a[k], { ...b2 });
2521
+ }
2522
+ else a[k] = b2;
2523
+ }
2398
2524
  }
2399
2525
 
2400
2526
  //#endregion
@@ -2455,15 +2581,12 @@ function normalize(token, { logger, src }) {
2455
2581
  switch (token.$type) {
2456
2582
  case "color":
2457
2583
  for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeColor(token.mode[mode].$value, token.mode[mode].source.node);
2458
- token.$value = token.mode["."].$value;
2459
2584
  break;
2460
2585
  case "fontFamily":
2461
2586
  for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeFontFamily(token.mode[mode].$value);
2462
- token.$value = token.mode["."].$value;
2463
2587
  break;
2464
2588
  case "fontWeight":
2465
2589
  for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeFontWeight(token.mode[mode].$value);
2466
- token.$value = token.mode["."].$value;
2467
2590
  break;
2468
2591
  case "border":
2469
2592
  for (const mode of Object.keys(token.mode)) {
@@ -2471,7 +2594,6 @@ function normalize(token, { logger, src }) {
2471
2594
  if (!border || typeof border !== "object") continue;
2472
2595
  if (border.color) border.color = normalizeColor(border.color, getObjMember(token.mode[mode].source.node, "color"));
2473
2596
  }
2474
- token.$value = token.mode["."].$value;
2475
2597
  break;
2476
2598
  case "shadow":
2477
2599
  for (const mode of Object.keys(token.mode)) {
@@ -2485,7 +2607,6 @@ function normalize(token, { logger, src }) {
2485
2607
  if (!("inset" in shadow)) shadow.inset = false;
2486
2608
  }
2487
2609
  }
2488
- token.$value = token.mode["."].$value;
2489
2610
  break;
2490
2611
  case "gradient":
2491
2612
  for (const mode of Object.keys(token.mode)) {
@@ -2498,7 +2619,6 @@ function normalize(token, { logger, src }) {
2498
2619
  if (stop.color) stop.color = normalizeColor(stop.color, getObjMember(stopNode, "color"));
2499
2620
  }
2500
2621
  }
2501
- token.$value = token.mode["."].$value;
2502
2622
  break;
2503
2623
  case "typography":
2504
2624
  for (const mode of Object.keys(token.mode)) {
@@ -2513,7 +2633,6 @@ function normalize(token, { logger, src }) {
2513
2633
  break;
2514
2634
  }
2515
2635
  }
2516
- token.$value = token.mode["."].$value;
2517
2636
  break;
2518
2637
  }
2519
2638
  }
@@ -2532,6 +2651,7 @@ function aliasToTokenRef(alias, mode) {
2532
2651
  if (id === alias) return;
2533
2652
  return { $ref: `#/${id.replace(/~/g, "~0").replace(/\//g, "~1").replace(/\./g, "/")}${mode && mode !== "." ? `/$extensions/mode/${mode}` : ""}/$value` };
2534
2653
  }
2654
+ const cachedMatcher = new CachedWildcardMatcher();
2535
2655
  /** Generate a TokenNormalized from a Momoa node */
2536
2656
  function tokenFromNode(node, { groups, path, source, ignore }) {
2537
2657
  if (!(node.type === "Object" && !!getObjMember(node, "$value") && !path.includes("$extensions"))) return;
@@ -2549,33 +2669,45 @@ function tokenFromNode(node, { groups, path, source, ignore }) {
2549
2669
  $type: originalToken.$type || group.$type,
2550
2670
  $description: originalToken.$description || void 0,
2551
2671
  $deprecated: originalToken.$deprecated ?? group.$deprecated ?? void 0,
2552
- $value: originalToken.$value,
2672
+ get $value() {
2673
+ return this.mode["."].$value;
2674
+ },
2553
2675
  $extensions: originalToken.$extensions || void 0,
2554
2676
  $extends: originalToken.$extends || void 0,
2555
- aliasChain: void 0,
2556
- aliasedBy: void 0,
2557
- aliasOf: void 0,
2558
- partialAliasOf: void 0,
2559
- dependencies: void 0,
2677
+ get aliasChain() {
2678
+ return this.mode["."].aliasChain;
2679
+ },
2680
+ get aliasedBy() {
2681
+ return this.mode["."].aliasedBy;
2682
+ },
2683
+ get aliasOf() {
2684
+ return this.mode["."].aliasOf;
2685
+ },
2686
+ get partialAliasOf() {
2687
+ return this.mode["."].partialAliasOf;
2688
+ },
2689
+ get dependencies() {
2690
+ return this.mode["."].dependencies;
2691
+ },
2560
2692
  group,
2561
2693
  originalValue: void 0,
2562
2694
  source: nodeSource,
2563
2695
  jsonID,
2564
2696
  mode: { ".": {
2565
2697
  $value: originalToken.$value,
2566
- aliasOf: void 0,
2567
2698
  aliasChain: void 0,
2568
- partialAliasOf: void 0,
2569
2699
  aliasedBy: void 0,
2570
- originalValue: void 0,
2700
+ aliasOf: void 0,
2701
+ partialAliasOf: void 0,
2571
2702
  dependencies: void 0,
2703
+ originalValue: void 0,
2572
2704
  source: {
2573
2705
  ...nodeSource,
2574
2706
  node: getObjMember(nodeSource.node, "$value") ?? nodeSource.node
2575
2707
  }
2576
2708
  } }
2577
2709
  };
2578
- if (ignore?.deprecated && token.$deprecated || ignore?.tokens && getTokenMatcher(ignore.tokens)(token.id)) return;
2710
+ if (ignore?.deprecated && token.$deprecated || ignore?.tokens && cachedMatcher.tokenIDMatch(ignore.tokens)(token.id)) return;
2579
2711
  const $extensions = getObjMember(node, "$extensions");
2580
2712
  if ($extensions) {
2581
2713
  const modeNode = getObjMember($extensions, "mode");
@@ -2684,7 +2816,7 @@ function graphAliases(refMap, { tokens, logger, sources }) {
2684
2816
  if (!modeValue) continue;
2685
2817
  if (!modeValue.dependencies) modeValue.dependencies = [];
2686
2818
  modeValue.dependencies.push(...refChain.filter((r) => !modeValue.dependencies.includes(r)));
2687
- modeValue.dependencies.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2819
+ modeValue.dependencies.sort(alphaComparator);
2688
2820
  if (jsonID.endsWith("/$value") || tokens[jsonID]) {
2689
2821
  modeValue.aliasOf = refToTokenID(refChain.at(-1));
2690
2822
  modeValue.aliasChain = [...refChain.map(refToTokenID)];
@@ -2728,26 +2860,16 @@ function graphAliases(refMap, { tokens, logger, sources }) {
2728
2860
  const aliasedByRefs = [jsonID, ...refChain].reverse();
2729
2861
  for (let i = 0; i < aliasedByRefs.length; i++) {
2730
2862
  const baseRef = getTokenRef(aliasedByRefs[i]);
2731
- const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef];
2863
+ const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef]?.mode["."];
2732
2864
  if (!baseToken) continue;
2733
2865
  const upstream = aliasedByRefs.slice(i + 1);
2734
2866
  if (!upstream.length) break;
2735
2867
  if (!baseToken.aliasedBy) baseToken.aliasedBy = [];
2736
2868
  for (let j = 0; j < upstream.length; j++) {
2737
2869
  const downstream = refToTokenID(upstream[j]);
2738
- if (!baseToken.aliasedBy.includes(downstream)) {
2739
- baseToken.aliasedBy.push(downstream);
2740
- if (mode === ".") tokens[baseRef].aliasedBy = baseToken.aliasedBy;
2741
- }
2870
+ if (!baseToken.aliasedBy.includes(downstream)) baseToken.aliasedBy.push(downstream);
2742
2871
  }
2743
- baseToken.aliasedBy.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2744
- }
2745
- if (mode === ".") {
2746
- tokens[rootRef].aliasChain = modeValue.aliasChain;
2747
- tokens[rootRef].aliasedBy = modeValue.aliasedBy;
2748
- tokens[rootRef].aliasOf = modeValue.aliasOf;
2749
- tokens[rootRef].dependencies = modeValue.dependencies;
2750
- tokens[rootRef].partialAliasOf = modeValue.partialAliasOf;
2872
+ baseToken.aliasedBy.sort(alphaComparator);
2751
2873
  }
2752
2874
  }
2753
2875
  }
@@ -2884,7 +3006,6 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
2884
3006
  });
2885
3007
  if (!token.$type) token.$type = $type;
2886
3008
  if ($value) token.mode[mode].$value = $value;
2887
- if (mode === ".") token.$value = token.mode[mode].$value;
2888
3009
  }
2889
3010
  }
2890
3011
  }
@@ -3045,6 +3166,7 @@ function processTokens(rootSource, { config, logger, sourceByFilename, isResolve
3045
3166
  });
3046
3167
  if (tokenRawValues && tokens[tokenRawValues?.jsonID]) {
3047
3168
  tokens[tokenRawValues.jsonID].originalValue = tokenRawValues.originalValue;
3169
+ tokens[tokenRawValues.jsonID].mode["."].originalValue = tokenRawValues.originalValue;
3048
3170
  tokens[tokenRawValues.jsonID].source = tokenRawValues.source;
3049
3171
  for (const mode of Object.keys(tokenRawValues.mode)) {
3050
3172
  tokens[tokenRawValues.jsonID].mode[mode].originalValue = tokenRawValues.mode[mode].originalValue;
@@ -3089,12 +3211,12 @@ function processTokens(rootSource, { config, logger, sourceByFilename, isResolve
3089
3211
  if (config.alphabetize === false) return tokens;
3090
3212
  const sortStart = performance.now();
3091
3213
  const tokensSorted = {};
3092
- tokenIDs.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3214
+ tokenIDs.sort(alphaComparator);
3093
3215
  for (const path of tokenIDs) {
3094
3216
  const id = refToTokenID(path);
3095
3217
  tokensSorted[id] = tokens[path];
3096
3218
  }
3097
- for (const group of Object.values(groups)) group.tokens.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3219
+ for (const group of Object.values(groups)) group.tokens.sort(alphaComparator);
3098
3220
  logger.debug({
3099
3221
  ...entry,
3100
3222
  message: "Sorted tokens",
@@ -3678,7 +3800,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3678
3800
  }
3679
3801
  return {
3680
3802
  apply(inputRaw) {
3681
- let tokensRaw = {};
3803
+ const tokensRaw = {};
3682
3804
  const input = {
3683
3805
  ...inputDefaults,
3684
3806
  ...inputRaw
@@ -3687,7 +3809,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3687
3809
  if (resolverCache[permutationID]) return resolverCache[permutationID];
3688
3810
  for (const item of resolverSource.resolutionOrder) switch (item.type) {
3689
3811
  case "set":
3690
- for (const s of item.sources) tokensRaw = merge(tokensRaw, s);
3812
+ for (const s of item.sources) destructiveMerge(tokensRaw, s);
3691
3813
  break;
3692
3814
  case "modifier": {
3693
3815
  const context = input[item.name];
@@ -3696,7 +3818,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3696
3818
  group: "resolver",
3697
3819
  message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`
3698
3820
  });
3699
- for (const s of sources ?? []) tokensRaw = merge(tokensRaw, s);
3821
+ for (const s of sources ?? []) destructiveMerge(tokensRaw, s);
3700
3822
  break;
3701
3823
  }
3702
3824
  }
@@ -3734,6 +3856,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3734
3856
  return false;
3735
3857
  }
3736
3858
  for (const [name, contexts] of Object.entries(validContexts)) if (name in input) {
3859
+ if (name === "tzMode") continue;
3737
3860
  if (!contexts.includes(input[name])) {
3738
3861
  if (throwError) logger.error({
3739
3862
  group: "resolver",
@@ -3783,7 +3906,6 @@ function calculatePermutations(options) {
3783
3906
  async function createSyntheticResolver(tokens, { config, logger, req, sources }) {
3784
3907
  const contexts = {};
3785
3908
  for (const token of Object.values(tokens)) for (const [mode, value] of Object.entries(token.mode)) {
3786
- if (mode === ".") continue;
3787
3909
  if (!(mode in contexts)) contexts[mode] = [{}];
3788
3910
  addToken(contexts[mode][0], {
3789
3911
  ...token,
@@ -3797,7 +3919,8 @@ async function createSyntheticResolver(tokens, { config, logger, req, sources })
3797
3919
  sets: { allTokens: { sources: [simpleFlatten(tokens, { logger })] } },
3798
3920
  modifiers: { tzMode: {
3799
3921
  description: "Automatically built from $extensions.mode",
3800
- contexts
3922
+ contexts,
3923
+ default: "."
3801
3924
  } }
3802
3925
  }, void 0, 2);
3803
3926
  return createResolver(await normalizeResolver(momoa.parse(src), {
@@ -3954,6 +4077,16 @@ async function parse(_input, { logger = new Logger(), req = defaultReq, skipLint
3954
4077
  let tokens = {};
3955
4078
  let resolver;
3956
4079
  let sources = [];
4080
+ if (inputs.length === 0) logger.error({
4081
+ group: "parser",
4082
+ label: "init",
4083
+ message: "Nothing to parse."
4084
+ });
4085
+ for (let i = 0; i < inputs.length; i++) if (!inputs[i] || typeof inputs[i] !== "object" || !inputs[i]?.src || inputs[i]?.filename && !(inputs[i].filename instanceof URL)) logger.error({
4086
+ group: "parser",
4087
+ label: "init",
4088
+ message: `Input ${i}: expected { src: any; filename: URL }`
4089
+ });
3957
4090
  const totalStart = performance.now();
3958
4091
  const initStart = performance.now();
3959
4092
  const resolverResult = await loadResolver(inputs, {