@terrazzo/parser 0.10.3 → 2.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.d.ts +82 -333
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2203 -3660
  5. package/dist/index.js.map +1 -1
  6. package/package.json +6 -5
  7. package/src/build/index.ts +32 -41
  8. package/src/config.ts +13 -6
  9. package/src/lib/code-frame.ts +5 -2
  10. package/src/lib/momoa.ts +10 -0
  11. package/src/lint/index.ts +41 -37
  12. package/src/lint/plugin-core/index.ts +73 -16
  13. package/src/lint/plugin-core/rules/colorspace.ts +4 -0
  14. package/src/lint/plugin-core/rules/duplicate-values.ts +2 -0
  15. package/src/lint/plugin-core/rules/max-gamut.ts +24 -4
  16. package/src/lint/plugin-core/rules/no-type-on-alias.ts +29 -0
  17. package/src/lint/plugin-core/rules/required-modes.ts +2 -0
  18. package/src/lint/plugin-core/rules/required-typography-properties.ts +13 -3
  19. package/src/lint/plugin-core/rules/valid-boolean.ts +41 -0
  20. package/src/lint/plugin-core/rules/valid-border.ts +57 -0
  21. package/src/lint/plugin-core/rules/valid-color.ts +265 -0
  22. package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +83 -0
  23. package/src/lint/plugin-core/rules/valid-dimension.ts +199 -0
  24. package/src/lint/plugin-core/rules/valid-duration.ts +123 -0
  25. package/src/lint/plugin-core/rules/valid-font-family.ts +68 -0
  26. package/src/lint/plugin-core/rules/valid-font-weight.ts +89 -0
  27. package/src/lint/plugin-core/rules/valid-gradient.ts +79 -0
  28. package/src/lint/plugin-core/rules/valid-link.ts +41 -0
  29. package/src/lint/plugin-core/rules/valid-number.ts +63 -0
  30. package/src/lint/plugin-core/rules/valid-shadow.ts +67 -0
  31. package/src/lint/plugin-core/rules/valid-string.ts +41 -0
  32. package/src/lint/plugin-core/rules/valid-stroke-style.ts +104 -0
  33. package/src/lint/plugin-core/rules/valid-transition.ts +61 -0
  34. package/src/lint/plugin-core/rules/valid-typography.ts +67 -0
  35. package/src/logger.ts +70 -59
  36. package/src/parse/index.ts +23 -318
  37. package/src/parse/load.ts +257 -0
  38. package/src/parse/normalize.ts +134 -170
  39. package/src/parse/token.ts +530 -0
  40. package/src/types.ts +76 -10
  41. package/src/parse/alias.ts +0 -369
  42. package/src/parse/json.ts +0 -211
  43. package/src/parse/validate.ts +0 -961
@@ -0,0 +1,530 @@
1
+ import * as momoa from '@humanwhocodes/momoa';
2
+ import { getObjMember, parseRef, type RefMap } from '@terrazzo/json-schema-tools';
3
+ import {
4
+ type GroupNormalized,
5
+ isAlias,
6
+ parseAlias,
7
+ type TokenNormalized,
8
+ type TokenNormalizedSet,
9
+ } from '@terrazzo/token-tools';
10
+ import wcmatch from 'wildcard-match';
11
+ import type { default as Logger } from '../logger.js';
12
+ import type { Config, InputSource, ReferenceObject } from '../types.js';
13
+
14
+ /** Convert valid DTCG alias to $ref */
15
+ export function aliasToRef(alias: string, mode?: string): ReferenceObject | undefined {
16
+ const id = parseAlias(alias);
17
+ // if this is invalid, stop
18
+ if (id === alias) {
19
+ return;
20
+ }
21
+ return {
22
+ $ref: `#/${id.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\./g, '/')}${mode && mode !== '.' ? `/$extensions/mode/${mode}` : ''}/$value`,
23
+ };
24
+ }
25
+
26
+ export interface TokenFromNodeOptions {
27
+ groups: Record<string, GroupNormalized>;
28
+ path: string[];
29
+ source: InputSource;
30
+ ignore: Config['ignore'];
31
+ }
32
+
33
+ /** Generate a TokenNormalized from a Momoa node */
34
+ export function tokenFromNode(
35
+ node: momoa.AnyNode,
36
+ { groups, path, source, ignore }: TokenFromNodeOptions,
37
+ ): TokenNormalized | undefined {
38
+ const isToken = node.type === 'Object' && getObjMember(node, '$value') && !path.includes('$extensions');
39
+ if (!isToken) {
40
+ return undefined;
41
+ }
42
+
43
+ const jsonID = `#/${path.join('/')}`;
44
+ const id = path.join('.');
45
+
46
+ const originalToken = momoa.evaluate(node) as any;
47
+
48
+ const groupID = `#/${path.slice(0, -1).join('/')}`;
49
+ const group = groups[groupID]!;
50
+ if (group?.tokens && !group.tokens.includes(id)) {
51
+ group.tokens.push(id);
52
+ }
53
+
54
+ const nodeSource = { filename: source.filename?.href, node };
55
+ const token: TokenNormalized = {
56
+ id,
57
+ $type: originalToken.$type || group.$type,
58
+ $description: originalToken.$description || undefined,
59
+ $deprecated: originalToken.$deprecated ?? group.$deprecated ?? undefined, // ⚠️ MUST use ?? here to inherit false correctly
60
+ $value: originalToken.$value,
61
+ $extensions: originalToken.$extensions || undefined,
62
+ aliasChain: undefined,
63
+ aliasedBy: undefined,
64
+ aliasOf: undefined,
65
+ partialAliasOf: undefined,
66
+ dependencies: undefined,
67
+ group,
68
+ originalValue: undefined, // undefined because we are not sure if the value has been modified or not
69
+ source: nodeSource,
70
+ jsonID,
71
+ mode: {
72
+ '.': {
73
+ $value: originalToken.$value,
74
+ aliasOf: undefined,
75
+ aliasChain: undefined,
76
+ partialAliasOf: undefined,
77
+ aliasedBy: undefined,
78
+ originalValue: undefined,
79
+ dependencies: undefined,
80
+ source: {
81
+ ...nodeSource,
82
+ node: (getObjMember(nodeSource.node, '$value') ?? nodeSource.node) as momoa.ObjectNode,
83
+ },
84
+ },
85
+ },
86
+ };
87
+
88
+ // after assembling token, handle ignores to see if the final result should be ignored or not
89
+ // filter out ignored
90
+ if ((ignore?.deprecated && token.$deprecated) || (ignore?.tokens && wcmatch(ignore.tokens)(token.id))) {
91
+ return;
92
+ }
93
+
94
+ const $extensions = getObjMember(node, '$extensions');
95
+ if ($extensions) {
96
+ const modeNode = getObjMember($extensions as momoa.ObjectNode, 'mode') as momoa.ObjectNode;
97
+ for (const mode of Object.keys((token.$extensions as any).mode)) {
98
+ const modeValue = (token.$extensions as any).mode[mode];
99
+ token.mode[mode] = {
100
+ $value: modeValue,
101
+ aliasOf: undefined,
102
+ aliasChain: undefined,
103
+ partialAliasOf: undefined,
104
+ aliasedBy: undefined,
105
+ originalValue: undefined,
106
+ dependencies: undefined,
107
+ source: {
108
+ ...nodeSource,
109
+ node: getObjMember(modeNode, mode) as any,
110
+ },
111
+ };
112
+ }
113
+ }
114
+ return token;
115
+ }
116
+
117
+ export interface TokenRawValues {
118
+ jsonID: string;
119
+ originalValue: any;
120
+ source: TokenNormalized['source'];
121
+ mode: Record<string, { originalValue: any; source: TokenNormalized['source'] }>;
122
+ }
123
+
124
+ /** Generate originalValue and source from node */
125
+ export function tokenRawValuesFromNode(
126
+ node: momoa.AnyNode,
127
+ { filename, path }: { filename: string; path: string[] },
128
+ ): TokenRawValues | undefined {
129
+ const isToken = node.type === 'Object' && getObjMember(node, '$value') && !path.includes('$extensions');
130
+ if (!isToken) {
131
+ return undefined;
132
+ }
133
+
134
+ const jsonID = `#/${path.join('/')}`;
135
+ const rawValues: TokenRawValues = {
136
+ jsonID,
137
+ originalValue: momoa.evaluate(node),
138
+ source: { loc: filename, filename, node: node as momoa.ObjectNode },
139
+ mode: {},
140
+ };
141
+ rawValues.mode['.'] = {
142
+ originalValue: rawValues.originalValue.$value,
143
+ source: { ...rawValues.source, node: getObjMember(node as momoa.ObjectNode, '$value') as momoa.ObjectNode },
144
+ };
145
+ const $extensions = getObjMember(node, '$extensions');
146
+ if ($extensions) {
147
+ const modes = getObjMember($extensions as momoa.ObjectNode, 'mode');
148
+ if (modes) {
149
+ for (const modeMember of (modes as momoa.ObjectNode).members) {
150
+ const mode = (modeMember.name as momoa.StringNode).value;
151
+ rawValues.mode[mode] = {
152
+ originalValue: momoa.evaluate(modeMember.value),
153
+ source: { loc: filename, filename, node: modeMember.value as momoa.ObjectNode },
154
+ };
155
+ }
156
+ }
157
+ }
158
+
159
+ return rawValues;
160
+ }
161
+
162
+ /** Arbitrary keys that should be associated with a token group */
163
+ const GROUP_PROPERTIES = ['$deprecated', '$description', '$extensions', '$type'];
164
+
165
+ /**
166
+ * Generate a group from a node.
167
+ * This method mutates the groups index as it goes because of group inheritance.
168
+ * As it encounters new groups it may have to update other groups.
169
+ */
170
+ export function groupFromNode(
171
+ node: momoa.ObjectNode,
172
+ { path, groups }: { path: string[]; groups: Record<string, GroupNormalized> },
173
+ ): GroupNormalized {
174
+ const id = path.join('.');
175
+ const jsonID = `#/${path.join('/')}`;
176
+
177
+ // group
178
+ if (!groups[jsonID]) {
179
+ groups[jsonID] = {
180
+ id,
181
+ $deprecated: undefined,
182
+ $description: undefined,
183
+ $extensions: undefined,
184
+ $type: undefined,
185
+ tokens: [],
186
+ };
187
+ }
188
+
189
+ // first, copy all parent groups’ properties into local, since they cascade
190
+ const groupIDs = Object.keys(groups);
191
+ groupIDs.sort(); // these may not be sorted; re-sort just in case (order determines final values)
192
+ for (const groupID of groupIDs) {
193
+ const isParentGroup = jsonID.startsWith(groupID) && groupID !== jsonID;
194
+ if (isParentGroup) {
195
+ groups[jsonID].$deprecated = groups[groupID]?.$deprecated ?? groups[jsonID].$deprecated;
196
+ groups[jsonID].$description = groups[groupID]?.$description ?? groups[jsonID].$description;
197
+ groups[jsonID].$type = groups[groupID]?.$type ?? groups[jsonID].$type;
198
+ }
199
+ }
200
+
201
+ // next, override cascading values with local
202
+ for (const m of node.members) {
203
+ if (m.name.type !== 'String' || !GROUP_PROPERTIES.includes(m.name.value)) {
204
+ continue;
205
+ }
206
+ (groups as any)[jsonID]![m.name.value] = momoa.evaluate(m.value);
207
+ }
208
+
209
+ return groups[jsonID]!;
210
+ }
211
+
212
+ export interface GraphAliasesOptions {
213
+ tokens: TokenNormalizedSet;
214
+ sources: Record<string, InputSource>;
215
+ logger: Logger;
216
+ }
217
+
218
+ /**
219
+ * Link and reverse-link tokens in one pass.
220
+ */
221
+ export function graphAliases(refMap: RefMap, { tokens, logger, sources }: GraphAliasesOptions) {
222
+ // mini-helper that probably shouldn’t be used outside this function
223
+ const getTokenRef = (ref: string) => ref.replace(/\/(\$value|\$extensions)\/?.*/, '');
224
+
225
+ for (const [jsonID, { refChain }] of Object.entries(refMap)) {
226
+ if (!refChain.length) {
227
+ continue;
228
+ }
229
+
230
+ const mode = jsonID.match(/\/\$extensions\/mode\/([^/]+)/)?.[1] || '.';
231
+ const rootRef = getTokenRef(jsonID);
232
+ const modeValue = tokens[rootRef]?.mode[mode];
233
+ if (!modeValue) {
234
+ continue;
235
+ }
236
+
237
+ // aliasChain + dependencies
238
+ if (!modeValue.dependencies) {
239
+ modeValue.dependencies = [];
240
+ }
241
+ modeValue.dependencies.push(...refChain.filter((r) => !modeValue.dependencies!.includes(r)));
242
+ modeValue.dependencies.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
243
+
244
+ // Top alias
245
+ const isTopLevelAlias = jsonID.endsWith('/$value') || tokens[jsonID];
246
+ if (isTopLevelAlias) {
247
+ modeValue.aliasOf = refToTokenID(refChain.at(-1)!);
248
+ const aliasChain = refChain.map(refToTokenID) as string[];
249
+ modeValue.aliasChain = [...aliasChain];
250
+ }
251
+
252
+ // Partial alias
253
+ const partial = jsonID
254
+ .replace(/.*\/\$value\/?/, '')
255
+ .split('/')
256
+ .filter(Boolean);
257
+ if (partial.length && modeValue.$value && typeof modeValue.$value === 'object') {
258
+ let node: any = modeValue.$value;
259
+ let sourceNode = modeValue.source.node as momoa.AnyNode;
260
+ if (!modeValue.partialAliasOf) {
261
+ modeValue.partialAliasOf = Array.isArray(modeValue.$value) || tokens[rootRef]?.$type === 'shadow' ? [] : {};
262
+ }
263
+ let partialAliasOf = modeValue.partialAliasOf as any;
264
+ // special case: for shadows, normalize object to array
265
+ if (tokens[rootRef]?.$type === 'shadow' && !Array.isArray(node)) {
266
+ if (Array.isArray(modeValue.partialAliasOf) && !modeValue.partialAliasOf.length) {
267
+ modeValue.partialAliasOf.push({} as any);
268
+ }
269
+ partialAliasOf = (modeValue.partialAliasOf as any)[0]!;
270
+ }
271
+
272
+ for (let i = 0; i < partial.length; i++) {
273
+ let key = partial[i] as string | number;
274
+ if (String(Number(key)) === key) {
275
+ key = Number(key);
276
+ }
277
+ if (key in node && typeof node[key] !== 'undefined') {
278
+ node = node[key];
279
+ if (sourceNode.type === 'Object') {
280
+ sourceNode = getObjMember(sourceNode, key as string) ?? sourceNode;
281
+ } else if (sourceNode.type === 'Array') {
282
+ sourceNode = sourceNode.elements[key as number]?.value ?? sourceNode;
283
+ }
284
+ }
285
+ // last node: apply partial alias
286
+ if (i === partial.length - 1) {
287
+ const aliasedID = getTokenRef(refChain.at(-1)!);
288
+ if (!(aliasedID in tokens)) {
289
+ logger.error({
290
+ group: 'parser',
291
+ label: 'init',
292
+ message: `Invalid alias: ${aliasedID}`,
293
+ node: sourceNode,
294
+ src: sources[tokens[rootRef]!.source.filename!]?.src,
295
+ });
296
+ break;
297
+ }
298
+ partialAliasOf[key] = refToTokenID(aliasedID);
299
+ }
300
+ // otherwise, create deeper structure and continue traversing
301
+ if (!(key in partialAliasOf)) {
302
+ partialAliasOf[key] = Array.isArray(node) ? [] : {};
303
+ }
304
+ partialAliasOf = partialAliasOf[key];
305
+ }
306
+ }
307
+
308
+ // aliasedBy (reversed)
309
+ const aliasedByRefs = [jsonID, ...refChain].reverse();
310
+ for (let i = 0; i < aliasedByRefs.length; i++) {
311
+ const baseRef = getTokenRef(aliasedByRefs[i]!);
312
+ const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef];
313
+ if (!baseToken) {
314
+ continue;
315
+ }
316
+ const upstream = aliasedByRefs.slice(i + 1);
317
+ if (!upstream.length) {
318
+ break;
319
+ }
320
+ if (!baseToken.aliasedBy) {
321
+ baseToken.aliasedBy = [];
322
+ }
323
+ for (let j = 0; j < upstream.length; j++) {
324
+ const downstream = refToTokenID(upstream[j]!)!;
325
+ if (!baseToken.aliasedBy.includes(downstream)) {
326
+ baseToken.aliasedBy.push(downstream);
327
+ if (mode === '.') {
328
+ tokens[baseRef]!.aliasedBy = baseToken.aliasedBy;
329
+ }
330
+ }
331
+ }
332
+ baseToken.aliasedBy.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true })); // sort, because the ordering is arbitrary and flaky
333
+ }
334
+
335
+ if (mode === '.') {
336
+ tokens[rootRef]!.aliasChain = modeValue.aliasChain;
337
+ tokens[rootRef]!.aliasedBy = modeValue.aliasedBy;
338
+ tokens[rootRef]!.aliasOf = modeValue.aliasOf;
339
+ tokens[rootRef]!.dependencies = modeValue.dependencies;
340
+ tokens[rootRef]!.partialAliasOf = modeValue.partialAliasOf;
341
+ }
342
+ }
343
+ }
344
+
345
+ /** Convert valid DTCG alias to $ref Momoa Node */
346
+ export function aliasToMomoa(
347
+ alias: string,
348
+ loc: momoa.ObjectNode['loc'] = {
349
+ start: { line: -1, column: -1, offset: 0 },
350
+ end: { line: -1, column: -1, offset: 0 },
351
+ },
352
+ ): momoa.ObjectNode | undefined {
353
+ const $ref = aliasToRef(alias);
354
+ if (!$ref) {
355
+ return;
356
+ }
357
+ return {
358
+ type: 'Object',
359
+ members: [
360
+ {
361
+ type: 'Member',
362
+ name: { type: 'String', value: '$ref', loc },
363
+ value: { type: 'String', value: $ref.$ref, loc },
364
+ loc,
365
+ },
366
+ ],
367
+ loc,
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Convert Reference Object to token ID.
373
+ * This can then be turned into an alias by surrounding with { … }
374
+ * ⚠️ This is not mode-aware. This will flatten multiple modes into the same root token.
375
+ */
376
+ export function refToTokenID($ref: ReferenceObject | string): string | undefined {
377
+ const path = typeof $ref === 'object' ? $ref.$ref : $ref;
378
+ if (typeof path !== 'string') {
379
+ return;
380
+ }
381
+ const { subpath } = parseRef(path);
382
+ return (subpath?.length && subpath.join('.').replace(/\.(\$value|\$extensions).*$/, '')) || undefined;
383
+ }
384
+
385
+ const EXPECTED_NESTED_ALIAS: Record<string, Record<string, string[]>> = {
386
+ border: {
387
+ color: ['color'],
388
+ stroke: ['strokeStyle'],
389
+ width: ['dimension'],
390
+ },
391
+ gradient: {
392
+ color: ['color'],
393
+ position: ['number'],
394
+ },
395
+ shadow: {
396
+ color: ['color'],
397
+ offsetX: ['dimension'],
398
+ offsetY: ['dimension'],
399
+ blur: ['dimension'],
400
+ spread: ['dimension'],
401
+ inset: ['boolean'],
402
+ },
403
+ strokeStyle: {
404
+ dashArray: ['dimension'],
405
+ },
406
+ transition: {
407
+ duration: ['duration'],
408
+ delay: ['duration'],
409
+ timingFunction: ['cubicBezier'],
410
+ },
411
+ typography: {
412
+ fontFamily: ['fontFamily'],
413
+ fontWeight: ['fontWeight'],
414
+ fontSize: ['dimension'],
415
+ lineHeight: ['dimension', 'number'],
416
+ letterSpacing: ['dimension'],
417
+ },
418
+ };
419
+
420
+ /**
421
+ * Resolve DTCG aliases
422
+ */
423
+ export function resolveAliases(
424
+ tokens: TokenNormalizedSet,
425
+ { logger, refMap, sources }: { logger: Logger; refMap: RefMap; sources: Record<string, InputSource> },
426
+ ): void {
427
+ for (const token of Object.values(tokens)) {
428
+ const aliasEntry = {
429
+ group: 'parser' as const,
430
+ label: 'init',
431
+ src: sources[token.source.filename!]?.src,
432
+ node: getObjMember(token.source.node, '$value'),
433
+ };
434
+
435
+ for (const mode of Object.keys(token.mode)) {
436
+ function resolveInner(alias: string, refChain: string[]): string {
437
+ const nextRef = aliasToRef(alias, mode)?.$ref!;
438
+ if (refChain.includes(nextRef)) {
439
+ logger.error({ ...aliasEntry, message: 'Circular alias detected.' });
440
+ }
441
+ const nextJSONID = nextRef.replace(/\/(\$value|\$extensions).*/, '');
442
+ const nextToken = tokens[nextJSONID]?.mode[mode] || tokens[nextJSONID]?.mode['.'];
443
+ if (!nextToken) {
444
+ logger.error({ ...aliasEntry, message: `Could not resolve alias ${alias}.` });
445
+ }
446
+ refChain.push(nextRef);
447
+ if (isAlias(nextToken!.originalValue! as string)) {
448
+ return resolveInner(nextToken!.originalValue! as string, refChain);
449
+ }
450
+ return nextJSONID;
451
+ }
452
+
453
+ function traverseAndResolve(
454
+ value: any,
455
+ { node, expectedTypes, path }: { node: momoa.AnyNode; expectedTypes?: string[]; path: (string | number)[] },
456
+ ): any {
457
+ if (typeof value !== 'string') {
458
+ if (Array.isArray(value)) {
459
+ for (let i = 0; i < value.length; i++) {
460
+ if (!value[i]) {
461
+ continue;
462
+ }
463
+ value[i] = traverseAndResolve(value[i], {
464
+ node: (node as momoa.ArrayNode).elements?.[i]?.value!,
465
+ // special case: cubicBezier
466
+ expectedTypes: expectedTypes?.includes('cubicBezier') ? ['number'] : expectedTypes,
467
+ path: [...path, i],
468
+ }).$value;
469
+ }
470
+ } else if (typeof value === 'object') {
471
+ for (const key of Object.keys(value)) {
472
+ if (!expectedTypes?.length || !EXPECTED_NESTED_ALIAS[expectedTypes[0]!]) {
473
+ continue;
474
+ }
475
+ value[key] = traverseAndResolve(value[key], {
476
+ node: getObjMember(node as momoa.ObjectNode, key)!,
477
+ expectedTypes: EXPECTED_NESTED_ALIAS[expectedTypes[0]!]![key],
478
+ path: [...path, key],
479
+ }).$value;
480
+ }
481
+ }
482
+ return { $value: value };
483
+ }
484
+
485
+ if (!isAlias(value)) {
486
+ if (!expectedTypes?.includes('string') && (value.includes('{') || value.includes('}'))) {
487
+ logger.error({ ...aliasEntry, message: 'Invalid alias syntax.', node });
488
+ }
489
+ return { $value: value };
490
+ }
491
+
492
+ const refChain: string[] = [];
493
+ const resolvedID = resolveInner(value, refChain);
494
+ if (expectedTypes?.length && !expectedTypes.includes(tokens[resolvedID]!.$type)) {
495
+ logger.error({
496
+ ...aliasEntry,
497
+ message: `Cannot alias to $type "${tokens[resolvedID]!.$type}" from $type "${expectedTypes.join(' / ')}".`,
498
+ node,
499
+ });
500
+ }
501
+
502
+ refMap[path.join('/')] = { filename: token.source.filename!, refChain };
503
+
504
+ return {
505
+ $type: tokens[resolvedID]!.$type,
506
+ $value: tokens[resolvedID]!.mode[mode]?.$value || tokens[resolvedID]!.$value,
507
+ };
508
+ }
509
+
510
+ // resolve DTCG aliases without
511
+ const pathBase = mode === '.' ? token.jsonID : `${token.jsonID}/$extensions/mode/${mode}`;
512
+ const { $type, $value } = traverseAndResolve(token.mode[mode]!.$value, {
513
+ node: aliasEntry.node!,
514
+ expectedTypes: token.$type ? [token.$type] : undefined,
515
+ path: [pathBase, '$value'],
516
+ });
517
+ if (!token.$type) {
518
+ (token as any).$type = $type;
519
+ }
520
+ if ($value) {
521
+ token.mode[mode]!.$value = $value;
522
+ }
523
+
524
+ // fill in $type and $value
525
+ if (mode === '.') {
526
+ token.$value = token.mode[mode]!.$value;
527
+ }
528
+ }
529
+ }
530
+ }
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { AnyNode, DocumentNode } from '@humanwhocodes/momoa';
1
+ import type * as momoa from '@humanwhocodes/momoa';
2
2
  import type { TokenNormalized } from '@terrazzo/token-tools';
3
+ import type ytm from 'yaml-to-momoa';
3
4
  import type Logger from './logger.js';
4
5
 
5
6
  export interface BuildHookOptions {
@@ -68,6 +69,40 @@ export interface Config {
68
69
  };
69
70
  }
70
71
 
72
+ export interface VisitorContext {
73
+ parent?: momoa.AnyNode;
74
+ filename: URL;
75
+ path: string[];
76
+ }
77
+
78
+ export type Visitor<T extends momoa.AnyNode = momoa.ObjectNode | momoa.DocumentNode> = (
79
+ node: T,
80
+ context: VisitorContext,
81
+ // biome-ignore lint/suspicious/noConfusingVoidType: TS requires void
82
+ ) => T | void | null | undefined;
83
+
84
+ export interface TransformVisitors {
85
+ boolean?: Visitor;
86
+ border?: Visitor;
87
+ color?: Visitor;
88
+ cubicBezier?: Visitor;
89
+ dimension?: Visitor;
90
+ duration?: Visitor;
91
+ fontFamily?: Visitor;
92
+ fontWeight?: Visitor;
93
+ gradient?: Visitor;
94
+ group?: Visitor;
95
+ link?: Visitor;
96
+ number?: Visitor;
97
+ root?: Visitor;
98
+ shadow?: Visitor;
99
+ strokeStyle?: Visitor;
100
+ token?: Visitor;
101
+ transition?: Visitor;
102
+ typography?: Visitor;
103
+ [key: string]: Visitor | undefined;
104
+ }
105
+
71
106
  // normalized, finalized config
72
107
  export interface ConfigInit {
73
108
  tokens: URL[];
@@ -92,14 +127,14 @@ export interface ConfigOptions {
92
127
  export interface InputSource {
93
128
  filename?: URL;
94
129
  src: any;
95
- document: DocumentNode;
130
+ document: momoa.DocumentNode;
96
131
  }
97
132
 
98
133
  export interface LintNotice {
99
134
  /** Lint message shown to the user */
100
135
  message: string;
101
136
  /** Erring node (used to point to a specific line) */
102
- node?: AnyNode;
137
+ node?: momoa.AnyNode;
103
138
  }
104
139
 
105
140
  export type LintRuleSeverity = 'error' | 'warn' | 'off';
@@ -113,10 +148,10 @@ export interface LintRuleNormalized<O = any> {
113
148
  }
114
149
 
115
150
  export type LintReportDescriptor<MessageIds extends string> = {
116
- /** To error on a specific token source file, provide an erring node */
117
- node?: AnyNode;
118
- /** To error on a specific token source file, also provide the source */
119
- source?: InputSource;
151
+ /** To error on a specific token source file, provide a Momoa node */
152
+ node?: momoa.AnyNode;
153
+ /** To provide correct line numbers, specify the filename (usually found on `token.source.loc`) */
154
+ filename?: string;
120
155
  /** Provide data for messages */
121
156
  data?: Record<string, unknown>;
122
157
  } & (
@@ -139,7 +174,7 @@ export type LintReportDescriptor<MessageIds extends string> = {
139
174
 
140
175
  export interface LintRule<
141
176
  MessageIds extends string,
142
- LintRuleOptions extends object | undefined = undefined,
177
+ LintRuleOptions extends Record<string, any> = Record<string, never>,
143
178
  LintRuleDocs = unknown,
144
179
  > {
145
180
  meta?: LintRuleMetaData<MessageIds, LintRuleOptions, LintRuleDocs>;
@@ -164,8 +199,11 @@ export interface LintRuleContext<MessageIds extends string, LintRuleOptions exte
164
199
  options: LintRuleOptions;
165
200
  /** The current working directory. */
166
201
  cwd?: URL;
167
- /** Source file the token came from. */
168
- src: string;
202
+ /**
203
+ * All source files present in this run. To find the original source, match a
204
+ * token’s `source.loc` filename to one of the source’s `filename`s.
205
+ */
206
+ sources: InputSource[];
169
207
  /** Source file location. */
170
208
  filename?: URL;
171
209
  /** ID:Token map of all tokens. */
@@ -221,6 +259,30 @@ export interface OutputFileExpanded extends OutputFile {
221
259
  time: number;
222
260
  }
223
261
 
262
+ export interface ParseOptions {
263
+ logger?: Logger;
264
+ config: ConfigInit;
265
+ /**
266
+ * Skip lint step
267
+ * @default false
268
+ */
269
+ skipLint?: boolean;
270
+ /**
271
+ * Continue on error? (Useful for `tz check`)
272
+ * @default false
273
+ */
274
+ continueOnError?: boolean;
275
+ /** Provide yamlToMomoa module to parse YAML (by default, this isn’t shipped to cut down on package weight) */
276
+ yamlToMomoa?: typeof ytm;
277
+ /**
278
+ * Transform API
279
+ * @see https://terrazzo.app/docs/api/js#transform-api
280
+ */
281
+ transform?: TransformVisitors;
282
+ /** (internal cache; do not use) */
283
+ _sources?: Record<string, InputSource>;
284
+ }
285
+
224
286
  export interface Plugin {
225
287
  name: string;
226
288
  /** Read config, and optionally modify */
@@ -308,3 +370,7 @@ export interface TransformHookOptions {
308
370
  /** Momoa documents */
309
371
  sources: InputSource[];
310
372
  }
373
+
374
+ export interface ReferenceObject {
375
+ $ref: string;
376
+ }