@terrazzo/parser 0.1.3 → 0.2.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 (118) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +23 -0
  3. package/CONTRIBUTING.md +0 -12
  4. package/dist/build/index.d.ts +19 -0
  5. package/dist/build/index.js +165 -0
  6. package/dist/build/index.js.map +1 -0
  7. package/dist/config.d.ts +7 -0
  8. package/dist/config.js +269 -0
  9. package/dist/config.js.map +1 -0
  10. package/{index.d.ts → dist/index.d.ts} +1 -5
  11. package/dist/index.js +13 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/code-frame.d.ts +30 -0
  14. package/dist/lib/code-frame.js +108 -0
  15. package/dist/lib/code-frame.js.map +1 -0
  16. package/dist/lint/index.d.ts +11 -0
  17. package/dist/lint/index.js +102 -0
  18. package/dist/lint/index.js.map +1 -0
  19. package/dist/lint/plugin-core/index.d.ts +12 -0
  20. package/dist/lint/plugin-core/index.js +40 -0
  21. package/dist/lint/plugin-core/index.js.map +1 -0
  22. package/dist/lint/plugin-core/lib/docs.d.ts +1 -0
  23. package/dist/lint/plugin-core/lib/docs.js +4 -0
  24. package/dist/lint/plugin-core/lib/docs.js.map +1 -0
  25. package/dist/lint/plugin-core/rules/a11y-min-contrast.d.ts +39 -0
  26. package/dist/lint/plugin-core/rules/a11y-min-contrast.js +58 -0
  27. package/dist/lint/plugin-core/rules/a11y-min-contrast.js.map +1 -0
  28. package/dist/lint/plugin-core/rules/a11y-min-font-size.d.ts +13 -0
  29. package/dist/lint/plugin-core/rules/a11y-min-font-size.js +45 -0
  30. package/dist/lint/plugin-core/rules/a11y-min-font-size.js.map +1 -0
  31. package/dist/lint/plugin-core/rules/colorspace.d.ts +14 -0
  32. package/dist/lint/plugin-core/rules/colorspace.js +85 -0
  33. package/dist/lint/plugin-core/rules/colorspace.js.map +1 -0
  34. package/dist/lint/plugin-core/rules/consistent-naming.d.ts +11 -0
  35. package/dist/lint/plugin-core/rules/consistent-naming.js +49 -0
  36. package/dist/lint/plugin-core/rules/consistent-naming.js.map +1 -0
  37. package/dist/lint/plugin-core/rules/descriptions.d.ts +9 -0
  38. package/dist/lint/plugin-core/rules/descriptions.js +32 -0
  39. package/dist/lint/plugin-core/rules/descriptions.js.map +1 -0
  40. package/dist/lint/plugin-core/rules/duplicate-values.d.ts +9 -0
  41. package/dist/lint/plugin-core/rules/duplicate-values.js +65 -0
  42. package/dist/lint/plugin-core/rules/duplicate-values.js.map +1 -0
  43. package/dist/lint/plugin-core/rules/max-gamut.d.ts +14 -0
  44. package/dist/lint/plugin-core/rules/max-gamut.js +101 -0
  45. package/dist/lint/plugin-core/rules/max-gamut.js.map +1 -0
  46. package/dist/lint/plugin-core/rules/required-children.d.ts +18 -0
  47. package/dist/lint/plugin-core/rules/required-children.js +78 -0
  48. package/dist/lint/plugin-core/rules/required-children.js.map +1 -0
  49. package/dist/lint/plugin-core/rules/required-modes.d.ts +13 -0
  50. package/dist/lint/plugin-core/rules/required-modes.js +52 -0
  51. package/dist/lint/plugin-core/rules/required-modes.js.map +1 -0
  52. package/dist/lint/plugin-core/rules/required-typography-properties.d.ts +10 -0
  53. package/dist/lint/plugin-core/rules/required-typography-properties.js +38 -0
  54. package/dist/lint/plugin-core/rules/required-typography-properties.js.map +1 -0
  55. package/dist/logger.d.ts +76 -0
  56. package/dist/logger.js +123 -0
  57. package/dist/logger.js.map +1 -0
  58. package/dist/parse/alias.d.ts +51 -0
  59. package/dist/parse/alias.js +188 -0
  60. package/dist/parse/alias.js.map +1 -0
  61. package/dist/parse/index.d.ts +27 -0
  62. package/dist/parse/index.js +379 -0
  63. package/dist/parse/index.js.map +1 -0
  64. package/dist/parse/json.d.ts +36 -0
  65. package/dist/parse/json.js +88 -0
  66. package/dist/parse/json.js.map +1 -0
  67. package/dist/parse/normalize.d.ts +23 -0
  68. package/dist/parse/normalize.js +163 -0
  69. package/dist/parse/normalize.js.map +1 -0
  70. package/dist/parse/validate.d.ts +45 -0
  71. package/dist/parse/validate.js +601 -0
  72. package/dist/parse/validate.js.map +1 -0
  73. package/dist/types.d.ts +264 -0
  74. package/dist/types.js +2 -0
  75. package/dist/types.js.map +1 -0
  76. package/package.json +7 -7
  77. package/{build/index.js → src/build/index.ts} +47 -63
  78. package/src/config.ts +280 -0
  79. package/src/index.ts +18 -0
  80. package/{lib/code-frame.js → src/lib/code-frame.ts} +41 -8
  81. package/src/lint/index.ts +135 -0
  82. package/src/lint/plugin-core/index.ts +47 -0
  83. package/src/lint/plugin-core/lib/docs.ts +3 -0
  84. package/src/lint/plugin-core/rules/a11y-min-contrast.ts +91 -0
  85. package/src/lint/plugin-core/rules/a11y-min-font-size.ts +64 -0
  86. package/src/lint/plugin-core/rules/colorspace.ts +101 -0
  87. package/src/lint/plugin-core/rules/consistent-naming.ts +65 -0
  88. package/src/lint/plugin-core/rules/descriptions.ts +41 -0
  89. package/src/lint/plugin-core/rules/duplicate-values.ts +80 -0
  90. package/src/lint/plugin-core/rules/max-gamut.ts +121 -0
  91. package/src/lint/plugin-core/rules/required-children.ts +104 -0
  92. package/src/lint/plugin-core/rules/required-modes.ts +71 -0
  93. package/src/lint/plugin-core/rules/required-typography-properties.ts +53 -0
  94. package/{logger.js → src/logger.ts} +55 -16
  95. package/src/parse/alias.ts +224 -0
  96. package/src/parse/index.ts +457 -0
  97. package/src/parse/json.ts +106 -0
  98. package/{parse/normalize.js → src/parse/normalize.ts} +70 -24
  99. package/{parse/validate.js → src/parse/validate.ts} +154 -236
  100. package/src/types.ts +310 -0
  101. package/build/index.d.ts +0 -113
  102. package/config.d.ts +0 -64
  103. package/config.js +0 -206
  104. package/index.js +0 -35
  105. package/lib/code-frame.d.ts +0 -56
  106. package/lint/index.d.ts +0 -44
  107. package/lint/index.js +0 -59
  108. package/lint/plugin-core/index.d.ts +0 -3
  109. package/lint/plugin-core/index.js +0 -12
  110. package/lint/plugin-core/rules/duplicate-values.d.ts +0 -10
  111. package/lint/plugin-core/rules/duplicate-values.js +0 -68
  112. package/logger.d.ts +0 -71
  113. package/parse/index.d.ts +0 -45
  114. package/parse/index.js +0 -592
  115. package/parse/json.d.ts +0 -30
  116. package/parse/json.js +0 -94
  117. package/parse/normalize.d.ts +0 -3
  118. package/parse/validate.d.ts +0 -43
@@ -0,0 +1,457 @@
1
+ import { type DocumentNode, type ObjectNode, evaluate, parse as parseJSON, print } from '@humanwhocodes/momoa';
2
+ import { type Token, type TokenNormalized, isTokenMatch, pluralize, splitID } from '@terrazzo/token-tools';
3
+ import type ytm from 'yaml-to-momoa';
4
+ import lintRunner from '../lint/index.js';
5
+ import Logger from '../logger.js';
6
+ import type { ConfigInit, InputSource } from '../types.js';
7
+ import { applyAliases } from './alias.js';
8
+ import { getObjMembers, injectObjMembers, maybeJSONString, traverse } from './json.js';
9
+ import normalize from './normalize.js';
10
+ import validate from './validate.js';
11
+
12
+ export * from './validate.js';
13
+
14
+ export interface ParseOptions {
15
+ logger?: Logger;
16
+ config: ConfigInit;
17
+ /**
18
+ * Skip lint step
19
+ * @default false
20
+ */
21
+ skipLint?: boolean;
22
+ /**
23
+ * Continue on error? (Useful for `tz check`)
24
+ * @default false
25
+ */
26
+ continueOnError?: boolean;
27
+ /** Provide yamlToMomoa module to parse YAML (by default, this isn’t shipped to cut down on package weight) */
28
+ yamlToMomoa?: typeof ytm;
29
+ }
30
+
31
+ export interface ParseResult {
32
+ tokens: Record<string, TokenNormalized>;
33
+ sources: InputSource[];
34
+ }
35
+
36
+ /** Parse */
37
+ export default async function parse(
38
+ input: Omit<InputSource, 'document'>[],
39
+ {
40
+ logger = new Logger(),
41
+ skipLint = false,
42
+ config = {} as ConfigInit,
43
+ continueOnError = false,
44
+ yamlToMomoa,
45
+ }: ParseOptions = {} as ParseOptions,
46
+ ): Promise<ParseResult> {
47
+ let tokens: Record<string, TokenNormalized> = {};
48
+ // note: only keeps track of sources with locations on disk; in-memory sources are discarded
49
+ // (it’s only for reporting line numbers, which doesn’t mean as much for dynamic sources)
50
+ const sources: Record<string, InputSource> = {};
51
+
52
+ if (!Array.isArray(input)) {
53
+ logger.error({ group: 'parser', label: 'init', message: 'Input must be an array of input objects.' });
54
+ }
55
+ for (let i = 0; i < input.length; i++) {
56
+ if (!input[i] || typeof input[i] !== 'object') {
57
+ logger.error({ group: 'parser', label: 'init', message: `Input (${i}) must be an object.` });
58
+ }
59
+ if (!input[i]!.src || (typeof input[i]!.src !== 'string' && typeof input[i]!.src !== 'object')) {
60
+ logger.error({
61
+ group: 'parser',
62
+ label: 'init',
63
+ message: `Input (${i}) missing "src" with a JSON/YAML string, or JSON object.`,
64
+ });
65
+ }
66
+ if (input[i]!.filename && !(input[i]!.filename instanceof URL)) {
67
+ logger.error({
68
+ group: 'parser',
69
+ label: 'init',
70
+ message: `Input (${i}) "filename" must be a URL (remote or file URL).`,
71
+ });
72
+ }
73
+
74
+ const result = await parseSingle(input[i]!.src, {
75
+ filename: input[i]!.filename!,
76
+ logger,
77
+ config,
78
+ skipLint,
79
+ continueOnError,
80
+ yamlToMomoa,
81
+ });
82
+
83
+ tokens = Object.assign(tokens, result.tokens);
84
+ if (input[i]!.filename) {
85
+ sources[input[i]!.filename!.protocol === 'file:' ? input[i]!.filename!.href : input[i]!.filename!.href] = {
86
+ filename: input[i]!.filename,
87
+ src: result.src,
88
+ document: result.document,
89
+ };
90
+ }
91
+ }
92
+
93
+ const totalStart = performance.now();
94
+
95
+ // 5. Resolve aliases and populate groups
96
+ for (const [id, token] of Object.entries(tokens)) {
97
+ if (!Object.hasOwn(tokens, id)) {
98
+ continue;
99
+ }
100
+ applyAliases(token, {
101
+ tokens,
102
+ filename: sources[token.source.loc!]?.filename!,
103
+ src: sources[token.source.loc!]?.src as string,
104
+ node: token.source.node,
105
+ logger,
106
+ });
107
+ token.mode['.']!.$value = token.$value;
108
+ if (token.aliasOf) {
109
+ token.mode['.']!.aliasOf = token.aliasOf;
110
+ }
111
+ if (token.partialAliasOf) {
112
+ token.mode['.']!.partialAliasOf = token.partialAliasOf;
113
+ }
114
+ const { group: parentGroup } = splitID(id);
115
+ if (parentGroup) {
116
+ for (const siblingID of Object.keys(tokens)) {
117
+ const { group: siblingGroup } = splitID(siblingID);
118
+ if (siblingGroup?.startsWith(parentGroup)) {
119
+ token.group.tokens.push(siblingID);
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ // 6. resolve mode aliases
126
+ const modesStart = performance.now();
127
+ logger.debug({
128
+ group: 'parser',
129
+ label: 'modes',
130
+ message: 'Start mode resolution',
131
+ });
132
+ for (const [id, token] of Object.entries(tokens)) {
133
+ if (!Object.hasOwn(tokens, id)) {
134
+ continue;
135
+ }
136
+ for (const [mode, modeValue] of Object.entries(token.mode)) {
137
+ if (mode === '.') {
138
+ continue; // skip shadow of root value
139
+ }
140
+ applyAliases(modeValue, {
141
+ tokens,
142
+ node: modeValue.source.node,
143
+ logger,
144
+ src: sources[token.source.loc!]?.src as string,
145
+ });
146
+ }
147
+ }
148
+ logger.debug({
149
+ group: 'parser',
150
+ label: 'modes',
151
+ message: 'Finish token modes',
152
+ timing: performance.now() - modesStart,
153
+ });
154
+
155
+ logger.debug({
156
+ group: 'parser',
157
+ label: 'core',
158
+ message: 'Finish all parser tasks',
159
+ timing: performance.now() - totalStart,
160
+ });
161
+
162
+ if (continueOnError) {
163
+ const { errorCount } = logger.stats();
164
+ if (errorCount > 0) {
165
+ logger.error({
166
+ message: `Parser encountered ${errorCount} ${pluralize(errorCount, 'error', 'errors')}. Exiting.`,
167
+ });
168
+ }
169
+ }
170
+
171
+ return {
172
+ tokens,
173
+ sources: Object.values(sources),
174
+ };
175
+ }
176
+
177
+ /** Parse a single input */
178
+ async function parseSingle(
179
+ input: string | Record<string, any>,
180
+ {
181
+ filename,
182
+ logger,
183
+ config,
184
+ skipLint,
185
+ continueOnError = false,
186
+ yamlToMomoa, // optional dependency, declared here so the parser itself doesn’t have to load a heavy dep in-browser
187
+ }: {
188
+ filename: URL;
189
+ logger: Logger;
190
+ config: ConfigInit;
191
+ skipLint: boolean;
192
+ continueOnError?: boolean;
193
+ yamlToMomoa?: typeof ytm;
194
+ },
195
+ ) {
196
+ // 1. Build AST
197
+ let src: any;
198
+ if (typeof input === 'string') {
199
+ src = input;
200
+ }
201
+ const startParsing = performance.now();
202
+ logger.debug({ group: 'parser', label: 'parse', message: 'Start tokens parsing' });
203
+ let document = {} as DocumentNode;
204
+ if (typeof input === 'string' && !maybeJSONString(input)) {
205
+ if (yamlToMomoa) {
206
+ try {
207
+ document = yamlToMomoa(input); // if string, but not JSON, attempt YAML
208
+ } catch (err) {
209
+ logger.error({ message: String(err), filename, src: input, continueOnError });
210
+ }
211
+ } else {
212
+ logger.error({
213
+ group: 'parser',
214
+ message: `Install \`yaml-to-momoa\` package to parse YAML, and pass in as option, e.g.:
215
+
216
+ import { parse } from '@terrazzo/parser';
217
+ import yamlToMomoa from 'yaml-to-momoa';
218
+
219
+ parse(yamlString, { yamlToMomoa });`,
220
+ continueOnError: false, // fail here; no point in continuing
221
+ });
222
+ }
223
+ } else {
224
+ document = parseJSON(
225
+ typeof input === 'string' ? input : JSON.stringify(input, undefined, 2), // everything else: assert it’s JSON-serializable
226
+ {
227
+ mode: 'jsonc',
228
+ ranges: true,
229
+ tokens: true,
230
+ },
231
+ );
232
+ }
233
+ if (!src) {
234
+ src = print(document, { indent: 2 });
235
+ }
236
+ logger.debug({
237
+ group: 'parser',
238
+ label: 'parse',
239
+ message: 'Finish tokens parsing',
240
+ timing: performance.now() - startParsing,
241
+ });
242
+
243
+ const tokens: Record<string, TokenNormalized> = {};
244
+
245
+ // 2. Walk AST once to validate tokens
246
+ const startValidation = performance.now();
247
+ logger.debug({ group: 'parser', label: 'validate', message: 'Start tokens validation' });
248
+ const $typeInheritance: Record<string, Token['$type']> = {};
249
+ traverse(document, {
250
+ enter(node, parent, path) {
251
+ if (node.type === 'Member' && node.value.type === 'Object' && node.value.members) {
252
+ const members = getObjMembers(node.value);
253
+
254
+ // keep track of $types
255
+ if (members.$type && members.$type.type === 'String' && !members.$value) {
256
+ // @ts-ignore
257
+ $typeInheritance[path.join('.') || '.'] = node.value.members.find((m) => m.name.value === '$type');
258
+ }
259
+
260
+ const id = path.join('.');
261
+
262
+ if (members.$value) {
263
+ const extensions = members.$extensions ? getObjMembers(members.$extensions as ObjectNode) : undefined;
264
+ const sourceNode = structuredClone(node);
265
+
266
+ // get parent type by taking the closest-scoped $type (length === closer)
267
+ let parent$type: Token['$type'] | undefined;
268
+ let longestPath = '';
269
+ for (const [k, v] of Object.entries($typeInheritance)) {
270
+ if (k === '.' || id.startsWith(k)) {
271
+ if (k.length > longestPath.length) {
272
+ parent$type = v;
273
+ longestPath = k;
274
+ }
275
+ }
276
+ }
277
+ if (parent$type && !members.$type) {
278
+ sourceNode.value = injectObjMembers(
279
+ // @ts-ignore
280
+ sourceNode.value,
281
+ [parent$type],
282
+ );
283
+ }
284
+
285
+ validate(sourceNode, { filename, src, logger });
286
+
287
+ // All tokens must be valid, so we want to validate it up till this
288
+ // point. However, if we are ignoring this token (or respecting
289
+ // $deprecated, we can omit it from the output.
290
+ const $deprecated = members.$deprecated && (evaluate(members.$deprecated) as string | boolean | undefined);
291
+ if (
292
+ (config.ignore.deprecated && $deprecated) ||
293
+ (config.ignore.tokens && isTokenMatch(id, config.ignore.tokens))
294
+ ) {
295
+ return;
296
+ }
297
+
298
+ const group: TokenNormalized['group'] = { id: splitID(id).group!, tokens: [] };
299
+ if (parent$type) {
300
+ group.$type =
301
+ // @ts-ignore
302
+ parent$type.value.value;
303
+ }
304
+ // note: this will also include sibling tokens, so be selective about only accessing group-specific properties
305
+ const groupMembers = getObjMembers(
306
+ // @ts-ignore
307
+ parent,
308
+ );
309
+ if (groupMembers.$description) {
310
+ group.$description = evaluate(groupMembers.$description) as string;
311
+ }
312
+ if (groupMembers.$extensions) {
313
+ group.$extensions = evaluate(groupMembers.$extensions) as Record<string, unknown>;
314
+ }
315
+ const token: TokenNormalized = {
316
+ // @ts-ignore
317
+ $type: members.$type?.value ?? parent$type?.value.value,
318
+ // @ts-ignore
319
+ $value: evaluate(members.$value),
320
+ id,
321
+ // @ts-ignore
322
+ mode: {},
323
+ // @ts-ignore
324
+ originalValue: evaluate(node.value),
325
+ group,
326
+ source: {
327
+ loc: filename ? filename.href : undefined,
328
+ // @ts-ignore
329
+ node: sourceNode.value,
330
+ },
331
+ };
332
+ // @ts-ignore
333
+ if (members.$description?.value) {
334
+ // @ts-ignore
335
+ token.$description = members.$description.value;
336
+ }
337
+
338
+ // handle modes
339
+ // note that circular refs are avoided here, such as not duplicating `modes`
340
+ const modeValues = extensions?.mode
341
+ ? getObjMembers(
342
+ // @ts-ignore
343
+ extensions.mode,
344
+ )
345
+ : {};
346
+ for (const mode of ['.', ...Object.keys(modeValues)]) {
347
+ token.mode[mode] = {
348
+ id: token.id,
349
+ // @ts-ignore
350
+ $type: token.$type,
351
+ // @ts-ignore
352
+ $value: mode === '.' ? token.$value : evaluate(modeValues[mode]),
353
+ source: {
354
+ loc: filename ? filename.href : undefined,
355
+ // @ts-ignore
356
+ node: mode === '.' ? structuredClone(token.source.node) : modeValues[mode],
357
+ },
358
+ };
359
+ if (token.$description) {
360
+ token.mode[mode]!.$description = token.$description;
361
+ }
362
+ }
363
+
364
+ tokens[id] = token;
365
+ } else if (!id.includes('.$value') && members.value) {
366
+ logger.warn({ message: `Group ${id} has "value". Did you mean "$value"?`, filename, node, src });
367
+ }
368
+ }
369
+
370
+ // edge case: if $type appears at root of tokens.json, collect it
371
+ if (node.type === 'Document' && node.body.type === 'Object' && node.body.members) {
372
+ const members = getObjMembers(node.body);
373
+ if (members.$type && members.$type.type === 'String' && !members.$value) {
374
+ // @ts-ignore
375
+ $typeInheritance['.'] = node.body.members.find((m) => m.name.value === '$type');
376
+ }
377
+ }
378
+ },
379
+ });
380
+ logger.debug({
381
+ group: 'parser',
382
+ label: 'validate',
383
+ message: 'Finish tokens validation',
384
+ timing: performance.now() - startValidation,
385
+ });
386
+
387
+ // 3. normalize values
388
+ const normalizeStart = performance.now();
389
+ logger.debug({
390
+ group: 'parser',
391
+ label: 'normalize',
392
+ message: 'Start token normalization',
393
+ });
394
+ for (const [id, token] of Object.entries(tokens)) {
395
+ try {
396
+ tokens[id]!.$value = normalize(token);
397
+ } catch (err) {
398
+ let { node } = token.source;
399
+ const members = getObjMembers(node);
400
+ if (members.$value) {
401
+ node = members.$value as ObjectNode;
402
+ }
403
+ logger.error({ message: (err as Error).message, filename, src, node, continueOnError });
404
+ }
405
+ for (const [mode, modeValue] of Object.entries(token.mode)) {
406
+ if (mode === '.') {
407
+ continue;
408
+ }
409
+ try {
410
+ tokens[id]!.mode[mode]!.$value = normalize(modeValue);
411
+ } catch (err) {
412
+ let { node } = token.source;
413
+ const members = getObjMembers(node);
414
+ if (members.$value) {
415
+ node = members.$value as ObjectNode;
416
+ }
417
+ logger.error({
418
+ message: (err as Error).message,
419
+ filename,
420
+ src,
421
+ node: modeValue.source.node,
422
+ continueOnError,
423
+ });
424
+ }
425
+ }
426
+ }
427
+
428
+ // 4. Execute lint runner with loaded plugins
429
+ if (!skipLint && config?.plugins?.length) {
430
+ const lintStart = performance.now();
431
+ logger.debug({
432
+ group: 'parser',
433
+ label: 'validate',
434
+ message: 'Start token linting',
435
+ });
436
+ await lintRunner({ tokens, src, config, logger });
437
+ logger.debug({
438
+ group: 'parser',
439
+ label: 'validate',
440
+ message: 'Finish token linting',
441
+ timing: performance.now() - lintStart,
442
+ });
443
+ }
444
+
445
+ logger.debug({
446
+ group: 'parser',
447
+ label: 'normalize',
448
+ message: 'Finish token normalization',
449
+ timing: performance.now() - normalizeStart,
450
+ });
451
+
452
+ return {
453
+ tokens,
454
+ document,
455
+ src,
456
+ };
457
+ }
@@ -0,0 +1,106 @@
1
+ import type { AnyNode, MemberNode, ObjectNode, ValueNode } from '@humanwhocodes/momoa';
2
+
3
+ export interface Visitor {
4
+ enter?: (node: AnyNode, parent: AnyNode | undefined, path: string[]) => void;
5
+ exit?: (node: AnyNode, parent: AnyNode | undefined, path: string[]) => void;
6
+ }
7
+
8
+ export const CHILD_KEYS = {
9
+ Document: ['body'] as const,
10
+ Object: ['members'] as const,
11
+ Member: ['name', 'value'] as const,
12
+ Element: ['value'] as const,
13
+ Array: ['elements'] as const,
14
+ String: [] as const,
15
+ Number: [] as const,
16
+ Boolean: [] as const,
17
+ Null: [] as const,
18
+ Identifier: [] as const,
19
+ NaN: [] as const,
20
+ Infinity: [] as const,
21
+ };
22
+
23
+ /** Determines if a given value is an AST node. */
24
+ export function isNode(value: unknown): boolean {
25
+ return !!value && typeof value === 'object' && 'type' in value && typeof value.type === 'string';
26
+ }
27
+
28
+ /** Get ObjectNode members as object */
29
+ export function getObjMembers(node: ObjectNode): Record<string | number, ValueNode> {
30
+ const members: Record<string | number, ValueNode> = {};
31
+ if (node.type !== 'Object') {
32
+ return members;
33
+ }
34
+ for (const m of node.members) {
35
+ if (m.name.type !== 'String') {
36
+ continue;
37
+ }
38
+ members[m.name.value] = m.value;
39
+ }
40
+ return members;
41
+ }
42
+
43
+ /**
44
+ * Inject members to ObjectNode and return a clone
45
+ * @param {ObjectNode} node
46
+ * @param {MemberNode[]} members
47
+ * @return {ObjectNode}
48
+ */
49
+ export function injectObjMembers(node: ObjectNode, members: MemberNode[] = []): ObjectNode {
50
+ if (node.type !== 'Object') {
51
+ return node;
52
+ }
53
+ const newNode = structuredClone(node);
54
+ newNode.members.push(...members);
55
+ return newNode;
56
+ }
57
+
58
+ /**
59
+ * Variation of Momoa’s traverse(), which keeps track of global path
60
+ */
61
+ export function traverse(root: AnyNode, visitor: Visitor) {
62
+ /**
63
+ * Recursively visits a node.
64
+ * @param {AnyNode} node The node to visit.
65
+ * @param {AnyNode} [parent] The parent of the node to visit.
66
+ * @return {void}
67
+ */
68
+ function visitNode(node: AnyNode, parent: AnyNode | undefined, path: string[] = []) {
69
+ const nextPath = [...path];
70
+ if (node.type === 'Member') {
71
+ const { name } = node;
72
+ nextPath.push('value' in name ? name.value : String(name));
73
+ }
74
+
75
+ visitor.enter?.(node, parent, nextPath);
76
+
77
+ const childNode = CHILD_KEYS[node.type];
78
+ for (const key of childNode ?? []) {
79
+ const value = node[key as keyof typeof node];
80
+
81
+ if (value && typeof value === 'object') {
82
+ if (Array.isArray(value)) {
83
+ for (let i = 0; i < value.length; i++) {
84
+ visitNode(
85
+ // @ts-expect-error this is safe
86
+ value[i],
87
+ node,
88
+ key === 'elements' ? [...nextPath, String(i)] : nextPath,
89
+ );
90
+ }
91
+ } else if (isNode(value)) {
92
+ visitNode(value as unknown as AnyNode, node, nextPath);
93
+ }
94
+ }
95
+ }
96
+
97
+ visitor.exit?.(node, parent, nextPath);
98
+ }
99
+
100
+ visitNode(root, undefined, []);
101
+ }
102
+
103
+ /** Determine if an input is likely a JSON string */
104
+ export function maybeJSONString(input: string): boolean {
105
+ return typeof input === 'string' && input.trim().startsWith('{');
106
+ }