@terrazzo/parser 2.0.0-alpha.7 → 2.0.0-beta.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 (53) hide show
  1. package/dist/index.d.ts +39 -6
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +578 -512
  4. package/dist/index.js.map +1 -1
  5. package/package.json +3 -3
  6. package/src/build/index.ts +0 -209
  7. package/src/config.ts +0 -304
  8. package/src/index.ts +0 -95
  9. package/src/lib/code-frame.ts +0 -177
  10. package/src/lib/momoa.ts +0 -10
  11. package/src/lib/resolver-utils.ts +0 -35
  12. package/src/lint/index.ts +0 -142
  13. package/src/lint/plugin-core/index.ts +0 -103
  14. package/src/lint/plugin-core/lib/docs.ts +0 -3
  15. package/src/lint/plugin-core/rules/a11y-min-contrast.ts +0 -91
  16. package/src/lint/plugin-core/rules/a11y-min-font-size.ts +0 -66
  17. package/src/lint/plugin-core/rules/colorspace.ts +0 -108
  18. package/src/lint/plugin-core/rules/consistent-naming.ts +0 -65
  19. package/src/lint/plugin-core/rules/descriptions.ts +0 -43
  20. package/src/lint/plugin-core/rules/duplicate-values.ts +0 -85
  21. package/src/lint/plugin-core/rules/max-gamut.ts +0 -144
  22. package/src/lint/plugin-core/rules/required-children.ts +0 -106
  23. package/src/lint/plugin-core/rules/required-modes.ts +0 -75
  24. package/src/lint/plugin-core/rules/required-type.ts +0 -28
  25. package/src/lint/plugin-core/rules/required-typography-properties.ts +0 -65
  26. package/src/lint/plugin-core/rules/valid-boolean.ts +0 -41
  27. package/src/lint/plugin-core/rules/valid-border.ts +0 -57
  28. package/src/lint/plugin-core/rules/valid-color.ts +0 -265
  29. package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +0 -83
  30. package/src/lint/plugin-core/rules/valid-dimension.ts +0 -199
  31. package/src/lint/plugin-core/rules/valid-duration.ts +0 -123
  32. package/src/lint/plugin-core/rules/valid-font-family.ts +0 -68
  33. package/src/lint/plugin-core/rules/valid-font-weight.ts +0 -89
  34. package/src/lint/plugin-core/rules/valid-gradient.ts +0 -79
  35. package/src/lint/plugin-core/rules/valid-link.ts +0 -41
  36. package/src/lint/plugin-core/rules/valid-number.ts +0 -63
  37. package/src/lint/plugin-core/rules/valid-shadow.ts +0 -67
  38. package/src/lint/plugin-core/rules/valid-string.ts +0 -41
  39. package/src/lint/plugin-core/rules/valid-stroke-style.ts +0 -104
  40. package/src/lint/plugin-core/rules/valid-transition.ts +0 -61
  41. package/src/lint/plugin-core/rules/valid-typography.ts +0 -67
  42. package/src/logger.ts +0 -213
  43. package/src/parse/index.ts +0 -124
  44. package/src/parse/load.ts +0 -172
  45. package/src/parse/normalize.ts +0 -163
  46. package/src/parse/process.ts +0 -251
  47. package/src/parse/token.ts +0 -553
  48. package/src/resolver/create-synthetic-resolver.ts +0 -86
  49. package/src/resolver/index.ts +0 -7
  50. package/src/resolver/load.ts +0 -215
  51. package/src/resolver/normalize.ts +0 -133
  52. package/src/resolver/validate.ts +0 -375
  53. package/src/types.ts +0 -468
@@ -1,553 +0,0 @@
1
- import * as momoa from '@humanwhocodes/momoa';
2
- import { encodeFragment, getObjMember, type InputSourceWithDocument, parseRef } 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, ReferenceObject, RefMap } from '../types.js';
13
-
14
- /** Convert valid DTCG alias to $ref */
15
- export function aliasToGroupRef(alias: string): ReferenceObject | undefined {
16
- const id = parseAlias(alias);
17
- // if this is invalid, stop
18
- if (id === alias) {
19
- return;
20
- }
21
- return { $ref: `#/${id.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\./g, '/')}` };
22
- }
23
-
24
- /** Convert valid DTCG alias to $ref */
25
- export function aliasToTokenRef(alias: string, mode?: string): ReferenceObject | undefined {
26
- const id = parseAlias(alias);
27
- // if this is invalid, stop
28
- if (id === alias) {
29
- return;
30
- }
31
- return {
32
- $ref: `#/${id.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\./g, '/')}${mode && mode !== '.' ? `/$extensions/mode/${mode}` : ''}/$value`,
33
- };
34
- }
35
-
36
- export interface TokenFromNodeOptions {
37
- groups: Record<string, GroupNormalized>;
38
- path: string[];
39
- source: InputSourceWithDocument;
40
- ignore: Config['ignore'];
41
- }
42
-
43
- /** Generate a TokenNormalized from a Momoa node */
44
- export function tokenFromNode(
45
- node: momoa.AnyNode,
46
- { groups, path, source, ignore }: TokenFromNodeOptions,
47
- ): TokenNormalized | undefined {
48
- const isToken = node.type === 'Object' && !!getObjMember(node, '$value') && !path.includes('$extensions');
49
- if (!isToken) {
50
- return undefined;
51
- }
52
-
53
- const jsonID = encodeFragment(path);
54
- const id = path.join('.');
55
-
56
- const originalToken = momoa.evaluate(node) as any;
57
-
58
- const groupID = encodeFragment(path.slice(0, -1));
59
- const group = groups[groupID]!;
60
- if (group?.tokens && !group.tokens.includes(id)) {
61
- group.tokens.push(id);
62
- }
63
-
64
- const nodeSource = { filename: source.filename.href, node };
65
- const token: TokenNormalized = {
66
- id,
67
- $type: originalToken.$type || group.$type,
68
- $description: originalToken.$description || undefined,
69
- $deprecated: originalToken.$deprecated ?? group.$deprecated ?? undefined, // ⚠️ MUST use ?? here to inherit false correctly
70
- $value: originalToken.$value,
71
- $extensions: originalToken.$extensions || undefined,
72
- $extends: originalToken.$extends || undefined,
73
- aliasChain: undefined,
74
- aliasedBy: undefined,
75
- aliasOf: undefined,
76
- partialAliasOf: undefined,
77
- dependencies: undefined,
78
- group,
79
- originalValue: undefined, // undefined because we are not sure if the value has been modified or not
80
- source: nodeSource,
81
- jsonID,
82
- mode: {
83
- '.': {
84
- $value: originalToken.$value,
85
- aliasOf: undefined,
86
- aliasChain: undefined,
87
- partialAliasOf: undefined,
88
- aliasedBy: undefined,
89
- originalValue: undefined,
90
- dependencies: undefined,
91
- source: {
92
- ...nodeSource,
93
- node: (getObjMember(nodeSource.node, '$value') ?? nodeSource.node) as momoa.ObjectNode,
94
- },
95
- },
96
- },
97
- };
98
-
99
- // after assembling token, handle ignores to see if the final result should be ignored or not
100
- // filter out ignored
101
- if ((ignore?.deprecated && token.$deprecated) || (ignore?.tokens && wcmatch(ignore.tokens)(token.id))) {
102
- return;
103
- }
104
-
105
- const $extensions = getObjMember(node, '$extensions');
106
- if ($extensions) {
107
- const modeNode = getObjMember($extensions as momoa.ObjectNode, 'mode') as momoa.ObjectNode;
108
- for (const mode of Object.keys((token.$extensions as any).mode ?? {})) {
109
- const modeValue = (token.$extensions as any).mode[mode];
110
- token.mode[mode] = {
111
- $value: modeValue,
112
- aliasOf: undefined,
113
- aliasChain: undefined,
114
- partialAliasOf: undefined,
115
- aliasedBy: undefined,
116
- originalValue: undefined,
117
- dependencies: undefined,
118
- source: {
119
- ...nodeSource,
120
- node: getObjMember(modeNode, mode) as any,
121
- },
122
- };
123
- }
124
- }
125
- return token;
126
- }
127
-
128
- export interface TokenRawValues {
129
- jsonID: string;
130
- originalValue: any;
131
- source: TokenNormalized['source'];
132
- mode: Record<string, { originalValue: any; source: TokenNormalized['source'] }>;
133
- }
134
-
135
- /** Generate originalValue and source from node */
136
- export function tokenRawValuesFromNode(
137
- node: momoa.AnyNode,
138
- { filename, path }: { filename: string; path: string[] },
139
- ): TokenRawValues | undefined {
140
- const isToken = node.type === 'Object' && getObjMember(node, '$value') && !path.includes('$extensions');
141
- if (!isToken) {
142
- return undefined;
143
- }
144
-
145
- const jsonID = encodeFragment(path);
146
- const rawValues: TokenRawValues = {
147
- jsonID,
148
- originalValue: momoa.evaluate(node),
149
- source: { loc: filename, filename, node: node as momoa.ObjectNode },
150
- mode: {},
151
- };
152
- rawValues.mode['.'] = {
153
- originalValue: rawValues.originalValue.$value,
154
- source: { ...rawValues.source, node: getObjMember(node as momoa.ObjectNode, '$value') as momoa.ObjectNode },
155
- };
156
- const $extensions = getObjMember(node, '$extensions');
157
- if ($extensions) {
158
- const modes = getObjMember($extensions as momoa.ObjectNode, 'mode');
159
- if (modes) {
160
- for (const modeMember of (modes as momoa.ObjectNode).members) {
161
- const mode = (modeMember.name as momoa.StringNode).value;
162
- rawValues.mode[mode] = {
163
- originalValue: momoa.evaluate(modeMember.value),
164
- source: { loc: filename, filename, node: modeMember.value as momoa.ObjectNode },
165
- };
166
- }
167
- }
168
- }
169
-
170
- return rawValues;
171
- }
172
-
173
- /** Arbitrary keys that should be associated with a token group */
174
- const GROUP_PROPERTIES = ['$deprecated', '$description', '$extensions', '$type'];
175
-
176
- /**
177
- * Generate a group from a node.
178
- * This method mutates the groups index as it goes because of group inheritance.
179
- * As it encounters new groups it may have to update other groups.
180
- */
181
- export function groupFromNode(
182
- node: momoa.ObjectNode,
183
- { path, groups }: { path: string[]; groups: Record<string, GroupNormalized> },
184
- ): GroupNormalized {
185
- const id = path.join('.');
186
- const jsonID = encodeFragment(path);
187
-
188
- // group
189
- if (!groups[jsonID]) {
190
- groups[jsonID] = {
191
- id,
192
- $deprecated: undefined,
193
- $description: undefined,
194
- $extensions: undefined,
195
- $type: undefined,
196
- tokens: [],
197
- };
198
- }
199
-
200
- // first, copy all parent groups’ properties into local, since they cascade
201
- const groupIDs = Object.keys(groups);
202
- groupIDs.sort(); // these may not be sorted; re-sort just in case (order determines final values)
203
- for (const groupID of groupIDs) {
204
- const isParentGroup = jsonID.startsWith(groupID) && groupID !== jsonID;
205
- if (isParentGroup) {
206
- groups[jsonID].$deprecated = groups[groupID]?.$deprecated ?? groups[jsonID].$deprecated;
207
- groups[jsonID].$description = groups[groupID]?.$description ?? groups[jsonID].$description;
208
- groups[jsonID].$type = groups[groupID]?.$type ?? groups[jsonID].$type;
209
- }
210
- }
211
-
212
- // next, override cascading values with local
213
- for (const m of node.members) {
214
- if (m.name.type !== 'String' || !GROUP_PROPERTIES.includes(m.name.value)) {
215
- continue;
216
- }
217
- (groups as any)[jsonID]![m.name.value] = momoa.evaluate(m.value);
218
- }
219
-
220
- return groups[jsonID]!;
221
- }
222
-
223
- export interface GraphAliasesOptions {
224
- tokens: TokenNormalizedSet;
225
- sources: Record<string, InputSourceWithDocument>;
226
- logger: Logger;
227
- }
228
-
229
- /**
230
- * Link and reverse-link tokens in one pass.
231
- */
232
- export function graphAliases(refMap: RefMap, { tokens, logger, sources }: GraphAliasesOptions) {
233
- // mini-helper that probably shouldn’t be used outside this function
234
- const getTokenRef = (ref: string) => ref.replace(/\/(\$value|\$extensions)\/?.*/, '');
235
-
236
- for (const [jsonID, { refChain }] of Object.entries(refMap)) {
237
- if (!refChain.length) {
238
- continue;
239
- }
240
-
241
- const mode = jsonID.match(/\/\$extensions\/mode\/([^/]+)/)?.[1] || '.';
242
- const rootRef = getTokenRef(jsonID);
243
- const modeValue = tokens[rootRef]?.mode[mode];
244
- if (!modeValue) {
245
- continue;
246
- }
247
-
248
- // aliasChain + dependencies
249
- if (!modeValue.dependencies) {
250
- modeValue.dependencies = [];
251
- }
252
- modeValue.dependencies.push(...refChain.filter((r) => !modeValue.dependencies!.includes(r)));
253
- modeValue.dependencies.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
254
-
255
- // Top alias
256
- const isTopLevelAlias = jsonID.endsWith('/$value') || tokens[jsonID];
257
- if (isTopLevelAlias) {
258
- modeValue.aliasOf = refToTokenID(refChain.at(-1)!);
259
- const aliasChain = refChain.map(refToTokenID) as string[];
260
- modeValue.aliasChain = [...aliasChain];
261
- }
262
-
263
- // Partial alias
264
- const partial = jsonID
265
- .replace(/.*\/\$value\/?/, '')
266
- .split('/')
267
- .filter(Boolean);
268
- if (partial.length && modeValue.$value && typeof modeValue.$value === 'object') {
269
- let node: any = modeValue.$value;
270
- let sourceNode = modeValue.source.node as momoa.AnyNode;
271
- if (!modeValue.partialAliasOf) {
272
- modeValue.partialAliasOf = Array.isArray(modeValue.$value) || tokens[rootRef]?.$type === 'shadow' ? [] : {};
273
- }
274
- let partialAliasOf = modeValue.partialAliasOf as any;
275
- // special case: for shadows, normalize object to array
276
- if (tokens[rootRef]?.$type === 'shadow' && !Array.isArray(node)) {
277
- if (Array.isArray(modeValue.partialAliasOf) && !modeValue.partialAliasOf.length) {
278
- modeValue.partialAliasOf.push({} as any);
279
- }
280
- partialAliasOf = (modeValue.partialAliasOf as any)[0]!;
281
- }
282
-
283
- for (let i = 0; i < partial.length; i++) {
284
- let key = partial[i] as string | number;
285
- if (String(Number(key)) === key) {
286
- key = Number(key);
287
- }
288
- if (key in node && typeof node[key] !== 'undefined') {
289
- node = node[key];
290
- if (sourceNode.type === 'Object') {
291
- sourceNode = getObjMember(sourceNode, key as string) ?? sourceNode;
292
- } else if (sourceNode.type === 'Array') {
293
- sourceNode = sourceNode.elements[key as number]?.value ?? sourceNode;
294
- }
295
- }
296
- // last node: apply partial alias
297
- if (i === partial.length - 1) {
298
- // important: we want to get only the immediate alias [0], not the final one [.length - 1].
299
- // if we resolve this too far, we could get incorrect values especially in plugin-css if a
300
- // user is applying cascades to the intermediate aliases but not the final one
301
- const aliasedID = getTokenRef(refChain[0]!);
302
- if (!(aliasedID in tokens)) {
303
- logger.error({
304
- group: 'parser',
305
- label: 'init',
306
- message: `Invalid alias: ${aliasedID}`,
307
- node: sourceNode,
308
- src: sources[tokens[rootRef]!.source.filename!]?.src,
309
- });
310
- break;
311
- }
312
- partialAliasOf[key] = refToTokenID(aliasedID);
313
- }
314
- // otherwise, create deeper structure and continue traversing
315
- if (!(key in partialAliasOf)) {
316
- partialAliasOf[key] = Array.isArray(node) ? [] : {};
317
- }
318
- partialAliasOf = partialAliasOf[key];
319
- }
320
- }
321
-
322
- // aliasedBy (reversed)
323
- const aliasedByRefs = [jsonID, ...refChain].reverse();
324
- for (let i = 0; i < aliasedByRefs.length; i++) {
325
- const baseRef = getTokenRef(aliasedByRefs[i]!);
326
- const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef];
327
- if (!baseToken) {
328
- continue;
329
- }
330
- const upstream = aliasedByRefs.slice(i + 1);
331
- if (!upstream.length) {
332
- break;
333
- }
334
- if (!baseToken.aliasedBy) {
335
- baseToken.aliasedBy = [];
336
- }
337
- for (let j = 0; j < upstream.length; j++) {
338
- const downstream = refToTokenID(upstream[j]!)!;
339
- if (!baseToken.aliasedBy.includes(downstream)) {
340
- baseToken.aliasedBy.push(downstream);
341
- if (mode === '.') {
342
- tokens[baseRef]!.aliasedBy = baseToken.aliasedBy;
343
- }
344
- }
345
- }
346
- baseToken.aliasedBy.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true })); // sort, because the ordering is arbitrary and flaky
347
- }
348
-
349
- if (mode === '.') {
350
- tokens[rootRef]!.aliasChain = modeValue.aliasChain;
351
- tokens[rootRef]!.aliasedBy = modeValue.aliasedBy;
352
- tokens[rootRef]!.aliasOf = modeValue.aliasOf;
353
- tokens[rootRef]!.dependencies = modeValue.dependencies;
354
- tokens[rootRef]!.partialAliasOf = modeValue.partialAliasOf;
355
- }
356
- }
357
- }
358
-
359
- /** Convert valid DTCG alias to $ref Momoa Node */
360
- export function aliasToMomoa(
361
- alias: string,
362
- loc: momoa.ObjectNode['loc'] = {
363
- start: { line: -1, column: -1, offset: 0 },
364
- end: { line: -1, column: -1, offset: 0 },
365
- },
366
- ): momoa.ObjectNode | undefined {
367
- const $ref = aliasToTokenRef(alias);
368
- if (!$ref) {
369
- return;
370
- }
371
- return {
372
- type: 'Object',
373
- members: [
374
- {
375
- type: 'Member',
376
- name: { type: 'String', value: '$ref', loc },
377
- value: { type: 'String', value: $ref.$ref, loc },
378
- loc,
379
- },
380
- ],
381
- loc,
382
- };
383
- }
384
-
385
- /**
386
- * Convert Reference Object to token ID.
387
- * This can then be turned into an alias by surrounding with { … }
388
- * ⚠️ This is not mode-aware. This will flatten multiple modes into the same root token.
389
- */
390
- export function refToTokenID($ref: ReferenceObject | string): string | undefined {
391
- const path = typeof $ref === 'object' ? $ref.$ref : $ref;
392
- if (typeof path !== 'string') {
393
- return;
394
- }
395
- const { subpath } = parseRef(path);
396
- // if this ID comes from #/$defs/…, strip the first 2 segments to get the global ID
397
- if (subpath?.[0] === '$defs') {
398
- subpath.splice(0, 2);
399
- }
400
- return (subpath?.length && subpath.join('.').replace(/\.(\$value|\$extensions).*$/, '')) || undefined;
401
- }
402
-
403
- const EXPECTED_NESTED_ALIAS: Record<string, Record<string, string[]>> = {
404
- border: {
405
- color: ['color'],
406
- stroke: ['strokeStyle'],
407
- width: ['dimension'],
408
- },
409
- gradient: {
410
- color: ['color'],
411
- position: ['number'],
412
- },
413
- shadow: {
414
- color: ['color'],
415
- offsetX: ['dimension'],
416
- offsetY: ['dimension'],
417
- blur: ['dimension'],
418
- spread: ['dimension'],
419
- inset: ['boolean'],
420
- },
421
- strokeStyle: {
422
- dashArray: ['dimension'],
423
- },
424
- transition: {
425
- duration: ['duration'],
426
- delay: ['duration'],
427
- timingFunction: ['cubicBezier'],
428
- },
429
- typography: {
430
- fontFamily: ['fontFamily'],
431
- fontWeight: ['fontWeight'],
432
- fontSize: ['dimension'],
433
- lineHeight: ['dimension', 'number'],
434
- letterSpacing: ['dimension'],
435
- },
436
- };
437
-
438
- /**
439
- * Resolve DTCG aliases, $extends, and $ref
440
- */
441
- export function resolveAliases(
442
- tokens: TokenNormalizedSet,
443
- { logger, refMap, sources }: { logger: Logger; refMap: RefMap; sources: Record<string, InputSourceWithDocument> },
444
- ): void {
445
- for (const token of Object.values(tokens)) {
446
- const aliasEntry = {
447
- group: 'parser' as const,
448
- label: 'init',
449
- src: sources[token.source.filename!]?.src,
450
- node: getObjMember(token.source.node, '$value'),
451
- };
452
-
453
- for (const mode of Object.keys(token.mode)) {
454
- function resolveInner(alias: string, refChain: string[]): string {
455
- const nextRef = aliasToTokenRef(alias, mode)?.$ref;
456
- if (!nextRef) {
457
- logger.error({ ...aliasEntry, message: `Internal error resolving ${JSON.stringify(refChain)}` });
458
- throw new Error('Internal error');
459
- }
460
- if (refChain.includes(nextRef)) {
461
- logger.error({ ...aliasEntry, message: 'Circular alias detected.' });
462
- }
463
- const nextJSONID = nextRef.replace(/\/(\$value|\$extensions).*/, '');
464
- const nextToken = tokens[nextJSONID]?.mode[mode] || tokens[nextJSONID]?.mode['.'];
465
- if (!nextToken) {
466
- logger.error({ ...aliasEntry, message: `Could not resolve alias ${alias}.` });
467
- }
468
- refChain.push(nextRef);
469
- if (isAlias(nextToken!.originalValue! as string)) {
470
- return resolveInner(nextToken!.originalValue! as string, refChain);
471
- }
472
- return nextJSONID;
473
- }
474
-
475
- function traverseAndResolve(
476
- value: any,
477
- { node, expectedTypes, path }: { node: momoa.AnyNode; expectedTypes?: string[]; path: (string | number)[] },
478
- ): any {
479
- if (typeof value !== 'string') {
480
- if (Array.isArray(value)) {
481
- for (let i = 0; i < value.length; i++) {
482
- if (!value[i]) {
483
- continue;
484
- }
485
- value[i] = traverseAndResolve(value[i], {
486
- // biome-ignore lint/suspicious/noNonNullAssertedOptionalChain: we checked for this earlier
487
- node: (node as momoa.ArrayNode).elements?.[i]?.value!,
488
- // special case: cubicBezier
489
- expectedTypes: expectedTypes?.includes('cubicBezier') ? ['number'] : expectedTypes,
490
- path: [...path, i],
491
- }).$value;
492
- }
493
- } else if (typeof value === 'object') {
494
- for (const key of Object.keys(value)) {
495
- if (!expectedTypes?.length || !EXPECTED_NESTED_ALIAS[expectedTypes[0]!]) {
496
- continue;
497
- }
498
- value[key] = traverseAndResolve(value[key], {
499
- node: getObjMember(node as momoa.ObjectNode, key)!,
500
- expectedTypes: EXPECTED_NESTED_ALIAS[expectedTypes[0]!]![key],
501
- path: [...path, key],
502
- }).$value;
503
- }
504
- }
505
- return { $value: value };
506
- }
507
-
508
- if (!isAlias(value)) {
509
- if (!expectedTypes?.includes('string') && (value.includes('{') || value.includes('}'))) {
510
- logger.error({ ...aliasEntry, message: 'Invalid alias syntax.', node });
511
- }
512
- return { $value: value };
513
- }
514
-
515
- const refChain: string[] = [];
516
- const resolvedID = resolveInner(value, refChain);
517
- if (expectedTypes?.length && !expectedTypes.includes(tokens[resolvedID]!.$type)) {
518
- logger.error({
519
- ...aliasEntry,
520
- message: `Cannot alias to $type "${tokens[resolvedID]!.$type}" from $type "${expectedTypes.join(' / ')}".`,
521
- node,
522
- });
523
- }
524
-
525
- refMap[path.join('/')] = { filename: token.source.filename!, refChain };
526
-
527
- return {
528
- $type: tokens[resolvedID]!.$type,
529
- $value: tokens[resolvedID]!.mode[mode]?.$value || tokens[resolvedID]!.$value,
530
- };
531
- }
532
-
533
- // resolve DTCG aliases without
534
- const pathBase = mode === '.' ? token.jsonID : `${token.jsonID}/$extensions/mode/${mode}`;
535
- const { $type, $value } = traverseAndResolve(token.mode[mode]!.$value, {
536
- node: aliasEntry.node!,
537
- expectedTypes: token.$type ? [token.$type] : undefined,
538
- path: [pathBase, '$value'],
539
- });
540
- if (!token.$type) {
541
- (token as any).$type = $type;
542
- }
543
- if ($value) {
544
- token.mode[mode]!.$value = $value;
545
- }
546
-
547
- // fill in $type and $value
548
- if (mode === '.') {
549
- token.$value = token.mode[mode]!.$value;
550
- }
551
- }
552
- }
553
- }
@@ -1,86 +0,0 @@
1
- import * as momoa from '@humanwhocodes/momoa';
2
- import type { InputSourceWithDocument } from '@terrazzo/json-schema-tools';
3
- import type Logger from '../logger.js';
4
- import type { ConfigInit, Group, Resolver, TokenNormalized, TokenNormalizedSet } from '../types.js';
5
- import { createResolver } from './load.js';
6
- import { normalizeResolver } from './normalize.js';
7
-
8
- export interface CreateSyntheticResolverOptions {
9
- config: ConfigInit;
10
- logger: Logger;
11
- req: (url: URL, origin: URL) => Promise<string>;
12
- sources: InputSourceWithDocument[];
13
- }
14
-
15
- /**
16
- * Interop layer upgrading legacy Terrazzo modes to resolvers
17
- */
18
- export async function createSyntheticResolver(
19
- tokens: TokenNormalizedSet,
20
- { config, logger, req, sources }: CreateSyntheticResolverOptions,
21
- ): Promise<Resolver> {
22
- const contexts: Record<string, any[]> = {};
23
- for (const token of Object.values(tokens)) {
24
- for (const [mode, value] of Object.entries(token.mode)) {
25
- if (mode === '.') {
26
- continue;
27
- }
28
- if (!(mode in contexts)) {
29
- contexts[mode] = [{}];
30
- }
31
- addToken(contexts[mode]![0], { ...token, $value: value.$value }, { logger });
32
- }
33
- }
34
-
35
- const src = JSON.stringify(
36
- {
37
- name: 'Terrazzo',
38
- version: '2025.10',
39
- resolutionOrder: [{ $ref: '#/sets/allTokens' }, { $ref: '#/modifiers/tzMode' }],
40
- sets: {
41
- allTokens: { sources: [simpleFlatten(tokens, { logger })] },
42
- },
43
- modifiers: {
44
- tzMode: {
45
- description: 'Automatically built from $extensions.mode',
46
- contexts,
47
- },
48
- },
49
- },
50
- undefined,
51
- 2,
52
- );
53
- const normalized = await normalizeResolver(momoa.parse(src), {
54
- filename: new URL('file:///virtual:resolver.json'),
55
- logger,
56
- req,
57
- src,
58
- });
59
- return createResolver(normalized, { config, logger, sources });
60
- }
61
-
62
- /** Add a normalized token back into an arbitrary, hierarchial structure */
63
- function addToken(structure: any, token: TokenNormalized, { logger }: { logger: Logger }): void {
64
- let node = structure;
65
- const parts = token.id.split('.');
66
- const localID = parts.pop()!;
67
- for (const part of parts) {
68
- if (!(part in node)) {
69
- node[part] = {};
70
- }
71
- node = node[part];
72
- }
73
- if (localID in node) {
74
- logger.error({ group: 'parser', label: 'resolver', message: `${localID} already exists!` });
75
- }
76
- node[localID] = { $type: token.$type, $value: token.$value };
77
- }
78
-
79
- /** Downconvert normalized tokens back into a simplified, hierarchial shape. This is extremely lossy, and only done to build a resolver. */
80
- function simpleFlatten(tokens: TokenNormalizedSet, { logger }: { logger: Logger }): Group {
81
- const group: Group = {};
82
- for (const token of Object.values(tokens)) {
83
- addToken(group, token, { logger });
84
- }
85
- return group;
86
- }
@@ -1,7 +0,0 @@
1
- import { loadResolver } from './load.js';
2
-
3
- export * from './load.js';
4
- export * from './normalize.js';
5
- export * from './validate.js';
6
-
7
- export default loadResolver;