@terrazzo/parser 0.10.4 → 2.0.0-alpha.1

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 +67 -0
  2. package/dist/index.d.ts +112 -325
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2194 -3621
  5. package/dist/index.js.map +1 -1
  6. package/package.json +4 -3
  7. package/src/build/index.ts +42 -42
  8. package/src/config.ts +13 -6
  9. package/src/lib/code-frame.ts +3 -0
  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 -328
  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 +106 -28
  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
+ }