@terrazzo/parser 2.0.0-beta.4 → 2.0.0-beta.6

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,98 @@ 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] = { [FALLBACK_PERMUTATION_ID]: [] };
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) {
316
+ const transformedToken = {
317
+ ...params,
318
+ id,
319
+ value: cleanValue,
320
+ type: typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE,
321
+ mode: mode || ".",
322
+ token: makeReadOnlyToken(token),
323
+ permutationID,
324
+ input: JSON.parse(permutationID)
325
+ };
326
+ formats[params.format][permutationID].push(transformedToken);
327
+ if (params.input && !Object.keys(params.input).length && permutationID !== FALLBACK_PERMUTATION_ID) formats[params.format][FALLBACK_PERMUTATION_ID].push(transformedToken);
328
+ } else {
329
+ formats[params.format][permutationID][foundTokenI].value = cleanValue;
330
+ formats[params.format][permutationID][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
331
+ }
332
+ },
333
+ resolver
334
+ });
335
+ logger.debug({
336
+ group: "plugin",
337
+ label: plugin.name,
338
+ message: "transform()",
339
+ timing: performance.now() - pt
340
+ });
341
+ }
327
342
  transformsLocked = true;
328
343
  logger.debug({
329
344
  group: "parser",
330
345
  label: "transform",
331
- message: "transform() step",
346
+ message: "All plugins finished transform()",
332
347
  timing: performance.now() - startTransform
333
348
  });
334
349
  const startBuild = performance.now();
335
350
  await Promise.all(config.plugins.map(async (plugin) => {
336
351
  if (typeof plugin.build === "function") {
337
- const pluginBuildStart = performance.now();
352
+ const pb = performance.now();
338
353
  await plugin.build({
339
354
  context: { logger },
340
355
  tokens,
@@ -352,18 +367,25 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
352
367
  filename,
353
368
  contents,
354
369
  plugin: plugin.name,
355
- time: performance.now() - pluginBuildStart
370
+ time: performance.now() - pb
356
371
  });
357
372
  }
358
373
  });
374
+ logger.debug({
375
+ group: "plugin",
376
+ label: plugin.name,
377
+ message: "build()",
378
+ timing: performance.now() - pb
379
+ });
359
380
  }
360
381
  }));
361
382
  logger.debug({
362
383
  group: "parser",
363
384
  label: "build",
364
- message: "build() step",
385
+ message: "All plugins finished build()",
365
386
  timing: performance.now() - startBuild
366
387
  });
388
+ cachedMatcher$1.reset();
367
389
  const startBuildEnd = performance.now();
368
390
  await Promise.all(config.plugins.map(async (plugin) => plugin.buildEnd?.({
369
391
  context: { logger },
@@ -380,6 +402,65 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
380
402
  });
381
403
  return result;
382
404
  }
405
+ function isFullWildcard(value) {
406
+ return typeof value === "string" && (value === "*" || value === "**") || Array.isArray(value) && value.some((v) => v === "*" || v === "**");
407
+ }
408
+ /** Generate getters for transformed tokens. Reduces memory usage while improving accuracy. Provides some safety for read-only values. */
409
+ function makeReadOnlyToken(token) {
410
+ return {
411
+ get id() {
412
+ return token.id;
413
+ },
414
+ get $value() {
415
+ return token.$value;
416
+ },
417
+ get $type() {
418
+ return token.$type;
419
+ },
420
+ get $description() {
421
+ return token.$description;
422
+ },
423
+ get $deprecated() {
424
+ return token.$deprecated;
425
+ },
426
+ get $extends() {
427
+ return token.$extends;
428
+ },
429
+ get $extensions() {
430
+ return token.$extensions;
431
+ },
432
+ get mode() {
433
+ return token.mode;
434
+ },
435
+ get originalValue() {
436
+ return token.originalValue;
437
+ },
438
+ get aliasChain() {
439
+ return token.aliasChain;
440
+ },
441
+ get aliasOf() {
442
+ return token.aliasOf;
443
+ },
444
+ get partialAliasOf() {
445
+ return token.partialAliasOf;
446
+ },
447
+ get aliasedBy() {
448
+ return token.aliasedBy;
449
+ },
450
+ get group() {
451
+ return token.group;
452
+ },
453
+ get source() {
454
+ return token.source;
455
+ },
456
+ get jsonID() {
457
+ return token.jsonID;
458
+ },
459
+ get dependencies() {
460
+ return token.dependencies;
461
+ }
462
+ };
463
+ }
383
464
 
384
465
  //#endregion
385
466
  //#region src/lint/plugin-core/lib/docs.ts
@@ -435,6 +516,22 @@ const rule$26 = {
435
516
  }
436
517
  };
437
518
 
519
+ //#endregion
520
+ //#region src/lint/plugin-core/lib/matchers.ts
521
+ /**
522
+ * Share one cached matcher factory for all lint plugins.
523
+ *
524
+ * Creating matchers is CPU-intensive, however, if we made one matcher for very
525
+ * getTransform plugin query, we could end up with tens of thousands of
526
+ * matchers, all taking up space in memory, but without providing any caching
527
+ * benefits if a matcher is used only once. So a reasonable balance is we
528
+ * maintain one cache per task category, and we garbage-collect everything after
529
+ * it’s done. Lint tasks are likely to have frequently-occurring patterns. So
530
+ * we’d expect for most use cases a shared lint cache has benefits, but only
531
+ * so long as this doesn’t spread to other plugins and other task categories.
532
+ */
533
+ const cachedLintMatcher = new CachedWildcardMatcher();
534
+
438
535
  //#endregion
439
536
  //#region src/lint/plugin-core/rules/a11y-min-font-size.ts
440
537
  const A11Y_MIN_FONT_SIZE = "a11y/min-font-size";
@@ -450,7 +547,7 @@ const rule$25 = {
450
547
  defaultOptions: {},
451
548
  create({ tokens, options, report }) {
452
549
  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;
550
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
454
551
  for (const t of Object.values(tokens)) {
455
552
  if (shouldIgnore?.(t.id)) continue;
456
553
  if (t.aliasOf) continue;
@@ -491,7 +588,7 @@ const rule$24 = {
491
588
  defaultOptions: { colorSpace: "srgb" },
492
589
  create({ tokens, options, report }) {
493
590
  if (!options.colorSpace) return;
494
- const shouldIgnore = options.ignore ? getTokenMatcher(options.ignore) : null;
591
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
495
592
  for (const t of Object.values(tokens)) {
496
593
  if (shouldIgnore?.(t.id)) continue;
497
594
  if (t.aliasOf) continue;
@@ -602,7 +699,7 @@ const rule$22 = {
602
699
  },
603
700
  defaultOptions: {},
604
701
  create({ tokens, options, report }) {
605
- const shouldIgnore = options.ignore ? getTokenMatcher(options.ignore) : null;
702
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
606
703
  for (const t of Object.values(tokens)) {
607
704
  if (shouldIgnore?.(t.id)) continue;
608
705
  if (!t.$description) report({
@@ -629,7 +726,7 @@ const rule$21 = {
629
726
  defaultOptions: {},
630
727
  create({ report, tokens, options }) {
631
728
  const values = {};
632
- const shouldIgnore = options.ignore ? getTokenMatcher(options.ignore) : null;
729
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
633
730
  for (const t of Object.values(tokens)) {
634
731
  if (shouldIgnore?.(t.id)) continue;
635
732
  if (!values[t.$type]) values[t.$type] = /* @__PURE__ */ new Set();
@@ -682,7 +779,7 @@ const rule$20 = {
682
779
  create({ tokens, options, report }) {
683
780
  if (!options?.gamut) return;
684
781
  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;
782
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
686
783
  for (const t of Object.values(tokens)) {
687
784
  if (shouldIgnore?.(t.id)) continue;
688
785
  if (t.aliasOf) continue;
@@ -761,7 +858,7 @@ const rule$19 = {
761
858
  const { match, requiredTokens, requiredGroups } = options.matches[matchI];
762
859
  if (!match.length) throw new Error(`Match ${matchI}: must declare \`match: […]\``);
763
860
  if (!requiredTokens?.length && !requiredGroups?.length) throw new Error(`Match ${matchI}: must declare either \`requiredTokens: […]\` or \`requiredGroups: […]\``);
764
- const matcher = getTokenMatcher(match);
861
+ const matcher = cachedLintMatcher.tokenIDMatch(match);
765
862
  const matchGroups = [];
766
863
  const matchTokens = [];
767
864
  let tokensMatched = false;
@@ -816,7 +913,7 @@ const rule$18 = {
816
913
  const { match, modes } = options.matches[matchI];
817
914
  if (!match.length) throw new Error(`Match ${matchI}: must declare \`match: […]\``);
818
915
  if (!modes?.length) throw new Error(`Match ${matchI}: must declare \`modes: […]\``);
819
- const matcher = getTokenMatcher(match);
916
+ const matcher = cachedLintMatcher.tokenIDMatch(match);
820
917
  let tokensMatched = false;
821
918
  for (const t of Object.values(tokens)) {
822
919
  if (!matcher(t.id)) continue;
@@ -877,7 +974,7 @@ const rule$16 = {
877
974
  create({ tokens, options, report }) {
878
975
  if (!options) return;
879
976
  if (!options.properties.length) throw new Error(`"properties" can’t be empty`);
880
- const shouldIgnore = options.ignore ? getTokenMatcher(options.ignore) : null;
977
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
881
978
  for (const t of Object.values(tokens)) {
882
979
  if (shouldIgnore?.(t.id)) continue;
883
980
  if (t.$type !== "typography") continue;
@@ -1388,7 +1485,7 @@ const rule$6 = {
1388
1485
  "lineHeight"
1389
1486
  ] },
1390
1487
  create({ tokens, options, report }) {
1391
- const isIgnored = options.ignore ? getTokenMatcher(options.ignore) : () => false;
1488
+ const isIgnored = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : () => false;
1392
1489
  for (const t of Object.values(tokens)) {
1393
1490
  if (t.aliasOf || !t.originalValue || t.$type !== "typography" || isIgnored(t.id)) continue;
1394
1491
  validateTypography(t.originalValue.$value, {
@@ -2298,14 +2395,14 @@ async function lintRunner({ tokens, filename, config = {}, sources, logger }) {
2298
2395
  let message = "";
2299
2396
  if (!descriptor.message && !descriptor.messageId) logger.error({
2300
2397
  group: "lint",
2301
- label: `${plugin.name} › lint › ${id}`,
2398
+ label: `${plugin.name}:${id}`,
2302
2399
  message: "Unable to report error: missing message or messageId"
2303
2400
  });
2304
2401
  if (descriptor.message) message = descriptor.message;
2305
2402
  else {
2306
2403
  if (!(descriptor.messageId in (rule.meta?.messages ?? {}))) logger.error({
2307
2404
  group: "lint",
2308
- label: `${plugin.name} › lint › ${id}`,
2405
+ label: `${plugin.name}:${id}`,
2309
2406
  message: `messageId "${descriptor.messageId}" does not exist`
2310
2407
  });
2311
2408
  message = rule.meta?.messages?.[descriptor.messageId] ?? "";
@@ -2341,6 +2438,7 @@ async function lintRunner({ tokens, filename, config = {}, sources, logger }) {
2341
2438
  message: "Finished",
2342
2439
  timing: performance.now() - s
2343
2440
  });
2441
+ cachedLintMatcher.reset();
2344
2442
  }
2345
2443
  const errCount = errors.length ? `${errors.length} ${pluralize(errors.length, "error", "errors")}` : "";
2346
2444
  const warnCount = warnings.length ? `${warnings.length} ${pluralize(warnings.length, "warning", "warnings")}` : "";
@@ -2372,6 +2470,13 @@ function toMomoa(srcRaw) {
2372
2470
  });
2373
2471
  }
2374
2472
 
2473
+ //#endregion
2474
+ //#region src/lib/array.ts
2475
+ /** JS compiler-optimizable comparator */
2476
+ function alphaComparator(a, b) {
2477
+ return a.localeCompare(b, "en-us", { numeric: true });
2478
+ }
2479
+
2375
2480
  //#endregion
2376
2481
  //#region src/lib/resolver-utils.ts
2377
2482
  /**
@@ -2393,8 +2498,32 @@ function filterResolverPaths(path) {
2393
2498
  }
2394
2499
  /** Make a deterministic string from an object */
2395
2500
  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]])));
2501
+ const keys = Object.keys(input).sort(alphaComparator);
2502
+ const sortedInput = {};
2503
+ for (const k of keys) sortedInput[k] = input[k];
2504
+ return JSON.stringify(sortedInput);
2505
+ }
2506
+ /**
2507
+ * Destructively merge B into A, with B overwriting A
2508
+ *
2509
+ * This is needed for resolvers because we need a really performant way to merge
2510
+ * token sets. merge-anything is a package we use for merging more complex
2511
+ * configurations like terrazzo.config.ts files, but that’s too slow for tokens.
2512
+ */
2513
+ function destructiveMerge(a, b) {
2514
+ if (!a || !b || typeof b !== "object") return;
2515
+ for (const k in b) {
2516
+ if (!Object.hasOwn(b, k)) continue;
2517
+ const b2 = b[k];
2518
+ if (b2 != null && typeof b2 === "object") if (Array.isArray(b2)) {
2519
+ a[k] = [];
2520
+ destructiveMerge(a[k], [...b2]);
2521
+ } else {
2522
+ if (!(k in a)) a[k] = {};
2523
+ destructiveMerge(a[k], { ...b2 });
2524
+ }
2525
+ else a[k] = b2;
2526
+ }
2398
2527
  }
2399
2528
 
2400
2529
  //#endregion
@@ -2455,15 +2584,12 @@ function normalize(token, { logger, src }) {
2455
2584
  switch (token.$type) {
2456
2585
  case "color":
2457
2586
  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
2587
  break;
2460
2588
  case "fontFamily":
2461
2589
  for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeFontFamily(token.mode[mode].$value);
2462
- token.$value = token.mode["."].$value;
2463
2590
  break;
2464
2591
  case "fontWeight":
2465
2592
  for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeFontWeight(token.mode[mode].$value);
2466
- token.$value = token.mode["."].$value;
2467
2593
  break;
2468
2594
  case "border":
2469
2595
  for (const mode of Object.keys(token.mode)) {
@@ -2471,7 +2597,6 @@ function normalize(token, { logger, src }) {
2471
2597
  if (!border || typeof border !== "object") continue;
2472
2598
  if (border.color) border.color = normalizeColor(border.color, getObjMember(token.mode[mode].source.node, "color"));
2473
2599
  }
2474
- token.$value = token.mode["."].$value;
2475
2600
  break;
2476
2601
  case "shadow":
2477
2602
  for (const mode of Object.keys(token.mode)) {
@@ -2485,7 +2610,6 @@ function normalize(token, { logger, src }) {
2485
2610
  if (!("inset" in shadow)) shadow.inset = false;
2486
2611
  }
2487
2612
  }
2488
- token.$value = token.mode["."].$value;
2489
2613
  break;
2490
2614
  case "gradient":
2491
2615
  for (const mode of Object.keys(token.mode)) {
@@ -2498,7 +2622,6 @@ function normalize(token, { logger, src }) {
2498
2622
  if (stop.color) stop.color = normalizeColor(stop.color, getObjMember(stopNode, "color"));
2499
2623
  }
2500
2624
  }
2501
- token.$value = token.mode["."].$value;
2502
2625
  break;
2503
2626
  case "typography":
2504
2627
  for (const mode of Object.keys(token.mode)) {
@@ -2513,7 +2636,6 @@ function normalize(token, { logger, src }) {
2513
2636
  break;
2514
2637
  }
2515
2638
  }
2516
- token.$value = token.mode["."].$value;
2517
2639
  break;
2518
2640
  }
2519
2641
  }
@@ -2532,6 +2654,7 @@ function aliasToTokenRef(alias, mode) {
2532
2654
  if (id === alias) return;
2533
2655
  return { $ref: `#/${id.replace(/~/g, "~0").replace(/\//g, "~1").replace(/\./g, "/")}${mode && mode !== "." ? `/$extensions/mode/${mode}` : ""}/$value` };
2534
2656
  }
2657
+ const cachedMatcher = new CachedWildcardMatcher();
2535
2658
  /** Generate a TokenNormalized from a Momoa node */
2536
2659
  function tokenFromNode(node, { groups, path, source, ignore }) {
2537
2660
  if (!(node.type === "Object" && !!getObjMember(node, "$value") && !path.includes("$extensions"))) return;
@@ -2549,33 +2672,45 @@ function tokenFromNode(node, { groups, path, source, ignore }) {
2549
2672
  $type: originalToken.$type || group.$type,
2550
2673
  $description: originalToken.$description || void 0,
2551
2674
  $deprecated: originalToken.$deprecated ?? group.$deprecated ?? void 0,
2552
- $value: originalToken.$value,
2675
+ get $value() {
2676
+ return this.mode["."].$value;
2677
+ },
2553
2678
  $extensions: originalToken.$extensions || void 0,
2554
2679
  $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,
2680
+ get aliasChain() {
2681
+ return this.mode["."].aliasChain;
2682
+ },
2683
+ get aliasedBy() {
2684
+ return this.mode["."].aliasedBy;
2685
+ },
2686
+ get aliasOf() {
2687
+ return this.mode["."].aliasOf;
2688
+ },
2689
+ get partialAliasOf() {
2690
+ return this.mode["."].partialAliasOf;
2691
+ },
2692
+ get dependencies() {
2693
+ return this.mode["."].dependencies;
2694
+ },
2560
2695
  group,
2561
2696
  originalValue: void 0,
2562
2697
  source: nodeSource,
2563
2698
  jsonID,
2564
2699
  mode: { ".": {
2565
2700
  $value: originalToken.$value,
2566
- aliasOf: void 0,
2567
2701
  aliasChain: void 0,
2568
- partialAliasOf: void 0,
2569
2702
  aliasedBy: void 0,
2570
- originalValue: void 0,
2703
+ aliasOf: void 0,
2704
+ partialAliasOf: void 0,
2571
2705
  dependencies: void 0,
2706
+ originalValue: void 0,
2572
2707
  source: {
2573
2708
  ...nodeSource,
2574
2709
  node: getObjMember(nodeSource.node, "$value") ?? nodeSource.node
2575
2710
  }
2576
2711
  } }
2577
2712
  };
2578
- if (ignore?.deprecated && token.$deprecated || ignore?.tokens && getTokenMatcher(ignore.tokens)(token.id)) return;
2713
+ if (ignore?.deprecated && token.$deprecated || ignore?.tokens && cachedMatcher.tokenIDMatch(ignore.tokens)(token.id)) return;
2579
2714
  const $extensions = getObjMember(node, "$extensions");
2580
2715
  if ($extensions) {
2581
2716
  const modeNode = getObjMember($extensions, "mode");
@@ -2684,7 +2819,7 @@ function graphAliases(refMap, { tokens, logger, sources }) {
2684
2819
  if (!modeValue) continue;
2685
2820
  if (!modeValue.dependencies) modeValue.dependencies = [];
2686
2821
  modeValue.dependencies.push(...refChain.filter((r) => !modeValue.dependencies.includes(r)));
2687
- modeValue.dependencies.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2822
+ modeValue.dependencies.sort(alphaComparator);
2688
2823
  if (jsonID.endsWith("/$value") || tokens[jsonID]) {
2689
2824
  modeValue.aliasOf = refToTokenID(refChain.at(-1));
2690
2825
  modeValue.aliasChain = [...refChain.map(refToTokenID)];
@@ -2728,26 +2863,16 @@ function graphAliases(refMap, { tokens, logger, sources }) {
2728
2863
  const aliasedByRefs = [jsonID, ...refChain].reverse();
2729
2864
  for (let i = 0; i < aliasedByRefs.length; i++) {
2730
2865
  const baseRef = getTokenRef(aliasedByRefs[i]);
2731
- const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef];
2866
+ const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef]?.mode["."];
2732
2867
  if (!baseToken) continue;
2733
2868
  const upstream = aliasedByRefs.slice(i + 1);
2734
2869
  if (!upstream.length) break;
2735
2870
  if (!baseToken.aliasedBy) baseToken.aliasedBy = [];
2736
2871
  for (let j = 0; j < upstream.length; j++) {
2737
2872
  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
- }
2873
+ if (!baseToken.aliasedBy.includes(downstream)) baseToken.aliasedBy.push(downstream);
2742
2874
  }
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;
2875
+ baseToken.aliasedBy.sort(alphaComparator);
2751
2876
  }
2752
2877
  }
2753
2878
  }
@@ -2884,7 +3009,6 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
2884
3009
  });
2885
3010
  if (!token.$type) token.$type = $type;
2886
3011
  if ($value) token.mode[mode].$value = $value;
2887
- if (mode === ".") token.$value = token.mode[mode].$value;
2888
3012
  }
2889
3013
  }
2890
3014
  }
@@ -3015,7 +3139,7 @@ function processTokens(rootSource, { config, logger, sourceByFilename, isResolve
3015
3139
  const tokenIDs = [];
3016
3140
  const groups = {};
3017
3141
  traverse(rootSource.document, { enter(node, _parent, rawPath) {
3018
- if (node.type !== "Object") return;
3142
+ if (node.type !== "Object" || rawPath.includes("$value") || rawPath.includes("$extensions")) return;
3019
3143
  groupFromNode(node, {
3020
3144
  path: isResolver ? filterResolverPaths(rawPath) : rawPath,
3021
3145
  groups
@@ -3045,6 +3169,7 @@ function processTokens(rootSource, { config, logger, sourceByFilename, isResolve
3045
3169
  });
3046
3170
  if (tokenRawValues && tokens[tokenRawValues?.jsonID]) {
3047
3171
  tokens[tokenRawValues.jsonID].originalValue = tokenRawValues.originalValue;
3172
+ tokens[tokenRawValues.jsonID].mode["."].originalValue = tokenRawValues.originalValue;
3048
3173
  tokens[tokenRawValues.jsonID].source = tokenRawValues.source;
3049
3174
  for (const mode of Object.keys(tokenRawValues.mode)) {
3050
3175
  tokens[tokenRawValues.jsonID].mode[mode].originalValue = tokenRawValues.mode[mode].originalValue;
@@ -3089,12 +3214,12 @@ function processTokens(rootSource, { config, logger, sourceByFilename, isResolve
3089
3214
  if (config.alphabetize === false) return tokens;
3090
3215
  const sortStart = performance.now();
3091
3216
  const tokensSorted = {};
3092
- tokenIDs.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3217
+ tokenIDs.sort(alphaComparator);
3093
3218
  for (const path of tokenIDs) {
3094
3219
  const id = refToTokenID(path);
3095
3220
  tokensSorted[id] = tokens[path];
3096
3221
  }
3097
- for (const group of Object.values(groups)) group.tokens.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3222
+ for (const group of Object.values(groups)) group.tokens.sort(alphaComparator);
3098
3223
  logger.debug({
3099
3224
  ...entry,
3100
3225
  message: "Sorted tokens",
@@ -3678,7 +3803,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3678
3803
  }
3679
3804
  return {
3680
3805
  apply(inputRaw) {
3681
- let tokensRaw = {};
3806
+ const tokensRaw = {};
3682
3807
  const input = {
3683
3808
  ...inputDefaults,
3684
3809
  ...inputRaw
@@ -3687,7 +3812,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3687
3812
  if (resolverCache[permutationID]) return resolverCache[permutationID];
3688
3813
  for (const item of resolverSource.resolutionOrder) switch (item.type) {
3689
3814
  case "set":
3690
- for (const s of item.sources) tokensRaw = merge(tokensRaw, s);
3815
+ for (const s of item.sources) destructiveMerge(tokensRaw, s);
3691
3816
  break;
3692
3817
  case "modifier": {
3693
3818
  const context = input[item.name];
@@ -3696,7 +3821,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3696
3821
  group: "resolver",
3697
3822
  message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`
3698
3823
  });
3699
- for (const s of sources ?? []) tokensRaw = merge(tokensRaw, s);
3824
+ for (const s of sources ?? []) destructiveMerge(tokensRaw, s);
3700
3825
  break;
3701
3826
  }
3702
3827
  }
@@ -3734,6 +3859,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3734
3859
  return false;
3735
3860
  }
3736
3861
  for (const [name, contexts] of Object.entries(validContexts)) if (name in input) {
3862
+ if (name === "tzMode") continue;
3737
3863
  if (!contexts.includes(input[name])) {
3738
3864
  if (throwError) logger.error({
3739
3865
  group: "resolver",
@@ -3783,7 +3909,6 @@ function calculatePermutations(options) {
3783
3909
  async function createSyntheticResolver(tokens, { config, logger, req, sources }) {
3784
3910
  const contexts = {};
3785
3911
  for (const token of Object.values(tokens)) for (const [mode, value] of Object.entries(token.mode)) {
3786
- if (mode === ".") continue;
3787
3912
  if (!(mode in contexts)) contexts[mode] = [{}];
3788
3913
  addToken(contexts[mode][0], {
3789
3914
  ...token,
@@ -3797,7 +3922,8 @@ async function createSyntheticResolver(tokens, { config, logger, req, sources })
3797
3922
  sets: { allTokens: { sources: [simpleFlatten(tokens, { logger })] } },
3798
3923
  modifiers: { tzMode: {
3799
3924
  description: "Automatically built from $extensions.mode",
3800
- contexts
3925
+ contexts,
3926
+ default: "."
3801
3927
  } }
3802
3928
  }, void 0, 2);
3803
3929
  return createResolver(await normalizeResolver(momoa.parse(src), {
@@ -3954,6 +4080,16 @@ async function parse(_input, { logger = new Logger(), req = defaultReq, skipLint
3954
4080
  let tokens = {};
3955
4081
  let resolver;
3956
4082
  let sources = [];
4083
+ if (inputs.length === 0) logger.error({
4084
+ group: "parser",
4085
+ label: "init",
4086
+ message: "Nothing to parse."
4087
+ });
4088
+ 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({
4089
+ group: "parser",
4090
+ label: "init",
4091
+ message: `Input ${i}: expected { src: any; filename: URL }`
4092
+ });
3957
4093
  const totalStart = performance.now();
3958
4094
  const initStart = performance.now();
3959
4095
  const resolverResult = await loadResolver(inputs, {