@terrazzo/parser 2.0.0-beta.3 → 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,8 +1,7 @@
1
- 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";
2
2
  import * as momoa from "@humanwhocodes/momoa";
3
3
  import pc from "picocolors";
4
4
  import { merge } from "merge-anything";
5
- 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, isAlias, parseAlias, parseColor, pluralize, tokenToColor } from "@terrazzo/token-tools";
6
5
  import { contrastWCAG21, inGamut } from "colorjs.io/fn";
7
6
  import { camelCase, kebabCase, pascalCase, snakeCase } from "scule";
8
7
  import { bundle, encodeFragment, findNode, getObjMember, getObjMembers, isPure$ref, maybeRawJSON, mergeObjects, parseRef, replaceNode, traverse } from "@terrazzo/json-schema-tools";
@@ -96,9 +95,20 @@ const LOG_ORDER = [
96
95
  "info",
97
96
  "debug"
98
97
  ];
98
+ const GROUP_COLOR = {
99
+ config: pc.cyan,
100
+ import: pc.green,
101
+ lint: pc.yellowBright,
102
+ parser: pc.magenta,
103
+ plugin: pc.greenBright,
104
+ resolver: pc.magentaBright,
105
+ server: pc.gray
106
+ };
99
107
  const MESSAGE_COLOR = {
100
108
  error: pc.red,
101
- warn: pc.yellow
109
+ warn: pc.yellow,
110
+ info: (msg) => msg,
111
+ debug: pc.gray
102
112
  };
103
113
  const timeFormatter = new Intl.DateTimeFormat("en-us", {
104
114
  hour: "numeric",
@@ -113,9 +123,10 @@ const timeFormatter = new Intl.DateTimeFormat("en-us", {
113
123
  * @return {string}
114
124
  */
115
125
  function formatMessage(entry, severity) {
126
+ const groupColor = GROUP_COLOR[entry.group];
127
+ const messageColor = MESSAGE_COLOR[severity];
116
128
  let message = entry.message;
117
- message = `[${entry.group}${entry.label ? `:${entry.label}` : ""}] ${message}`;
118
- if (severity in MESSAGE_COLOR) message = MESSAGE_COLOR[severity](message);
129
+ message = `${groupColor(`${entry.group}${entry.label ? `:${entry.label}` : ""}:`)} ${messageColor(message)}`;
119
130
  if (typeof entry.timing === "number") message = `${message} ${formatTiming(entry.timing)}`;
120
131
  if (entry.node) {
121
132
  const start = entry.node?.loc?.start ?? {
@@ -128,6 +139,7 @@ function formatMessage(entry, severity) {
128
139
  }
129
140
  return message;
130
141
  }
142
+ const debugMatch = new CachedWildcardMatcher();
131
143
  var Logger = class {
132
144
  level = "info";
133
145
  debugScope = "*";
@@ -179,7 +191,7 @@ var Logger = class {
179
191
  this.debugCount++;
180
192
  let message = formatMessage(entry, "debug");
181
193
  const debugPrefix = entry.label ? `${entry.group}:${entry.label}` : entry.group;
182
- if (this.debugScope !== "*" && !wcmatch(this.debugScope)(debugPrefix)) return;
194
+ if (this.debugScope !== "*" && !debugMatch.match(this.debugScope)(debugPrefix)) return;
183
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));
184
196
  message = `${pc.dim(timeFormatter.format(performance.now()))} ${message}`;
185
197
  if (typeof entry.timing === "number") message = `${message} ${formatTiming(entry.timing)}`;
@@ -231,6 +243,7 @@ function validateTransformParams({ params, logger, pluginName }) {
231
243
  });
232
244
  }
233
245
  const FALLBACK_PERMUTATION_ID = JSON.stringify({ tzMode: "*" });
246
+ const cachedMatcher$1 = new CachedWildcardMatcher();
234
247
  /** Run build stage */
235
248
  async function build(tokens, { resolver, sources, logger = new Logger(), config }) {
236
249
  const formats = {};
@@ -245,84 +258,95 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
245
258
  });
246
259
  return [];
247
260
  }
248
- const tokenMatcher = params.id && params.id !== "*" ? wcmatch(params.id) : null;
249
- const modeMatcher = params.mode ? wcmatch(params.mode) : null;
250
- const permutationID = params.input ? resolver.getPermutationID(params.input) : JSON.stringify({ tzMode: "*" });
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;
251
268
  return (formats[params.format]?.[permutationID] ?? []).filter((token) => {
252
- if (params.$type) {
253
- if (typeof params.$type === "string" && token.token.$type !== params.$type) return false;
254
- else if (Array.isArray(params.$type) && !params.$type.some(($type) => token.token.$type === $type)) return false;
255
- }
256
- if (tokenMatcher && !tokenMatcher(token.token.id)) return false;
257
- if (params.input && token.permutationID !== resolver.getPermutationID(params.input)) return false;
258
- if (modeMatcher && !modeMatcher(token.mode)) return false;
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;
259
272
  return true;
260
273
  });
261
274
  };
262
275
  }
263
276
  let transformsLocked = false;
264
277
  const startTransform = performance.now();
265
- for (const plugin of config.plugins) if (typeof plugin.transform === "function") await plugin.transform({
266
- context: { logger },
267
- tokens,
268
- sources,
269
- getTransforms: getTransforms(plugin.name),
270
- setTransform(id, params) {
271
- if (transformsLocked) {
272
- logger.warn({
273
- 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({
274
296
  group: "plugin",
275
- label: plugin.name
297
+ label: plugin.name,
298
+ message: `No token "${id}"`
276
299
  });
277
- return;
278
- }
279
- const token = tokens[id];
280
- const permutationID = params.input ? resolver.getPermutationID(params.input) : FALLBACK_PERMUTATION_ID;
281
- const cleanValue = typeof params.value === "string" ? params.value : { ...params.value };
282
- validateTransformParams({
283
- logger,
284
- params: {
285
- ...params,
286
- value: cleanValue
287
- },
288
- pluginName: plugin.name
289
- });
290
- if (!formats[params.format]) formats[params.format] = {};
291
- if (!formats[params.format][permutationID]) formats[params.format][permutationID] = [];
292
- let foundTokenI = -1;
293
- if (params.mode) foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && params.mode === t.mode);
294
- else if (params.input) {
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] = {};
295
313
  if (!formats[params.format][permutationID]) formats[params.format][permutationID] = [];
296
- foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && permutationID === t.permutationID);
297
- } else foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID));
298
- if (foundTokenI === -1) formats[params.format][permutationID].push({
299
- ...params,
300
- id,
301
- value: cleanValue,
302
- type: typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE,
303
- mode: params.mode || ".",
304
- token: structuredClone(token),
305
- permutationID,
306
- input: JSON.parse(permutationID)
307
- });
308
- else {
309
- formats[params.format][permutationID][foundTokenI].value = cleanValue;
310
- formats[params.format][permutationID][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
311
- }
312
- },
313
- resolver
314
- });
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
+ }
315
339
  transformsLocked = true;
316
340
  logger.debug({
317
341
  group: "parser",
318
342
  label: "transform",
319
- message: "transform() step",
343
+ message: "All plugins finished transform()",
320
344
  timing: performance.now() - startTransform
321
345
  });
322
346
  const startBuild = performance.now();
323
347
  await Promise.all(config.plugins.map(async (plugin) => {
324
348
  if (typeof plugin.build === "function") {
325
- const pluginBuildStart = performance.now();
349
+ const pb = performance.now();
326
350
  await plugin.build({
327
351
  context: { logger },
328
352
  tokens,
@@ -340,18 +364,25 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
340
364
  filename,
341
365
  contents,
342
366
  plugin: plugin.name,
343
- time: performance.now() - pluginBuildStart
367
+ time: performance.now() - pb
344
368
  });
345
369
  }
346
370
  });
371
+ logger.debug({
372
+ group: "plugin",
373
+ label: plugin.name,
374
+ message: "build()",
375
+ timing: performance.now() - pb
376
+ });
347
377
  }
348
378
  }));
349
379
  logger.debug({
350
380
  group: "parser",
351
381
  label: "build",
352
- message: "build() step",
382
+ message: "All plugins finished build()",
353
383
  timing: performance.now() - startBuild
354
384
  });
385
+ cachedMatcher$1.reset();
355
386
  const startBuildEnd = performance.now();
356
387
  await Promise.all(config.plugins.map(async (plugin) => plugin.buildEnd?.({
357
388
  context: { logger },
@@ -368,6 +399,65 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
368
399
  });
369
400
  return result;
370
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
+ }
371
461
 
372
462
  //#endregion
373
463
  //#region src/lint/plugin-core/lib/docs.ts
@@ -423,6 +513,22 @@ const rule$26 = {
423
513
  }
424
514
  };
425
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
+
426
532
  //#endregion
427
533
  //#region src/lint/plugin-core/rules/a11y-min-font-size.ts
428
534
  const A11Y_MIN_FONT_SIZE = "a11y/min-font-size";
@@ -438,7 +544,7 @@ const rule$25 = {
438
544
  defaultOptions: {},
439
545
  create({ tokens, options, report }) {
440
546
  if (!options.minSizePx && !options.minSizeRem) throw new Error("Must specify at least one of minSizePx or minSizeRem");
441
- const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
547
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
442
548
  for (const t of Object.values(tokens)) {
443
549
  if (shouldIgnore?.(t.id)) continue;
444
550
  if (t.aliasOf) continue;
@@ -479,7 +585,7 @@ const rule$24 = {
479
585
  defaultOptions: { colorSpace: "srgb" },
480
586
  create({ tokens, options, report }) {
481
587
  if (!options.colorSpace) return;
482
- const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
588
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
483
589
  for (const t of Object.values(tokens)) {
484
590
  if (shouldIgnore?.(t.id)) continue;
485
591
  if (t.aliasOf) continue;
@@ -590,7 +696,7 @@ const rule$22 = {
590
696
  },
591
697
  defaultOptions: {},
592
698
  create({ tokens, options, report }) {
593
- const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
699
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
594
700
  for (const t of Object.values(tokens)) {
595
701
  if (shouldIgnore?.(t.id)) continue;
596
702
  if (!t.$description) report({
@@ -617,7 +723,7 @@ const rule$21 = {
617
723
  defaultOptions: {},
618
724
  create({ report, tokens, options }) {
619
725
  const values = {};
620
- const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
726
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
621
727
  for (const t of Object.values(tokens)) {
622
728
  if (shouldIgnore?.(t.id)) continue;
623
729
  if (!values[t.$type]) values[t.$type] = /* @__PURE__ */ new Set();
@@ -670,7 +776,7 @@ const rule$20 = {
670
776
  create({ tokens, options, report }) {
671
777
  if (!options?.gamut) return;
672
778
  if (options.gamut !== "srgb" && options.gamut !== "p3" && options.gamut !== "rec2020") throw new Error(`Unknown gamut "${options.gamut}". Options are "srgb", "p3", or "rec2020"`);
673
- const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
779
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
674
780
  for (const t of Object.values(tokens)) {
675
781
  if (shouldIgnore?.(t.id)) continue;
676
782
  if (t.aliasOf) continue;
@@ -749,7 +855,7 @@ const rule$19 = {
749
855
  const { match, requiredTokens, requiredGroups } = options.matches[matchI];
750
856
  if (!match.length) throw new Error(`Match ${matchI}: must declare \`match: […]\``);
751
857
  if (!requiredTokens?.length && !requiredGroups?.length) throw new Error(`Match ${matchI}: must declare either \`requiredTokens: […]\` or \`requiredGroups: […]\``);
752
- const matcher = wcmatch(match);
858
+ const matcher = cachedLintMatcher.tokenIDMatch(match);
753
859
  const matchGroups = [];
754
860
  const matchTokens = [];
755
861
  let tokensMatched = false;
@@ -804,7 +910,7 @@ const rule$18 = {
804
910
  const { match, modes } = options.matches[matchI];
805
911
  if (!match.length) throw new Error(`Match ${matchI}: must declare \`match: […]\``);
806
912
  if (!modes?.length) throw new Error(`Match ${matchI}: must declare \`modes: […]\``);
807
- const matcher = wcmatch(match);
913
+ const matcher = cachedLintMatcher.tokenIDMatch(match);
808
914
  let tokensMatched = false;
809
915
  for (const t of Object.values(tokens)) {
810
916
  if (!matcher(t.id)) continue;
@@ -865,7 +971,7 @@ const rule$16 = {
865
971
  create({ tokens, options, report }) {
866
972
  if (!options) return;
867
973
  if (!options.properties.length) throw new Error(`"properties" can’t be empty`);
868
- const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
974
+ const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : null;
869
975
  for (const t of Object.values(tokens)) {
870
976
  if (shouldIgnore?.(t.id)) continue;
871
977
  if (t.$type !== "typography") continue;
@@ -1376,7 +1482,7 @@ const rule$6 = {
1376
1482
  "lineHeight"
1377
1483
  ] },
1378
1484
  create({ tokens, options, report }) {
1379
- const isIgnored = options.ignore ? wcmatch(options.ignore) : () => false;
1485
+ const isIgnored = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : () => false;
1380
1486
  for (const t of Object.values(tokens)) {
1381
1487
  if (t.aliasOf || !t.originalValue || t.$type !== "typography" || isIgnored(t.id)) continue;
1382
1488
  validateTypography(t.originalValue.$value, {
@@ -2100,13 +2206,11 @@ function normalizeTokens({ rawConfig, config, logger, cwd }) {
2100
2206
  for (const file of rawConfig.tokens) if (typeof file === "string" || file instanceof URL) config.tokens.push(file);
2101
2207
  else logger.error({
2102
2208
  group: "config",
2103
- label: "tokens",
2104
- message: `Expected array of strings, encountered ${JSON.stringify(file)}`
2209
+ message: `tokens: Expected array of strings, encountered ${JSON.stringify(file)}`
2105
2210
  });
2106
2211
  } else logger.error({
2107
2212
  group: "config",
2108
- label: "tokens",
2109
- message: `Expected string or array of strings, received ${typeof rawConfig.tokens}`
2213
+ message: `tokens: Expected string or array of strings, received ${typeof rawConfig.tokens}`
2110
2214
  });
2111
2215
  for (let i = 0; i < config.tokens.length; i++) {
2112
2216
  const filepath = config.tokens[i];
@@ -2128,8 +2232,7 @@ function normalizeOutDir({ config, cwd, logger }) {
2128
2232
  if (config.outDir instanceof URL) {} else if (typeof config.outDir === "undefined") config.outDir = new URL("./tokens/", cwd);
2129
2233
  else if (typeof config.outDir !== "string") logger.error({
2130
2234
  group: "config",
2131
- label: "outDir",
2132
- message: `Expected string, received ${JSON.stringify(config.outDir)}`
2235
+ message: `outDir: Expected string, received ${JSON.stringify(config.outDir)}`
2133
2236
  });
2134
2237
  else {
2135
2238
  config.outDir = new URL(config.outDir, cwd);
@@ -2141,16 +2244,14 @@ function normalizePlugins({ config, logger }) {
2141
2244
  if (typeof config.plugins === "undefined") config.plugins = [];
2142
2245
  if (!Array.isArray(config.plugins)) logger.error({
2143
2246
  group: "config",
2144
- label: "plugins",
2145
- message: `Expected array of plugins, received ${JSON.stringify(config.plugins)}`
2247
+ message: `plugins: Expected array of plugins, received ${JSON.stringify(config.plugins)}`
2146
2248
  });
2147
2249
  config.plugins.push(coreLintPlugin());
2148
2250
  for (let n = 0; n < config.plugins.length; n++) {
2149
2251
  const plugin = config.plugins[n];
2150
2252
  if (typeof plugin !== "object") logger.error({
2151
2253
  group: "config",
2152
- label: `plugin[${n}]`,
2153
- message: `Expected output plugin, received ${JSON.stringify(plugin)}`
2254
+ message: `plugin#${n}: Expected output plugin, received ${JSON.stringify(plugin)}`
2154
2255
  });
2155
2256
  else if (!plugin.name) logger.error({
2156
2257
  group: "config",
@@ -2175,8 +2276,7 @@ function normalizeLint({ config, logger }) {
2175
2276
  if (config.lint.build.enabled !== void 0) {
2176
2277
  if (typeof config.lint.build.enabled !== "boolean") logger.error({
2177
2278
  group: "config",
2178
- label: "lint build enabled",
2179
- message: `Expected boolean, received ${JSON.stringify(config.lint.build)}`
2279
+ message: `lint.build.enabled: Expected boolean, received ${JSON.stringify(config.lint.build)}`
2180
2280
  });
2181
2281
  } else config.lint.build.enabled = true;
2182
2282
  if (config.lint.rules === void 0) config.lint.rules = { ...RECOMMENDED_CONFIG };
@@ -2184,8 +2284,7 @@ function normalizeLint({ config, logger }) {
2184
2284
  if (config.lint.rules === null || typeof config.lint.rules !== "object" || Array.isArray(config.lint.rules)) {
2185
2285
  logger.error({
2186
2286
  group: "config",
2187
- label: "lint rules",
2188
- message: `Expected object, received ${JSON.stringify(config.lint.rules)}`
2287
+ message: `lint.rules: Expected object, received ${JSON.stringify(config.lint.rules)}`
2189
2288
  });
2190
2289
  return;
2191
2290
  }
@@ -2196,16 +2295,14 @@ function normalizeLint({ config, logger }) {
2196
2295
  if (!pluginRules || Array.isArray(pluginRules) || typeof pluginRules !== "object") {
2197
2296
  logger.error({
2198
2297
  group: "config",
2199
- label: `plugin ${plugin.name}`,
2200
- message: `Expected object for lint() received ${JSON.stringify(pluginRules)}`
2298
+ message: `${plugin.name}: Expected object for lint() received ${JSON.stringify(pluginRules)}`
2201
2299
  });
2202
2300
  continue;
2203
2301
  }
2204
2302
  for (const rule of Object.keys(pluginRules)) {
2205
2303
  if (allRules.get(rule) && allRules.get(rule) !== plugin.name) logger.error({
2206
2304
  group: "config",
2207
- label: `plugin ${plugin.name}`,
2208
- message: `Duplicate rule ${rule} already registered by plugin ${allRules.get(rule)}`
2305
+ message: `${plugin.name}: Duplicate rule ${rule} already registered by plugin ${allRules.get(rule)}`
2209
2306
  });
2210
2307
  allRules.set(rule, plugin.name);
2211
2308
  }
@@ -2213,8 +2310,7 @@ function normalizeLint({ config, logger }) {
2213
2310
  for (const id of Object.keys(config.lint.rules)) {
2214
2311
  if (!allRules.has(id)) logger.error({
2215
2312
  group: "config",
2216
- label: `lint rule ${id}`,
2217
- message: "Unknown rule. Is the plugin installed?"
2313
+ message: `lint.rules.${id}: Unknown rule. Is the plugin installed?`
2218
2314
  });
2219
2315
  const value = config.lint.rules[id];
2220
2316
  let severity = "off";
@@ -2225,15 +2321,13 @@ function normalizeLint({ config, logger }) {
2225
2321
  options = value[1];
2226
2322
  } else if (value !== void 0) logger.error({
2227
2323
  group: "config",
2228
- label: `lint rule ${id}`,
2229
- message: `Invalid eyntax. Expected \`string | number | Array\`, received ${JSON.stringify(value)}}`
2324
+ message: `lint.rules.${id}: Invalid syntax. Expected \`string | number | Array\`, received ${JSON.stringify(value)}}`
2230
2325
  });
2231
2326
  config.lint.rules[id] = [severity, options];
2232
2327
  if (typeof severity === "number") {
2233
2328
  if (severity !== 0 && severity !== 1 && severity !== 2) logger.error({
2234
2329
  group: "config",
2235
- label: `lint rule ${id}`,
2236
- message: `Invalid number ${severity}. Specify 0 (off), 1 (warn), or 2 (error).`
2330
+ message: `lint.rules.${id}: Invalid number ${severity}. Specify 0 (off), 1 (warn), or 2 (error).`
2237
2331
  });
2238
2332
  config.lint.rules[id][0] = [
2239
2333
  "off",
@@ -2243,13 +2337,11 @@ function normalizeLint({ config, logger }) {
2243
2337
  } else if (typeof severity === "string") {
2244
2338
  if (severity !== "off" && severity !== "warn" && severity !== "error") logger.error({
2245
2339
  group: "config",
2246
- label: `lint rule ${id}`,
2247
- message: `Invalid string ${JSON.stringify(severity)}. Specify "off", "warn", or "error".`
2340
+ message: `lint.rules.${id}: Invalid string ${JSON.stringify(severity)}. Specify "off", "warn", or "error".`
2248
2341
  });
2249
2342
  } else if (value !== null) logger.error({
2250
2343
  group: "config",
2251
- label: `lint rule ${id}`,
2252
- message: `Expected string or number, received ${JSON.stringify(value)}`
2344
+ message: `lint.rules.${id}: Expected string or number, received ${JSON.stringify(value)}`
2253
2345
  });
2254
2346
  }
2255
2347
  }
@@ -2264,13 +2356,11 @@ function normalizeIgnore({ config, logger }) {
2264
2356
  config.ignore.deprecated ??= false;
2265
2357
  if (!Array.isArray(config.ignore.tokens) || config.ignore.tokens.some((x) => typeof x !== "string")) logger.error({
2266
2358
  group: "config",
2267
- label: "ignore tokens",
2268
- message: `Expected array of strings, received ${JSON.stringify(config.ignore.tokens)}`
2359
+ message: `ignore.tokens: Expected array of strings, received ${JSON.stringify(config.ignore.tokens)}`
2269
2360
  });
2270
2361
  if (typeof config.ignore.deprecated !== "boolean") logger.error({
2271
2362
  group: "config",
2272
- label: "ignore deprecated",
2273
- message: `Expected boolean, received ${JSON.stringify(config.ignore.deprecated)}`
2363
+ message: `ignore.deprecated: Expected boolean, received ${JSON.stringify(config.ignore.deprecated)}`
2274
2364
  });
2275
2365
  }
2276
2366
  /** Merge configs */
@@ -2345,6 +2435,7 @@ async function lintRunner({ tokens, filename, config = {}, sources, logger }) {
2345
2435
  message: "Finished",
2346
2436
  timing: performance.now() - s
2347
2437
  });
2438
+ cachedLintMatcher.reset();
2348
2439
  }
2349
2440
  const errCount = errors.length ? `${errors.length} ${pluralize(errors.length, "error", "errors")}` : "";
2350
2441
  const warnCount = warnings.length ? `${warnings.length} ${pluralize(warnings.length, "warning", "warnings")}` : "";
@@ -2376,6 +2467,13 @@ function toMomoa(srcRaw) {
2376
2467
  });
2377
2468
  }
2378
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
+
2379
2477
  //#endregion
2380
2478
  //#region src/lib/resolver-utils.ts
2381
2479
  /**
@@ -2397,8 +2495,32 @@ function filterResolverPaths(path) {
2397
2495
  }
2398
2496
  /** Make a deterministic string from an object */
2399
2497
  function getPermutationID(input) {
2400
- const keys = Object.keys(input).sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2401
- 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
+ }
2402
2524
  }
2403
2525
 
2404
2526
  //#endregion
@@ -2459,15 +2581,12 @@ function normalize(token, { logger, src }) {
2459
2581
  switch (token.$type) {
2460
2582
  case "color":
2461
2583
  for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeColor(token.mode[mode].$value, token.mode[mode].source.node);
2462
- token.$value = token.mode["."].$value;
2463
2584
  break;
2464
2585
  case "fontFamily":
2465
2586
  for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeFontFamily(token.mode[mode].$value);
2466
- token.$value = token.mode["."].$value;
2467
2587
  break;
2468
2588
  case "fontWeight":
2469
2589
  for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeFontWeight(token.mode[mode].$value);
2470
- token.$value = token.mode["."].$value;
2471
2590
  break;
2472
2591
  case "border":
2473
2592
  for (const mode of Object.keys(token.mode)) {
@@ -2475,7 +2594,6 @@ function normalize(token, { logger, src }) {
2475
2594
  if (!border || typeof border !== "object") continue;
2476
2595
  if (border.color) border.color = normalizeColor(border.color, getObjMember(token.mode[mode].source.node, "color"));
2477
2596
  }
2478
- token.$value = token.mode["."].$value;
2479
2597
  break;
2480
2598
  case "shadow":
2481
2599
  for (const mode of Object.keys(token.mode)) {
@@ -2489,7 +2607,6 @@ function normalize(token, { logger, src }) {
2489
2607
  if (!("inset" in shadow)) shadow.inset = false;
2490
2608
  }
2491
2609
  }
2492
- token.$value = token.mode["."].$value;
2493
2610
  break;
2494
2611
  case "gradient":
2495
2612
  for (const mode of Object.keys(token.mode)) {
@@ -2502,7 +2619,6 @@ function normalize(token, { logger, src }) {
2502
2619
  if (stop.color) stop.color = normalizeColor(stop.color, getObjMember(stopNode, "color"));
2503
2620
  }
2504
2621
  }
2505
- token.$value = token.mode["."].$value;
2506
2622
  break;
2507
2623
  case "typography":
2508
2624
  for (const mode of Object.keys(token.mode)) {
@@ -2517,7 +2633,6 @@ function normalize(token, { logger, src }) {
2517
2633
  break;
2518
2634
  }
2519
2635
  }
2520
- token.$value = token.mode["."].$value;
2521
2636
  break;
2522
2637
  }
2523
2638
  }
@@ -2536,6 +2651,7 @@ function aliasToTokenRef(alias, mode) {
2536
2651
  if (id === alias) return;
2537
2652
  return { $ref: `#/${id.replace(/~/g, "~0").replace(/\//g, "~1").replace(/\./g, "/")}${mode && mode !== "." ? `/$extensions/mode/${mode}` : ""}/$value` };
2538
2653
  }
2654
+ const cachedMatcher = new CachedWildcardMatcher();
2539
2655
  /** Generate a TokenNormalized from a Momoa node */
2540
2656
  function tokenFromNode(node, { groups, path, source, ignore }) {
2541
2657
  if (!(node.type === "Object" && !!getObjMember(node, "$value") && !path.includes("$extensions"))) return;
@@ -2553,33 +2669,45 @@ function tokenFromNode(node, { groups, path, source, ignore }) {
2553
2669
  $type: originalToken.$type || group.$type,
2554
2670
  $description: originalToken.$description || void 0,
2555
2671
  $deprecated: originalToken.$deprecated ?? group.$deprecated ?? void 0,
2556
- $value: originalToken.$value,
2672
+ get $value() {
2673
+ return this.mode["."].$value;
2674
+ },
2557
2675
  $extensions: originalToken.$extensions || void 0,
2558
2676
  $extends: originalToken.$extends || void 0,
2559
- aliasChain: void 0,
2560
- aliasedBy: void 0,
2561
- aliasOf: void 0,
2562
- partialAliasOf: void 0,
2563
- 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
+ },
2564
2692
  group,
2565
2693
  originalValue: void 0,
2566
2694
  source: nodeSource,
2567
2695
  jsonID,
2568
2696
  mode: { ".": {
2569
2697
  $value: originalToken.$value,
2570
- aliasOf: void 0,
2571
2698
  aliasChain: void 0,
2572
- partialAliasOf: void 0,
2573
2699
  aliasedBy: void 0,
2574
- originalValue: void 0,
2700
+ aliasOf: void 0,
2701
+ partialAliasOf: void 0,
2575
2702
  dependencies: void 0,
2703
+ originalValue: void 0,
2576
2704
  source: {
2577
2705
  ...nodeSource,
2578
2706
  node: getObjMember(nodeSource.node, "$value") ?? nodeSource.node
2579
2707
  }
2580
2708
  } }
2581
2709
  };
2582
- if (ignore?.deprecated && token.$deprecated || ignore?.tokens && wcmatch(ignore.tokens)(token.id)) return;
2710
+ if (ignore?.deprecated && token.$deprecated || ignore?.tokens && cachedMatcher.tokenIDMatch(ignore.tokens)(token.id)) return;
2583
2711
  const $extensions = getObjMember(node, "$extensions");
2584
2712
  if ($extensions) {
2585
2713
  const modeNode = getObjMember($extensions, "mode");
@@ -2688,7 +2816,7 @@ function graphAliases(refMap, { tokens, logger, sources }) {
2688
2816
  if (!modeValue) continue;
2689
2817
  if (!modeValue.dependencies) modeValue.dependencies = [];
2690
2818
  modeValue.dependencies.push(...refChain.filter((r) => !modeValue.dependencies.includes(r)));
2691
- modeValue.dependencies.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2819
+ modeValue.dependencies.sort(alphaComparator);
2692
2820
  if (jsonID.endsWith("/$value") || tokens[jsonID]) {
2693
2821
  modeValue.aliasOf = refToTokenID(refChain.at(-1));
2694
2822
  modeValue.aliasChain = [...refChain.map(refToTokenID)];
@@ -2732,26 +2860,16 @@ function graphAliases(refMap, { tokens, logger, sources }) {
2732
2860
  const aliasedByRefs = [jsonID, ...refChain].reverse();
2733
2861
  for (let i = 0; i < aliasedByRefs.length; i++) {
2734
2862
  const baseRef = getTokenRef(aliasedByRefs[i]);
2735
- const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef];
2863
+ const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef]?.mode["."];
2736
2864
  if (!baseToken) continue;
2737
2865
  const upstream = aliasedByRefs.slice(i + 1);
2738
2866
  if (!upstream.length) break;
2739
2867
  if (!baseToken.aliasedBy) baseToken.aliasedBy = [];
2740
2868
  for (let j = 0; j < upstream.length; j++) {
2741
2869
  const downstream = refToTokenID(upstream[j]);
2742
- if (!baseToken.aliasedBy.includes(downstream)) {
2743
- baseToken.aliasedBy.push(downstream);
2744
- if (mode === ".") tokens[baseRef].aliasedBy = baseToken.aliasedBy;
2745
- }
2870
+ if (!baseToken.aliasedBy.includes(downstream)) baseToken.aliasedBy.push(downstream);
2746
2871
  }
2747
- baseToken.aliasedBy.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2748
- }
2749
- if (mode === ".") {
2750
- tokens[rootRef].aliasChain = modeValue.aliasChain;
2751
- tokens[rootRef].aliasedBy = modeValue.aliasedBy;
2752
- tokens[rootRef].aliasOf = modeValue.aliasOf;
2753
- tokens[rootRef].dependencies = modeValue.dependencies;
2754
- tokens[rootRef].partialAliasOf = modeValue.partialAliasOf;
2872
+ baseToken.aliasedBy.sort(alphaComparator);
2755
2873
  }
2756
2874
  }
2757
2875
  }
@@ -2888,7 +3006,6 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
2888
3006
  });
2889
3007
  if (!token.$type) token.$type = $type;
2890
3008
  if ($value) token.mode[mode].$value = $value;
2891
- if (mode === ".") token.$value = token.mode[mode].$value;
2892
3009
  }
2893
3010
  }
2894
3011
  }
@@ -3049,6 +3166,7 @@ function processTokens(rootSource, { config, logger, sourceByFilename, isResolve
3049
3166
  });
3050
3167
  if (tokenRawValues && tokens[tokenRawValues?.jsonID]) {
3051
3168
  tokens[tokenRawValues.jsonID].originalValue = tokenRawValues.originalValue;
3169
+ tokens[tokenRawValues.jsonID].mode["."].originalValue = tokenRawValues.originalValue;
3052
3170
  tokens[tokenRawValues.jsonID].source = tokenRawValues.source;
3053
3171
  for (const mode of Object.keys(tokenRawValues.mode)) {
3054
3172
  tokens[tokenRawValues.jsonID].mode[mode].originalValue = tokenRawValues.mode[mode].originalValue;
@@ -3093,12 +3211,12 @@ function processTokens(rootSource, { config, logger, sourceByFilename, isResolve
3093
3211
  if (config.alphabetize === false) return tokens;
3094
3212
  const sortStart = performance.now();
3095
3213
  const tokensSorted = {};
3096
- tokenIDs.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3214
+ tokenIDs.sort(alphaComparator);
3097
3215
  for (const path of tokenIDs) {
3098
3216
  const id = refToTokenID(path);
3099
3217
  tokensSorted[id] = tokens[path];
3100
3218
  }
3101
- 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);
3102
3220
  logger.debug({
3103
3221
  ...entry,
3104
3222
  message: "Sorted tokens",
@@ -3682,7 +3800,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3682
3800
  }
3683
3801
  return {
3684
3802
  apply(inputRaw) {
3685
- let tokensRaw = {};
3803
+ const tokensRaw = {};
3686
3804
  const input = {
3687
3805
  ...inputDefaults,
3688
3806
  ...inputRaw
@@ -3691,7 +3809,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3691
3809
  if (resolverCache[permutationID]) return resolverCache[permutationID];
3692
3810
  for (const item of resolverSource.resolutionOrder) switch (item.type) {
3693
3811
  case "set":
3694
- for (const s of item.sources) tokensRaw = merge(tokensRaw, s);
3812
+ for (const s of item.sources) destructiveMerge(tokensRaw, s);
3695
3813
  break;
3696
3814
  case "modifier": {
3697
3815
  const context = input[item.name];
@@ -3700,7 +3818,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3700
3818
  group: "resolver",
3701
3819
  message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`
3702
3820
  });
3703
- for (const s of sources ?? []) tokensRaw = merge(tokensRaw, s);
3821
+ for (const s of sources ?? []) destructiveMerge(tokensRaw, s);
3704
3822
  break;
3705
3823
  }
3706
3824
  }
@@ -3738,6 +3856,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3738
3856
  return false;
3739
3857
  }
3740
3858
  for (const [name, contexts] of Object.entries(validContexts)) if (name in input) {
3859
+ if (name === "tzMode") continue;
3741
3860
  if (!contexts.includes(input[name])) {
3742
3861
  if (throwError) logger.error({
3743
3862
  group: "resolver",
@@ -3787,7 +3906,6 @@ function calculatePermutations(options) {
3787
3906
  async function createSyntheticResolver(tokens, { config, logger, req, sources }) {
3788
3907
  const contexts = {};
3789
3908
  for (const token of Object.values(tokens)) for (const [mode, value] of Object.entries(token.mode)) {
3790
- if (mode === ".") continue;
3791
3909
  if (!(mode in contexts)) contexts[mode] = [{}];
3792
3910
  addToken(contexts[mode][0], {
3793
3911
  ...token,
@@ -3801,7 +3919,8 @@ async function createSyntheticResolver(tokens, { config, logger, req, sources })
3801
3919
  sets: { allTokens: { sources: [simpleFlatten(tokens, { logger })] } },
3802
3920
  modifiers: { tzMode: {
3803
3921
  description: "Automatically built from $extensions.mode",
3804
- contexts
3922
+ contexts,
3923
+ default: "."
3805
3924
  } }
3806
3925
  }, void 0, 2);
3807
3926
  return createResolver(await normalizeResolver(momoa.parse(src), {
@@ -3958,6 +4077,16 @@ async function parse(_input, { logger = new Logger(), req = defaultReq, skipLint
3958
4077
  let tokens = {};
3959
4078
  let resolver;
3960
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
+ });
3961
4090
  const totalStart = performance.now();
3962
4091
  const initStart = performance.now();
3963
4092
  const resolverResult = await loadResolver(inputs, {