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