@terrazzo/parser 2.0.0-alpha.2 → 2.0.0-alpha.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 +5 -12
- package/README.md +1 -1
- package/dist/index.d.ts +181 -57
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +877 -122
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/build/index.ts +2 -2
- package/src/config.ts +1 -2
- package/src/index.ts +2 -0
- package/src/lib/resolver-utils.ts +35 -0
- package/src/lint/index.ts +4 -3
- package/src/lint/plugin-core/index.ts +3 -4
- package/src/lint/plugin-core/lib/docs.ts +1 -1
- package/src/lint/plugin-core/rules/{no-type-on-alias.ts → required-type.ts} +5 -6
- package/src/parse/index.ts +51 -4
- package/src/parse/load.ts +25 -111
- package/src/parse/process.ts +124 -0
- package/src/parse/token.ts +12 -7
- package/src/resolver/create-synthetic-resolver.ts +86 -0
- package/src/resolver/index.ts +7 -0
- package/src/resolver/load.ts +216 -0
- package/src/resolver/normalize.ts +106 -0
- package/src/resolver/validate.ts +363 -0
- package/src/types.ts +113 -44
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import pc from "picocolors";
|
|
|
4
4
|
import { merge } from "merge-anything";
|
|
5
5
|
import { BORDER_REQUIRED_PROPERTIES, COLORSPACE, FONT_WEIGHTS, GRADIENT_REQUIRED_STOP_PROPERTIES, SHADOW_REQUIRED_PROPERTIES, STROKE_STYLE_LINE_CAP_VALUES, STROKE_STYLE_OBJECT_REQUIRED_PROPERTIES, STROKE_STYLE_STRING_VALUES, TRANSITION_REQUIRED_PROPERTIES, isAlias, parseAlias, parseColor, pluralize, tokenToCulori } from "@terrazzo/token-tools";
|
|
6
6
|
import { clampChroma, wcagContrast } from "culori";
|
|
7
|
-
import { bundle, getObjMember, getObjMembers, isPure$ref, parseRef, replaceNode,
|
|
7
|
+
import { bundle, getObjMember, getObjMembers, isPure$ref, maybeRawJSON, parseRef, replaceNode, traverse } from "@terrazzo/json-schema-tools";
|
|
8
8
|
|
|
9
9
|
//#region src/lib/code-frame.ts
|
|
10
10
|
/**
|
|
@@ -34,10 +34,8 @@ function getMarkerLines(loc, source, opts = {}) {
|
|
|
34
34
|
if (lineDiff) for (let i = 0; i <= lineDiff; i++) {
|
|
35
35
|
const lineNumber = i + startLine;
|
|
36
36
|
if (!startColumn) markerLines[lineNumber] = true;
|
|
37
|
-
else if (i === 0)
|
|
38
|
-
|
|
39
|
-
markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
|
|
40
|
-
} else if (i === lineDiff) markerLines[lineNumber] = [0, endColumn];
|
|
37
|
+
else if (i === 0) markerLines[lineNumber] = [startColumn, source[lineNumber - 1].length - startColumn + 1];
|
|
38
|
+
else if (i === lineDiff) markerLines[lineNumber] = [0, endColumn];
|
|
41
39
|
else markerLines[lineNumber] = [0, source[lineNumber - i].length];
|
|
42
40
|
}
|
|
43
41
|
else if (startColumn === endColumn) if (startColumn) markerLines[startLine] = [startColumn, 0];
|
|
@@ -55,8 +53,7 @@ function getMarkerLines(loc, source, opts = {}) {
|
|
|
55
53
|
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
|
|
56
54
|
function codeFrameColumns(rawLines, loc, opts = {}) {
|
|
57
55
|
if (typeof rawLines !== "string") throw new Error(`Expected string, got ${rawLines}`);
|
|
58
|
-
const
|
|
59
|
-
const { start, end, markerLines } = getMarkerLines(loc, lines, opts);
|
|
56
|
+
const { start, end, markerLines } = getMarkerLines(loc, rawLines.split(NEWLINE), opts);
|
|
60
57
|
const hasColumns = loc.start && typeof loc.start.column === "number";
|
|
61
58
|
const numberMaxWidth = String(end).length;
|
|
62
59
|
let frame = rawLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
|
|
@@ -354,7 +351,7 @@ async function build(tokens, { sources, logger = new Logger(), config }) {
|
|
|
354
351
|
//#endregion
|
|
355
352
|
//#region src/lint/plugin-core/lib/docs.ts
|
|
356
353
|
function docsLink(ruleName) {
|
|
357
|
-
return `https://terrazzo.app/docs/
|
|
354
|
+
return `https://terrazzo.app/docs/linting#${ruleName.replaceAll("/", "")}`;
|
|
358
355
|
}
|
|
359
356
|
|
|
360
357
|
//#endregion
|
|
@@ -390,9 +387,7 @@ const rule$26 = {
|
|
|
390
387
|
if (tokens[foreground].$type !== "color") throw new Error(`Token ${foreground} isn’t a color`);
|
|
391
388
|
if (!tokens[background]) throw new Error(`Token ${background} does not exist`);
|
|
392
389
|
if (tokens[background].$type !== "color") throw new Error(`Token ${background} isn’t a color`);
|
|
393
|
-
const
|
|
394
|
-
const b = tokenToCulori(tokens[background].$value);
|
|
395
|
-
const contrast = wcagContrast(a, b);
|
|
390
|
+
const contrast = wcagContrast(tokenToCulori(tokens[foreground].$value), tokenToCulori(tokens[background].$value));
|
|
396
391
|
const min = WCAG2_MIN_CONTRAST[options.level ?? "AA"][largeText ? "large" : "default"];
|
|
397
392
|
if (contrast < min) report({
|
|
398
393
|
messageId: ERROR_INSUFFICIENT_CONTRAST,
|
|
@@ -719,8 +714,7 @@ function isWithinGamut(color, gamut) {
|
|
|
719
714
|
"hsl",
|
|
720
715
|
"hwb"
|
|
721
716
|
].includes(parsed.mode)) return true;
|
|
722
|
-
|
|
723
|
-
return isWithinThreshold(parsed, clamped);
|
|
717
|
+
return isWithinThreshold(parsed, clampChroma(parsed, parsed.mode, gamut === "srgb" ? "rgb" : gamut));
|
|
724
718
|
}
|
|
725
719
|
/** is Color A close enough to Color B? */
|
|
726
720
|
function isWithinThreshold(a, b, tolerance = TOLERANCE) {
|
|
@@ -807,36 +801,13 @@ const rule$20 = {
|
|
|
807
801
|
};
|
|
808
802
|
var max_gamut_default = rule$20;
|
|
809
803
|
|
|
810
|
-
//#endregion
|
|
811
|
-
//#region src/lint/plugin-core/rules/no-type-on-alias.ts
|
|
812
|
-
const NO_TYPE_ON_ALIAS = "core/no-type-on-alias";
|
|
813
|
-
const ERROR$10 = "ERROR";
|
|
814
|
-
const rule$19 = {
|
|
815
|
-
meta: {
|
|
816
|
-
messages: { [ERROR$10]: "Remove $type from aliased value." },
|
|
817
|
-
docs: {
|
|
818
|
-
description: "If a $value is aliased it already has a $type defined.",
|
|
819
|
-
url: docsLink(NO_TYPE_ON_ALIAS)
|
|
820
|
-
}
|
|
821
|
-
},
|
|
822
|
-
defaultOptions: {},
|
|
823
|
-
create({ tokens, report }) {
|
|
824
|
-
for (const t of Object.values(tokens)) if (isAlias(t.originalValue.$value) && t.originalValue?.$type) report({
|
|
825
|
-
messageId: ERROR$10,
|
|
826
|
-
node: t.source.node,
|
|
827
|
-
filename: t.source.filename
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
};
|
|
831
|
-
var no_type_on_alias_default = rule$19;
|
|
832
|
-
|
|
833
804
|
//#endregion
|
|
834
805
|
//#region src/lint/plugin-core/rules/required-children.ts
|
|
835
806
|
const REQUIRED_CHILDREN = "core/required-children";
|
|
836
807
|
const ERROR_EMPTY_MATCH = "EMPTY_MATCH";
|
|
837
808
|
const ERROR_MISSING_REQUIRED_TOKENS = "MISSING_REQUIRED_TOKENS";
|
|
838
809
|
const ERROR_MISSING_REQUIRED_GROUP = "MISSING_REQUIRED_GROUP";
|
|
839
|
-
const rule$
|
|
810
|
+
const rule$19 = {
|
|
840
811
|
meta: {
|
|
841
812
|
messages: {
|
|
842
813
|
[ERROR_EMPTY_MATCH]: "No tokens matched {{ matcher }}",
|
|
@@ -894,12 +865,12 @@ const rule$18 = {
|
|
|
894
865
|
}
|
|
895
866
|
}
|
|
896
867
|
};
|
|
897
|
-
var required_children_default = rule$
|
|
868
|
+
var required_children_default = rule$19;
|
|
898
869
|
|
|
899
870
|
//#endregion
|
|
900
871
|
//#region src/lint/plugin-core/rules/required-modes.ts
|
|
901
872
|
const REQUIRED_MODES = "core/required-modes";
|
|
902
|
-
const rule$
|
|
873
|
+
const rule$18 = {
|
|
903
874
|
meta: { docs: {
|
|
904
875
|
description: "Enforce certain tokens have specific modes.",
|
|
905
876
|
url: docsLink(REQUIRED_MODES)
|
|
@@ -930,7 +901,30 @@ const rule$17 = {
|
|
|
930
901
|
}
|
|
931
902
|
}
|
|
932
903
|
};
|
|
933
|
-
var required_modes_default = rule$
|
|
904
|
+
var required_modes_default = rule$18;
|
|
905
|
+
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/lint/plugin-core/rules/required-type.ts
|
|
908
|
+
const REQUIRED_TYPE = "core/required-type";
|
|
909
|
+
const ERROR$10 = "ERROR";
|
|
910
|
+
const rule$17 = {
|
|
911
|
+
meta: {
|
|
912
|
+
messages: { [ERROR$10]: "Token missing $type." },
|
|
913
|
+
docs: {
|
|
914
|
+
description: "Requiring every token to have $type, even aliases, simplifies computation.",
|
|
915
|
+
url: docsLink(REQUIRED_TYPE)
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
defaultOptions: {},
|
|
919
|
+
create({ tokens, report }) {
|
|
920
|
+
for (const t of Object.values(tokens)) if (!t.originalValue?.$type) report({
|
|
921
|
+
messageId: ERROR$10,
|
|
922
|
+
node: t.source.node,
|
|
923
|
+
filename: t.source.filename
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
var required_type_default = rule$17;
|
|
934
928
|
|
|
935
929
|
//#endregion
|
|
936
930
|
//#region src/lint/plugin-core/rules/required-typography-properties.ts
|
|
@@ -1340,8 +1334,7 @@ const rule$11 = {
|
|
|
1340
1334
|
break;
|
|
1341
1335
|
case "strokeStyle":
|
|
1342
1336
|
if (typeof t.originalValue.$value === "object" && Array.isArray(t.originalValue.$value.dashArray)) {
|
|
1343
|
-
const
|
|
1344
|
-
const dashArray = getObjMember($valueNode, "dashArray");
|
|
1337
|
+
const dashArray = getObjMember(getObjMember(t.source.node, "$value"), "dashArray");
|
|
1345
1338
|
for (let i = 0; i < t.originalValue.$value.dashArray.length; i++) {
|
|
1346
1339
|
if (isAlias(t.originalValue.$value.dashArray[i])) continue;
|
|
1347
1340
|
validateDimension(t.originalValue.$value.dashArray[i], {
|
|
@@ -1359,8 +1352,7 @@ const rule$11 = {
|
|
|
1359
1352
|
filename: t.source.filename
|
|
1360
1353
|
});
|
|
1361
1354
|
if (typeof t.originalValue.$value.style === "object" && Array.isArray(t.originalValue.$value.style.dashArray)) {
|
|
1362
|
-
const
|
|
1363
|
-
const dashArray = getObjMember(style, "dashArray");
|
|
1355
|
+
const dashArray = getObjMember(getObjMember($valueNode, "style"), "dashArray");
|
|
1364
1356
|
for (let i = 0; i < t.originalValue.$value.style.dashArray.length; i++) {
|
|
1365
1357
|
if (isAlias(t.originalValue.$value.style.dashArray[i])) continue;
|
|
1366
1358
|
validateDimension(t.originalValue.$value.style.dashArray[i], {
|
|
@@ -1580,8 +1572,7 @@ const rule$9 = {
|
|
|
1580
1572
|
case "typography":
|
|
1581
1573
|
if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontFamily) {
|
|
1582
1574
|
if (t.partialAliasOf?.fontFamily) continue;
|
|
1583
|
-
const
|
|
1584
|
-
const properties = getObjMembers($value);
|
|
1575
|
+
const properties = getObjMembers(getObjMember(t.source.node, "$value"));
|
|
1585
1576
|
validateFontFamily(t.originalValue.$value.fontFamily, {
|
|
1586
1577
|
node: properties.fontFamily,
|
|
1587
1578
|
filename: t.source.filename
|
|
@@ -1643,8 +1634,7 @@ const rule$8 = {
|
|
|
1643
1634
|
case "typography":
|
|
1644
1635
|
if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontWeight) {
|
|
1645
1636
|
if (t.partialAliasOf?.fontWeight) continue;
|
|
1646
|
-
const
|
|
1647
|
-
const properties = getObjMembers($value);
|
|
1637
|
+
const properties = getObjMembers(getObjMember(t.source.node, "$value"));
|
|
1648
1638
|
validateFontWeight(t.originalValue.$value.fontWeight, {
|
|
1649
1639
|
node: properties.fontWeight,
|
|
1650
1640
|
filename: t.source.filename
|
|
@@ -2113,9 +2103,9 @@ const ALL_RULES = {
|
|
|
2113
2103
|
[DESCRIPTIONS]: descriptions_default,
|
|
2114
2104
|
[DUPLICATE_VALUES]: duplicate_values_default,
|
|
2115
2105
|
[MAX_GAMUT]: max_gamut_default,
|
|
2116
|
-
[NO_TYPE_ON_ALIAS]: no_type_on_alias_default,
|
|
2117
2106
|
[REQUIRED_CHILDREN]: required_children_default,
|
|
2118
2107
|
[REQUIRED_MODES]: required_modes_default,
|
|
2108
|
+
[REQUIRED_TYPE]: required_type_default,
|
|
2119
2109
|
[REQUIRED_TYPOGRAPHY_PROPERTIES]: required_typography_properties_default,
|
|
2120
2110
|
[A11Y_MIN_CONTRAST]: a11y_min_contrast_default,
|
|
2121
2111
|
[A11Y_MIN_FONT_SIZE]: a11y_min_font_size_default
|
|
@@ -2145,8 +2135,7 @@ const RECOMMENDED_CONFIG = {
|
|
|
2145
2135
|
[VALID_SHADOW]: ["error", {}],
|
|
2146
2136
|
[VALID_GRADIENT]: ["error", {}],
|
|
2147
2137
|
[VALID_TYPOGRAPHY]: ["error", {}],
|
|
2148
|
-
[CONSISTENT_NAMING]: ["warn", { format: "kebab-case" }]
|
|
2149
|
-
[NO_TYPE_ON_ALIAS]: ["warn", {}]
|
|
2138
|
+
[CONSISTENT_NAMING]: ["warn", { format: "kebab-case" }]
|
|
2150
2139
|
};
|
|
2151
2140
|
|
|
2152
2141
|
//#endregion
|
|
@@ -2321,7 +2310,7 @@ function normalizeLint({ config, logger }) {
|
|
|
2321
2310
|
});
|
|
2322
2311
|
const value = config.lint.rules[id];
|
|
2323
2312
|
let severity = "off";
|
|
2324
|
-
let options;
|
|
2313
|
+
let options = {};
|
|
2325
2314
|
if (typeof value === "number" || typeof value === "string") severity = value;
|
|
2326
2315
|
else if (Array.isArray(value)) {
|
|
2327
2316
|
severity = value[0];
|
|
@@ -2480,6 +2469,410 @@ function toMomoa(srcRaw) {
|
|
|
2480
2469
|
});
|
|
2481
2470
|
}
|
|
2482
2471
|
|
|
2472
|
+
//#endregion
|
|
2473
|
+
//#region src/lib/resolver-utils.ts
|
|
2474
|
+
/**
|
|
2475
|
+
* If tokens are found inside a resolver, strip out the resolver paths (don’t
|
|
2476
|
+
* include "sets"/"modifiers" in the token ID etc.)
|
|
2477
|
+
*/
|
|
2478
|
+
function filterResolverPaths(path) {
|
|
2479
|
+
switch (path[0]) {
|
|
2480
|
+
case "sets": return path.slice(4);
|
|
2481
|
+
case "modifiers": return path.slice(5);
|
|
2482
|
+
case "resolutionOrder":
|
|
2483
|
+
switch (path[2]) {
|
|
2484
|
+
case "sources": return path.slice(4);
|
|
2485
|
+
case "contexts": return path.slice(5);
|
|
2486
|
+
}
|
|
2487
|
+
break;
|
|
2488
|
+
}
|
|
2489
|
+
return path;
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* Make a deterministic string from an object
|
|
2493
|
+
*/
|
|
2494
|
+
function makeInputKey(input) {
|
|
2495
|
+
return JSON.stringify(Object.fromEntries(Object.entries(input).sort((a, b) => a[0].localeCompare(b[0], "en-us", { numeric: true }))));
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
//#endregion
|
|
2499
|
+
//#region src/resolver/validate.ts
|
|
2500
|
+
/**
|
|
2501
|
+
* Determine whether this is likely a resolver
|
|
2502
|
+
* We use terms the word “likely” because this occurs before validation. Since
|
|
2503
|
+
* we may be dealing with a doc _intended_ to be a resolver, but may be lacking
|
|
2504
|
+
* some critical information, how can we determine intent? There’s a bit of
|
|
2505
|
+
* guesswork here, but we try and find a reasonable edge case where we sniff out
|
|
2506
|
+
* invalid DTCG syntax that a resolver doc would have.
|
|
2507
|
+
*/
|
|
2508
|
+
function isLikelyResolver(doc) {
|
|
2509
|
+
if (doc.body.type !== "Object") return false;
|
|
2510
|
+
for (const member of doc.body.members) {
|
|
2511
|
+
if (member.name.type !== "String") continue;
|
|
2512
|
+
switch (member.name.value) {
|
|
2513
|
+
case "name":
|
|
2514
|
+
case "description":
|
|
2515
|
+
case "version":
|
|
2516
|
+
if (member.name.type === "String") return true;
|
|
2517
|
+
break;
|
|
2518
|
+
case "sets":
|
|
2519
|
+
case "modifiers":
|
|
2520
|
+
if (member.value.type !== "Object") continue;
|
|
2521
|
+
if (getObjMember(member.value, "description")?.type === "String") return true;
|
|
2522
|
+
if (member.name.value === "sets" && getObjMember(member.value, "sources")?.type === "Array") return true;
|
|
2523
|
+
else if (member.name.value === "modifiers") {
|
|
2524
|
+
const contexts = getObjMember(member.value, "contexts");
|
|
2525
|
+
if (contexts?.type === "Object" && contexts.members.some((m) => m.value.type === "Array")) return true;
|
|
2526
|
+
}
|
|
2527
|
+
break;
|
|
2528
|
+
case "resolutionOrder":
|
|
2529
|
+
if (member.value.type === "Array") return true;
|
|
2530
|
+
break;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
return false;
|
|
2534
|
+
}
|
|
2535
|
+
const MESSAGE_EXPECTED = {
|
|
2536
|
+
STRING: "Expected string.",
|
|
2537
|
+
OBJECT: "Expected object.",
|
|
2538
|
+
ARRAY: "Expected array."
|
|
2539
|
+
};
|
|
2540
|
+
/**
|
|
2541
|
+
* Validate a resolver document.
|
|
2542
|
+
* There’s a ton of boilerplate here, only to surface detailed code frames. Is there a better abstraction?
|
|
2543
|
+
*/
|
|
2544
|
+
function validateResolver(node, { logger, src }) {
|
|
2545
|
+
const entry = {
|
|
2546
|
+
group: "parser",
|
|
2547
|
+
label: "resolver",
|
|
2548
|
+
src
|
|
2549
|
+
};
|
|
2550
|
+
if (node.body.type !== "Object") logger.error({
|
|
2551
|
+
...entry,
|
|
2552
|
+
message: MESSAGE_EXPECTED.OBJECT,
|
|
2553
|
+
node
|
|
2554
|
+
});
|
|
2555
|
+
const errors = [];
|
|
2556
|
+
let hasVersion = false;
|
|
2557
|
+
let hasResolutionOrder = false;
|
|
2558
|
+
for (const member of node.body.members) {
|
|
2559
|
+
if (member.name.type !== "String") continue;
|
|
2560
|
+
switch (member.name.value) {
|
|
2561
|
+
case "name":
|
|
2562
|
+
case "description":
|
|
2563
|
+
if (member.value.type !== "String") errors.push({
|
|
2564
|
+
...entry,
|
|
2565
|
+
message: MESSAGE_EXPECTED.STRING
|
|
2566
|
+
});
|
|
2567
|
+
break;
|
|
2568
|
+
case "version":
|
|
2569
|
+
hasVersion = true;
|
|
2570
|
+
if (member.value.type !== "String" || member.value.value !== "2025.10") errors.push({
|
|
2571
|
+
...entry,
|
|
2572
|
+
message: `Expected "version" to be "2025.10".`,
|
|
2573
|
+
node: member.value
|
|
2574
|
+
});
|
|
2575
|
+
break;
|
|
2576
|
+
case "sets":
|
|
2577
|
+
case "modifiers":
|
|
2578
|
+
if (member.value.type !== "Object") errors.push({
|
|
2579
|
+
...entry,
|
|
2580
|
+
message: MESSAGE_EXPECTED.OBJECT,
|
|
2581
|
+
node: member.value
|
|
2582
|
+
});
|
|
2583
|
+
else for (const item of member.value.members) if (item.value.type !== "Object") errors.push({
|
|
2584
|
+
...entry,
|
|
2585
|
+
message: MESSAGE_EXPECTED.OBJECT,
|
|
2586
|
+
node: item.value
|
|
2587
|
+
});
|
|
2588
|
+
else {
|
|
2589
|
+
const validator = member.name.value === "sets" ? validateSet : validateModifier;
|
|
2590
|
+
errors.push(...validator(item.value, false, {
|
|
2591
|
+
logger,
|
|
2592
|
+
src
|
|
2593
|
+
}));
|
|
2594
|
+
}
|
|
2595
|
+
break;
|
|
2596
|
+
case "resolutionOrder":
|
|
2597
|
+
hasResolutionOrder = true;
|
|
2598
|
+
if (member.value.type !== "Array") errors.push({
|
|
2599
|
+
...entry,
|
|
2600
|
+
message: MESSAGE_EXPECTED.ARRAY,
|
|
2601
|
+
node: member.value
|
|
2602
|
+
});
|
|
2603
|
+
else if (member.value.elements.length === 0) errors.push({
|
|
2604
|
+
...entry,
|
|
2605
|
+
message: `"resolutionOrder" can’t be empty array.`,
|
|
2606
|
+
node: member.value
|
|
2607
|
+
});
|
|
2608
|
+
else for (const item of member.value.elements) if (item.value.type !== "Object") errors.push({
|
|
2609
|
+
...entry,
|
|
2610
|
+
message: MESSAGE_EXPECTED.OBJECT,
|
|
2611
|
+
node: item.value
|
|
2612
|
+
});
|
|
2613
|
+
else {
|
|
2614
|
+
const itemMembers = getObjMembers(item.value);
|
|
2615
|
+
if (itemMembers.$ref?.type === "String") continue;
|
|
2616
|
+
if (itemMembers.type?.type === "String") if (itemMembers.type.value === "set") validateSet(item.value, true, {
|
|
2617
|
+
logger,
|
|
2618
|
+
src
|
|
2619
|
+
});
|
|
2620
|
+
else if (itemMembers.type.value === "modifier") validateModifier(item.value, true, {
|
|
2621
|
+
logger,
|
|
2622
|
+
src
|
|
2623
|
+
});
|
|
2624
|
+
else errors.push({
|
|
2625
|
+
...entry,
|
|
2626
|
+
message: `Unknown type ${JSON.stringify(itemMembers.type.value)}`,
|
|
2627
|
+
node: itemMembers.type
|
|
2628
|
+
});
|
|
2629
|
+
if (itemMembers.sources?.type === "Array") validateSet(item.value, true, {
|
|
2630
|
+
logger,
|
|
2631
|
+
src
|
|
2632
|
+
});
|
|
2633
|
+
else if (itemMembers.contexts?.type === "Object") validateModifier(item.value, true, {
|
|
2634
|
+
logger,
|
|
2635
|
+
src
|
|
2636
|
+
});
|
|
2637
|
+
else if (itemMembers.name?.type === "String" || itemMembers.description?.type === "String") validateSet(item.value, true, {
|
|
2638
|
+
logger,
|
|
2639
|
+
src
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
break;
|
|
2643
|
+
case "$defs":
|
|
2644
|
+
case "$extensions":
|
|
2645
|
+
if (member.value.type !== "Object") errors.push({
|
|
2646
|
+
...entry,
|
|
2647
|
+
message: `Expected object`,
|
|
2648
|
+
node: member.value
|
|
2649
|
+
});
|
|
2650
|
+
break;
|
|
2651
|
+
case "$ref":
|
|
2652
|
+
if (member.value.type !== "String") errors.push({
|
|
2653
|
+
...entry,
|
|
2654
|
+
message: `Expected string`,
|
|
2655
|
+
node: member.value
|
|
2656
|
+
});
|
|
2657
|
+
break;
|
|
2658
|
+
default:
|
|
2659
|
+
errors.push({
|
|
2660
|
+
...entry,
|
|
2661
|
+
message: `Unknown key ${JSON.stringify(member.name.value)}`,
|
|
2662
|
+
node: member.name,
|
|
2663
|
+
src
|
|
2664
|
+
});
|
|
2665
|
+
break;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
if (!hasVersion) errors.push({
|
|
2669
|
+
...entry,
|
|
2670
|
+
message: `Missing "version".`,
|
|
2671
|
+
node,
|
|
2672
|
+
src
|
|
2673
|
+
});
|
|
2674
|
+
if (!hasResolutionOrder) errors.push({
|
|
2675
|
+
...entry,
|
|
2676
|
+
message: `Missing "resolutionOrder".`,
|
|
2677
|
+
node,
|
|
2678
|
+
src
|
|
2679
|
+
});
|
|
2680
|
+
if (errors.length) logger.error(...errors);
|
|
2681
|
+
}
|
|
2682
|
+
function validateSet(node, isInline = false, { src }) {
|
|
2683
|
+
const entry = {
|
|
2684
|
+
group: "parser",
|
|
2685
|
+
label: "resolver",
|
|
2686
|
+
src
|
|
2687
|
+
};
|
|
2688
|
+
const errors = [];
|
|
2689
|
+
let hasName = !isInline;
|
|
2690
|
+
let hasType = !isInline;
|
|
2691
|
+
let hasSources = false;
|
|
2692
|
+
for (const member of node.members) {
|
|
2693
|
+
if (member.name.type !== "String") continue;
|
|
2694
|
+
switch (member.name.value) {
|
|
2695
|
+
case "name":
|
|
2696
|
+
hasName = true;
|
|
2697
|
+
if (member.value.type !== "String") errors.push({
|
|
2698
|
+
...entry,
|
|
2699
|
+
message: MESSAGE_EXPECTED.STRING,
|
|
2700
|
+
node: member.value
|
|
2701
|
+
});
|
|
2702
|
+
break;
|
|
2703
|
+
case "description":
|
|
2704
|
+
if (member.value.type !== "String") errors.push({
|
|
2705
|
+
...entry,
|
|
2706
|
+
message: MESSAGE_EXPECTED.STRING,
|
|
2707
|
+
node: member.value
|
|
2708
|
+
});
|
|
2709
|
+
break;
|
|
2710
|
+
case "type":
|
|
2711
|
+
hasType = true;
|
|
2712
|
+
if (member.value.type !== "String") errors.push({
|
|
2713
|
+
...entry,
|
|
2714
|
+
message: MESSAGE_EXPECTED.STRING,
|
|
2715
|
+
node: member.value
|
|
2716
|
+
});
|
|
2717
|
+
else if (member.value.value !== "set") errors.push({
|
|
2718
|
+
...entry,
|
|
2719
|
+
message: "\"type\" must be \"set\"."
|
|
2720
|
+
});
|
|
2721
|
+
break;
|
|
2722
|
+
case "sources":
|
|
2723
|
+
hasSources = true;
|
|
2724
|
+
if (member.value.type !== "Array") errors.push({
|
|
2725
|
+
...entry,
|
|
2726
|
+
message: MESSAGE_EXPECTED.ARRAY,
|
|
2727
|
+
node: member.value
|
|
2728
|
+
});
|
|
2729
|
+
else if (member.value.elements.length === 0) errors.push({
|
|
2730
|
+
...entry,
|
|
2731
|
+
message: `"sources" can’t be empty array.`,
|
|
2732
|
+
node: member.value
|
|
2733
|
+
});
|
|
2734
|
+
break;
|
|
2735
|
+
case "$defs":
|
|
2736
|
+
case "$extensions":
|
|
2737
|
+
if (member.value.type !== "Object") errors.push({
|
|
2738
|
+
...entry,
|
|
2739
|
+
message: `Expected object`,
|
|
2740
|
+
node: member.value
|
|
2741
|
+
});
|
|
2742
|
+
break;
|
|
2743
|
+
case "$ref":
|
|
2744
|
+
if (member.value.type !== "String") errors.push({
|
|
2745
|
+
...entry,
|
|
2746
|
+
message: `Expected string`,
|
|
2747
|
+
node: member.value
|
|
2748
|
+
});
|
|
2749
|
+
break;
|
|
2750
|
+
default:
|
|
2751
|
+
errors.push({
|
|
2752
|
+
...entry,
|
|
2753
|
+
message: `Unknown key ${JSON.stringify(member.name.value)}`,
|
|
2754
|
+
node: member.name
|
|
2755
|
+
});
|
|
2756
|
+
break;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
if (!hasName) errors.push({
|
|
2760
|
+
...entry,
|
|
2761
|
+
message: `Missing "name".`,
|
|
2762
|
+
node
|
|
2763
|
+
});
|
|
2764
|
+
if (!hasType) errors.push({
|
|
2765
|
+
...entry,
|
|
2766
|
+
message: `"type": "set" missing.`,
|
|
2767
|
+
node
|
|
2768
|
+
});
|
|
2769
|
+
if (!hasSources) errors.push({
|
|
2770
|
+
...entry,
|
|
2771
|
+
message: `Missing "sources".`,
|
|
2772
|
+
node
|
|
2773
|
+
});
|
|
2774
|
+
return errors;
|
|
2775
|
+
}
|
|
2776
|
+
function validateModifier(node, isInline = false, { src }) {
|
|
2777
|
+
const errors = [];
|
|
2778
|
+
const entry = {
|
|
2779
|
+
group: "parser",
|
|
2780
|
+
label: "resolver",
|
|
2781
|
+
src
|
|
2782
|
+
};
|
|
2783
|
+
let hasName = !isInline;
|
|
2784
|
+
let hasType = !isInline;
|
|
2785
|
+
let hasContexts = false;
|
|
2786
|
+
for (const member of node.members) {
|
|
2787
|
+
if (member.name.type !== "String") continue;
|
|
2788
|
+
switch (member.name.value) {
|
|
2789
|
+
case "name":
|
|
2790
|
+
hasName = true;
|
|
2791
|
+
if (member.value.type !== "String") errors.push({
|
|
2792
|
+
...entry,
|
|
2793
|
+
message: MESSAGE_EXPECTED.STRING,
|
|
2794
|
+
node: member.value
|
|
2795
|
+
});
|
|
2796
|
+
break;
|
|
2797
|
+
case "description":
|
|
2798
|
+
if (member.value.type !== "String") errors.push({
|
|
2799
|
+
...entry,
|
|
2800
|
+
message: MESSAGE_EXPECTED.STRING,
|
|
2801
|
+
node: member.value
|
|
2802
|
+
});
|
|
2803
|
+
break;
|
|
2804
|
+
case "type":
|
|
2805
|
+
hasType = true;
|
|
2806
|
+
if (member.value.type !== "String") errors.push({
|
|
2807
|
+
...entry,
|
|
2808
|
+
message: MESSAGE_EXPECTED.STRING,
|
|
2809
|
+
node: member.value
|
|
2810
|
+
});
|
|
2811
|
+
else if (member.value.value !== "modifier") errors.push({
|
|
2812
|
+
...entry,
|
|
2813
|
+
message: "\"type\" must be \"modifier\"."
|
|
2814
|
+
});
|
|
2815
|
+
break;
|
|
2816
|
+
case "contexts":
|
|
2817
|
+
hasContexts = true;
|
|
2818
|
+
if (member.value.type !== "Object") errors.push({
|
|
2819
|
+
...entry,
|
|
2820
|
+
message: MESSAGE_EXPECTED.OBJECT,
|
|
2821
|
+
node: member.value
|
|
2822
|
+
});
|
|
2823
|
+
else if (member.value.members.length === 0) errors.push({
|
|
2824
|
+
...entry,
|
|
2825
|
+
message: `"contexts" can’t be empty object.`,
|
|
2826
|
+
node: member.value
|
|
2827
|
+
});
|
|
2828
|
+
else for (const context of member.value.members) if (context.value.type !== "Array") errors.push({
|
|
2829
|
+
...entry,
|
|
2830
|
+
message: MESSAGE_EXPECTED.ARRAY,
|
|
2831
|
+
node: context.value
|
|
2832
|
+
});
|
|
2833
|
+
break;
|
|
2834
|
+
case "$defs":
|
|
2835
|
+
case "$extensions":
|
|
2836
|
+
if (member.value.type !== "Object") errors.push({
|
|
2837
|
+
...entry,
|
|
2838
|
+
message: `Expected object`,
|
|
2839
|
+
node: member.value
|
|
2840
|
+
});
|
|
2841
|
+
break;
|
|
2842
|
+
case "$ref":
|
|
2843
|
+
if (member.value.type !== "String") errors.push({
|
|
2844
|
+
...entry,
|
|
2845
|
+
message: `Expected string`,
|
|
2846
|
+
node: member.value
|
|
2847
|
+
});
|
|
2848
|
+
break;
|
|
2849
|
+
default:
|
|
2850
|
+
errors.push({
|
|
2851
|
+
...entry,
|
|
2852
|
+
message: `Unknown key ${JSON.stringify(member.name.value)}`,
|
|
2853
|
+
node: member.name
|
|
2854
|
+
});
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
if (!hasName) errors.push({
|
|
2859
|
+
...entry,
|
|
2860
|
+
message: `Missing "name".`,
|
|
2861
|
+
node
|
|
2862
|
+
});
|
|
2863
|
+
if (!hasType) errors.push({
|
|
2864
|
+
...entry,
|
|
2865
|
+
message: `"type": "modifier" missing.`,
|
|
2866
|
+
node
|
|
2867
|
+
});
|
|
2868
|
+
if (!hasContexts) errors.push({
|
|
2869
|
+
...entry,
|
|
2870
|
+
message: `Missing "contexts".`,
|
|
2871
|
+
node
|
|
2872
|
+
});
|
|
2873
|
+
return errors;
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2483
2876
|
//#endregion
|
|
2484
2877
|
//#region src/parse/normalize.ts
|
|
2485
2878
|
/**
|
|
@@ -2603,11 +2996,10 @@ function tokenFromNode(node, { groups, path, source, ignore }) {
|
|
|
2603
2996
|
const jsonID = `#/${path.join("/")}`;
|
|
2604
2997
|
const id = path.join(".");
|
|
2605
2998
|
const originalToken = momoa.evaluate(node);
|
|
2606
|
-
const
|
|
2607
|
-
const group = groups[groupID];
|
|
2999
|
+
const group = groups[`#/${path.slice(0, -1).join("/")}`];
|
|
2608
3000
|
if (group?.tokens && !group.tokens.includes(id)) group.tokens.push(id);
|
|
2609
3001
|
const nodeSource = {
|
|
2610
|
-
filename: source.filename
|
|
3002
|
+
filename: source.filename.href,
|
|
2611
3003
|
node
|
|
2612
3004
|
};
|
|
2613
3005
|
const token = {
|
|
@@ -2873,6 +3265,13 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
|
|
|
2873
3265
|
for (const mode of Object.keys(token.mode)) {
|
|
2874
3266
|
function resolveInner(alias, refChain) {
|
|
2875
3267
|
const nextRef = aliasToRef(alias, mode)?.$ref;
|
|
3268
|
+
if (!nextRef) {
|
|
3269
|
+
logger.error({
|
|
3270
|
+
...aliasEntry,
|
|
3271
|
+
message: `Internal error resolving ${JSON.stringify(refChain)}`
|
|
3272
|
+
});
|
|
3273
|
+
throw new Error("Internal error");
|
|
3274
|
+
}
|
|
2876
3275
|
if (refChain.includes(nextRef)) logger.error({
|
|
2877
3276
|
...aliasEntry,
|
|
2878
3277
|
message: "Circular alias detected."
|
|
@@ -2945,67 +3344,20 @@ function resolveAliases(tokens, { logger, refMap, sources }) {
|
|
|
2945
3344
|
}
|
|
2946
3345
|
|
|
2947
3346
|
//#endregion
|
|
2948
|
-
//#region src/parse/
|
|
2949
|
-
|
|
2950
|
-
async function loadSources(inputs, { config, logger, continueOnError, yamlToMomoa, transform }) {
|
|
3347
|
+
//#region src/parse/process.ts
|
|
3348
|
+
function processTokens(rootSource, { config, logger, sourceByFilename, refMap }) {
|
|
2951
3349
|
const entry = {
|
|
2952
3350
|
group: "parser",
|
|
2953
3351
|
label: "init"
|
|
2954
3352
|
};
|
|
2955
|
-
const firstLoad = performance.now();
|
|
2956
|
-
let document = {};
|
|
2957
|
-
/** The original user inputs, in original order, with parsed ASTs */
|
|
2958
|
-
const sources = inputs.map((input, i) => ({
|
|
2959
|
-
...input,
|
|
2960
|
-
document: {},
|
|
2961
|
-
filename: input.filename || new URL(`virtual:${i}`)
|
|
2962
|
-
}));
|
|
2963
|
-
/** The sources array, indexed by filename */
|
|
2964
|
-
let sourceByFilename = {};
|
|
2965
|
-
/** Mapping of all final $ref resolutions. This will be used to generate the graph later. */
|
|
2966
|
-
let refMap = {};
|
|
2967
|
-
try {
|
|
2968
|
-
const result = await bundle(sources, {
|
|
2969
|
-
parse: transform ? transformer(transform) : void 0,
|
|
2970
|
-
yamlToMomoa
|
|
2971
|
-
});
|
|
2972
|
-
document = result.document;
|
|
2973
|
-
sourceByFilename = result.sources;
|
|
2974
|
-
refMap = result.refMap;
|
|
2975
|
-
for (const [filename, source] of Object.entries(result.sources)) {
|
|
2976
|
-
const i = sources.findIndex((s) => s.filename.href === filename);
|
|
2977
|
-
if (i === -1) sources.push(source);
|
|
2978
|
-
else {
|
|
2979
|
-
sources[i].src = source.src;
|
|
2980
|
-
sources[i].document = source.document;
|
|
2981
|
-
}
|
|
2982
|
-
}
|
|
2983
|
-
} catch (err) {
|
|
2984
|
-
let src = sources.find((s) => s.filename.href === err.filename)?.src;
|
|
2985
|
-
if (src && typeof src !== "string") src = JSON.stringify(src, void 0, 2);
|
|
2986
|
-
logger.error({
|
|
2987
|
-
...entry,
|
|
2988
|
-
continueOnError,
|
|
2989
|
-
message: err.message,
|
|
2990
|
-
node: err.node,
|
|
2991
|
-
src
|
|
2992
|
-
});
|
|
2993
|
-
}
|
|
2994
|
-
logger.debug({
|
|
2995
|
-
...entry,
|
|
2996
|
-
message: `JSON loaded`,
|
|
2997
|
-
timing: performance.now() - firstLoad
|
|
2998
|
-
});
|
|
2999
|
-
const artificialSource = {
|
|
3000
|
-
src: momoa.print(document, { indent: 2 }),
|
|
3001
|
-
document
|
|
3002
|
-
};
|
|
3003
3353
|
const firstPass = performance.now();
|
|
3004
3354
|
const tokens = {};
|
|
3005
3355
|
const tokenIDs = [];
|
|
3006
3356
|
const groups = {};
|
|
3007
|
-
|
|
3357
|
+
const isResolver = isLikelyResolver(rootSource.document);
|
|
3358
|
+
traverse(rootSource.document, { enter(node, _parent, rawPath) {
|
|
3008
3359
|
if (node.type !== "Object") return;
|
|
3360
|
+
const path = isResolver ? filterResolverPaths(rawPath) : rawPath;
|
|
3009
3361
|
groupFromNode(node, {
|
|
3010
3362
|
path,
|
|
3011
3363
|
groups
|
|
@@ -3014,10 +3366,7 @@ async function loadSources(inputs, { config, logger, continueOnError, yamlToMomo
|
|
|
3014
3366
|
groups,
|
|
3015
3367
|
ignore: config.ignore,
|
|
3016
3368
|
path,
|
|
3017
|
-
source:
|
|
3018
|
-
src: artificialSource,
|
|
3019
|
-
document
|
|
3020
|
-
}
|
|
3369
|
+
source: rootSource
|
|
3021
3370
|
});
|
|
3022
3371
|
if (token) {
|
|
3023
3372
|
tokenIDs.push(token.jsonID);
|
|
@@ -3030,7 +3379,7 @@ async function loadSources(inputs, { config, logger, continueOnError, yamlToMomo
|
|
|
3030
3379
|
timing: performance.now() - firstPass
|
|
3031
3380
|
});
|
|
3032
3381
|
const secondPass = performance.now();
|
|
3033
|
-
for (const source of Object.values(sourceByFilename))
|
|
3382
|
+
for (const source of Object.values(sourceByFilename)) traverse(source.document, { enter(node, _parent, path) {
|
|
3034
3383
|
if (node.type !== "Object") return;
|
|
3035
3384
|
const tokenRawValues = tokenRawValuesFromNode(node, {
|
|
3036
3385
|
filename: source.filename.href,
|
|
@@ -3087,8 +3436,377 @@ async function loadSources(inputs, { config, logger, continueOnError, yamlToMomo
|
|
|
3087
3436
|
tokensSorted[id] = tokens[path];
|
|
3088
3437
|
}
|
|
3089
3438
|
for (const group of Object.values(groups)) group.tokens.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
|
|
3439
|
+
return tokensSorted;
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
//#endregion
|
|
3443
|
+
//#region src/resolver/normalize.ts
|
|
3444
|
+
/** Normalize resolver (assuming it’s been validated) */
|
|
3445
|
+
async function normalizeResolver(node, { filename, req, src, logger, yamlToMomoa }) {
|
|
3446
|
+
const resolverSource = momoa.evaluate(node);
|
|
3447
|
+
const resolutionOrder = getObjMember(node.body, "resolutionOrder");
|
|
3448
|
+
return {
|
|
3449
|
+
name: resolverSource.name,
|
|
3450
|
+
version: resolverSource.version,
|
|
3451
|
+
description: resolverSource.description,
|
|
3452
|
+
sets: resolverSource.sets,
|
|
3453
|
+
modifiers: resolverSource.modifiers,
|
|
3454
|
+
resolutionOrder: await Promise.all(resolutionOrder.elements.map(async (element, i) => {
|
|
3455
|
+
const layer = element.value;
|
|
3456
|
+
const members = getObjMembers(layer);
|
|
3457
|
+
let item = layer;
|
|
3458
|
+
if (members.$ref) {
|
|
3459
|
+
const entry = {
|
|
3460
|
+
group: "parser",
|
|
3461
|
+
label: "init",
|
|
3462
|
+
node: members.$ref,
|
|
3463
|
+
src
|
|
3464
|
+
};
|
|
3465
|
+
const { url, subpath } = parseRef(members.$ref.value);
|
|
3466
|
+
if (url === ".") if (!subpath?.[0]) logger.error({
|
|
3467
|
+
...entry,
|
|
3468
|
+
message: "$ref can’t refer to the root document."
|
|
3469
|
+
});
|
|
3470
|
+
else if (subpath[0] !== "sets" && subpath[0] !== "modifiers") logger.error({
|
|
3471
|
+
...entry,
|
|
3472
|
+
message: "Local $ref in resolutionOrder must point to either #/sets/[set] or #/modifiers/[modifiers]."
|
|
3473
|
+
});
|
|
3474
|
+
else {
|
|
3475
|
+
const resolvedItem = resolverSource[subpath[0]]?.[subpath[1]];
|
|
3476
|
+
if (!resolvedItem) logger.error({
|
|
3477
|
+
...entry,
|
|
3478
|
+
message: "Invalid $ref"
|
|
3479
|
+
});
|
|
3480
|
+
else item = {
|
|
3481
|
+
type: subpath[0] === "sets" ? "set" : "modifier",
|
|
3482
|
+
name: subpath[1],
|
|
3483
|
+
...resolvedItem
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
else {
|
|
3487
|
+
const result = await bundle([{
|
|
3488
|
+
filename: new URL(url, filename),
|
|
3489
|
+
src: resolverSource.resolutionOrder[i]
|
|
3490
|
+
}], {
|
|
3491
|
+
req,
|
|
3492
|
+
yamlToMomoa
|
|
3493
|
+
});
|
|
3494
|
+
if (result.document.body.type === "Object") {
|
|
3495
|
+
const type = getObjMember(result.document.body, "type");
|
|
3496
|
+
if (type?.type === "String" && type.value === "set") {
|
|
3497
|
+
validateSet(result.document.body, true, src);
|
|
3498
|
+
item = momoa.evaluate(result.document.body);
|
|
3499
|
+
} else if (type?.type === "String" && type.value === "modifier") {
|
|
3500
|
+
validateModifier(result.document.body, true, src);
|
|
3501
|
+
item = momoa.evaluate(result.document.body);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
logger.error({
|
|
3505
|
+
...entry,
|
|
3506
|
+
message: "$ref did not resolve to a valid Set or Modifier."
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
const finalResult = await bundle([{
|
|
3511
|
+
filename,
|
|
3512
|
+
src: item
|
|
3513
|
+
}], {
|
|
3514
|
+
req,
|
|
3515
|
+
yamlToMomoa
|
|
3516
|
+
});
|
|
3517
|
+
return momoa.evaluate(finalResult.document.body);
|
|
3518
|
+
})),
|
|
3519
|
+
_source: {
|
|
3520
|
+
filename,
|
|
3521
|
+
node
|
|
3522
|
+
}
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
//#endregion
|
|
3527
|
+
//#region src/resolver/load.ts
|
|
3528
|
+
/** Quick-parse input sources and find a resolver */
|
|
3529
|
+
async function loadResolver(inputs, { config, logger, req, yamlToMomoa }) {
|
|
3530
|
+
let resolverDoc;
|
|
3531
|
+
let tokens = {};
|
|
3532
|
+
const entry = {
|
|
3533
|
+
group: "parser",
|
|
3534
|
+
label: "init"
|
|
3535
|
+
};
|
|
3536
|
+
for (const input of inputs) {
|
|
3537
|
+
let document;
|
|
3538
|
+
if (typeof input.src === "string") if (maybeRawJSON(input.src)) document = toMomoa(input.src);
|
|
3539
|
+
else if (yamlToMomoa) document = yamlToMomoa(input.src);
|
|
3540
|
+
else logger.error({
|
|
3541
|
+
...entry,
|
|
3542
|
+
message: `Install yaml-to-momoa package to parse YAML, and pass in as option, e.g.:
|
|
3543
|
+
|
|
3544
|
+
import { bundle } from '@terrazzo/json-schema-tools';
|
|
3545
|
+
import yamlToMomoa from 'yaml-to-momoa';
|
|
3546
|
+
|
|
3547
|
+
bundle(yamlString, { yamlToMomoa });`
|
|
3548
|
+
});
|
|
3549
|
+
else if (input.src && typeof input.src === "object") document = toMomoa(JSON.stringify(input.src, void 0, 2));
|
|
3550
|
+
else logger.error({
|
|
3551
|
+
...entry,
|
|
3552
|
+
message: `Could not parse ${input.filename}. Is this valid JSON or YAML?`
|
|
3553
|
+
});
|
|
3554
|
+
if (!document || !isLikelyResolver(document)) continue;
|
|
3555
|
+
if (inputs.length > 1) logger.error({
|
|
3556
|
+
...entry,
|
|
3557
|
+
message: `Resolver must be the only input, found ${inputs.length} sources.`
|
|
3558
|
+
});
|
|
3559
|
+
resolverDoc = document;
|
|
3560
|
+
break;
|
|
3561
|
+
}
|
|
3562
|
+
let resolver;
|
|
3563
|
+
if (resolverDoc) {
|
|
3564
|
+
validateResolver(resolverDoc, {
|
|
3565
|
+
logger,
|
|
3566
|
+
src: inputs[0].src
|
|
3567
|
+
});
|
|
3568
|
+
resolver = createResolver(await normalizeResolver(resolverDoc, {
|
|
3569
|
+
filename: inputs[0].filename,
|
|
3570
|
+
logger,
|
|
3571
|
+
req,
|
|
3572
|
+
src: inputs[0].src,
|
|
3573
|
+
yamlToMomoa
|
|
3574
|
+
}), {
|
|
3575
|
+
config,
|
|
3576
|
+
logger,
|
|
3577
|
+
sources: [{
|
|
3578
|
+
...inputs[0],
|
|
3579
|
+
document: resolverDoc
|
|
3580
|
+
}]
|
|
3581
|
+
});
|
|
3582
|
+
const firstInput = {};
|
|
3583
|
+
for (const m of resolver.source.resolutionOrder) {
|
|
3584
|
+
if (m.type !== "modifier") continue;
|
|
3585
|
+
firstInput[m.name] = typeof m.default === "string" ? m.default : Object.keys(m.contexts)[0];
|
|
3586
|
+
}
|
|
3587
|
+
tokens = resolver.apply(firstInput);
|
|
3588
|
+
}
|
|
3589
|
+
return {
|
|
3590
|
+
resolver,
|
|
3591
|
+
tokens,
|
|
3592
|
+
sources: [{
|
|
3593
|
+
...inputs[0],
|
|
3594
|
+
document: resolverDoc
|
|
3595
|
+
}]
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
/** Create an interface to resolve permutations */
|
|
3599
|
+
function createResolver(resolverSource, { config, logger, sources }) {
|
|
3600
|
+
const inputDefaults = {};
|
|
3601
|
+
const validContexts = {};
|
|
3602
|
+
const allPermutations = [];
|
|
3603
|
+
const resolverCache = {};
|
|
3604
|
+
for (const m of resolverSource.resolutionOrder) if (m.type === "modifier") {
|
|
3605
|
+
if (typeof m.default === "string") inputDefaults[m.name] = m.default;
|
|
3606
|
+
validContexts[m.name] = Object.keys(m.contexts);
|
|
3607
|
+
}
|
|
3090
3608
|
return {
|
|
3091
|
-
|
|
3609
|
+
apply(inputRaw) {
|
|
3610
|
+
let tokensRaw = {};
|
|
3611
|
+
const input = {
|
|
3612
|
+
...inputDefaults,
|
|
3613
|
+
...inputRaw
|
|
3614
|
+
};
|
|
3615
|
+
const inputKey = makeInputKey(input);
|
|
3616
|
+
if (resolverCache[inputKey]) return resolverCache[inputKey];
|
|
3617
|
+
for (const item of resolverSource.resolutionOrder) switch (item.type) {
|
|
3618
|
+
case "set":
|
|
3619
|
+
for (const s of item.sources) tokensRaw = merge(tokensRaw, s);
|
|
3620
|
+
break;
|
|
3621
|
+
case "modifier": {
|
|
3622
|
+
const context = input[item.name];
|
|
3623
|
+
const sources$1 = item.contexts[context];
|
|
3624
|
+
if (!sources$1) logger.error({
|
|
3625
|
+
group: "parser",
|
|
3626
|
+
label: "resolver",
|
|
3627
|
+
message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`
|
|
3628
|
+
});
|
|
3629
|
+
for (const s of sources$1 ?? []) tokensRaw = merge(tokensRaw, s);
|
|
3630
|
+
break;
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
const src = JSON.stringify(tokensRaw, void 0, 2);
|
|
3634
|
+
const tokens = processTokens({
|
|
3635
|
+
filename: resolverSource._source.filename,
|
|
3636
|
+
document: toMomoa(src),
|
|
3637
|
+
src
|
|
3638
|
+
}, {
|
|
3639
|
+
config,
|
|
3640
|
+
logger,
|
|
3641
|
+
sourceByFilename: {},
|
|
3642
|
+
refMap: {},
|
|
3643
|
+
sources
|
|
3644
|
+
});
|
|
3645
|
+
resolverCache[inputKey] = tokens;
|
|
3646
|
+
return tokens;
|
|
3647
|
+
},
|
|
3648
|
+
source: resolverSource,
|
|
3649
|
+
listPermutations() {
|
|
3650
|
+
if (!allPermutations.length) allPermutations.push(...calculatePermutations(Object.entries(validContexts)));
|
|
3651
|
+
return allPermutations;
|
|
3652
|
+
},
|
|
3653
|
+
isValidInput(input) {
|
|
3654
|
+
if (!input || typeof input !== "object") logger.error({
|
|
3655
|
+
group: "parser",
|
|
3656
|
+
label: "resolver",
|
|
3657
|
+
message: `Invalid input: ${JSON.stringify(input)}.`
|
|
3658
|
+
});
|
|
3659
|
+
if (!Object.keys(input).every((k) => k in validContexts)) return false;
|
|
3660
|
+
for (const [name, contexts] of Object.entries(validContexts)) if (name in input) {
|
|
3661
|
+
if (!contexts.includes(input[name])) return false;
|
|
3662
|
+
} else if (!(name in inputDefaults)) return false;
|
|
3663
|
+
return true;
|
|
3664
|
+
}
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
/** Calculate all permutations */
|
|
3668
|
+
function calculatePermutations(options) {
|
|
3669
|
+
const permutationCount = [1];
|
|
3670
|
+
for (const [_name, contexts] of options) permutationCount.push(contexts.length * (permutationCount.at(-1) || 1));
|
|
3671
|
+
const permutations = [];
|
|
3672
|
+
for (let i = 0; i < permutationCount.at(-1); i++) {
|
|
3673
|
+
const input = {};
|
|
3674
|
+
for (let j = 0; j < options.length; j++) {
|
|
3675
|
+
const [name, contexts] = options[j];
|
|
3676
|
+
input[name] = contexts[Math.floor(i / permutationCount[j]) % contexts.length];
|
|
3677
|
+
}
|
|
3678
|
+
permutations.push(input);
|
|
3679
|
+
}
|
|
3680
|
+
return permutations.length ? permutations : [{}];
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
//#endregion
|
|
3684
|
+
//#region src/resolver/create-synthetic-resolver.ts
|
|
3685
|
+
/**
|
|
3686
|
+
* Interop layer upgrading legacy Terrazzo modes to resolvers
|
|
3687
|
+
*/
|
|
3688
|
+
async function createSyntheticResolver(tokens, { config, logger, req, sources }) {
|
|
3689
|
+
const contexts = {};
|
|
3690
|
+
for (const token of Object.values(tokens)) for (const [mode, value] of Object.entries(token.mode)) {
|
|
3691
|
+
if (mode === ".") continue;
|
|
3692
|
+
if (!(mode in contexts)) contexts[mode] = [{}];
|
|
3693
|
+
addToken(contexts[mode][0], {
|
|
3694
|
+
...token,
|
|
3695
|
+
$value: value.$value
|
|
3696
|
+
}, { logger });
|
|
3697
|
+
}
|
|
3698
|
+
const src = JSON.stringify({
|
|
3699
|
+
name: "Terrazzo",
|
|
3700
|
+
version: "2025.10",
|
|
3701
|
+
resolutionOrder: [{ $ref: "#/sets/allTokens" }, { $ref: "#/modifiers/tzMode" }],
|
|
3702
|
+
sets: { allTokens: { sources: [simpleFlatten(tokens, { logger })] } },
|
|
3703
|
+
modifiers: { tzMode: {
|
|
3704
|
+
description: "Automatically built from $extensions.mode",
|
|
3705
|
+
contexts
|
|
3706
|
+
} }
|
|
3707
|
+
}, void 0, 2);
|
|
3708
|
+
return createResolver(await normalizeResolver(momoa.parse(src), {
|
|
3709
|
+
filename: new URL("file:///virtual:resolver.json"),
|
|
3710
|
+
logger,
|
|
3711
|
+
req,
|
|
3712
|
+
src
|
|
3713
|
+
}), {
|
|
3714
|
+
config,
|
|
3715
|
+
logger,
|
|
3716
|
+
sources
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3719
|
+
/** Add a normalized token back into an arbitrary, hierarchial structure */
|
|
3720
|
+
function addToken(structure, token, { logger }) {
|
|
3721
|
+
let node = structure;
|
|
3722
|
+
const parts = token.id.split(".");
|
|
3723
|
+
const localID = parts.pop();
|
|
3724
|
+
for (const part of parts) {
|
|
3725
|
+
if (!(part in node)) node[part] = {};
|
|
3726
|
+
node = node[part];
|
|
3727
|
+
}
|
|
3728
|
+
if (localID in node) logger.error({
|
|
3729
|
+
group: "parser",
|
|
3730
|
+
label: "resolver",
|
|
3731
|
+
message: `${localID} already exists!`
|
|
3732
|
+
});
|
|
3733
|
+
node[localID] = {
|
|
3734
|
+
$type: token.$type,
|
|
3735
|
+
$value: token.$value
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
/** Downconvert normalized tokens back into a simplified, hierarchial shape. This is extremely lossy, and only done to build a resolver. */
|
|
3739
|
+
function simpleFlatten(tokens, { logger }) {
|
|
3740
|
+
const group = {};
|
|
3741
|
+
for (const token of Object.values(tokens)) addToken(group, token, { logger });
|
|
3742
|
+
return group;
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
//#endregion
|
|
3746
|
+
//#region src/parse/load.ts
|
|
3747
|
+
/** Load from multiple entries, while resolving remote files */
|
|
3748
|
+
async function loadSources(inputs, { config, logger, req, continueOnError, yamlToMomoa, transform }) {
|
|
3749
|
+
const entry = {
|
|
3750
|
+
group: "parser",
|
|
3751
|
+
label: "init"
|
|
3752
|
+
};
|
|
3753
|
+
const firstLoad = performance.now();
|
|
3754
|
+
let document = {};
|
|
3755
|
+
/** The original user inputs, in original order, with parsed ASTs */
|
|
3756
|
+
const sources = inputs.map((input, i) => ({
|
|
3757
|
+
...input,
|
|
3758
|
+
document: {},
|
|
3759
|
+
filename: input.filename || new URL(`virtual:${i}`)
|
|
3760
|
+
}));
|
|
3761
|
+
/** The sources array, indexed by filename */
|
|
3762
|
+
let sourceByFilename = {};
|
|
3763
|
+
/** Mapping of all final $ref resolutions. This will be used to generate the graph later. */
|
|
3764
|
+
let refMap = {};
|
|
3765
|
+
try {
|
|
3766
|
+
const result = await bundle(sources, {
|
|
3767
|
+
req,
|
|
3768
|
+
parse: transform ? transformer(transform) : void 0,
|
|
3769
|
+
yamlToMomoa
|
|
3770
|
+
});
|
|
3771
|
+
document = result.document;
|
|
3772
|
+
sourceByFilename = result.sources;
|
|
3773
|
+
refMap = result.refMap;
|
|
3774
|
+
for (const [filename, source] of Object.entries(result.sources)) {
|
|
3775
|
+
const i = sources.findIndex((s) => s.filename.href === filename);
|
|
3776
|
+
if (i === -1) sources.push(source);
|
|
3777
|
+
else {
|
|
3778
|
+
sources[i].src = source.src;
|
|
3779
|
+
sources[i].document = source.document;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
} catch (err) {
|
|
3783
|
+
let src = sources.find((s) => s.filename.href === err.filename)?.src;
|
|
3784
|
+
if (src && typeof src !== "string") src = JSON.stringify(src, void 0, 2);
|
|
3785
|
+
logger.error({
|
|
3786
|
+
...entry,
|
|
3787
|
+
continueOnError,
|
|
3788
|
+
message: err.message,
|
|
3789
|
+
node: err.node,
|
|
3790
|
+
src
|
|
3791
|
+
});
|
|
3792
|
+
}
|
|
3793
|
+
logger.debug({
|
|
3794
|
+
...entry,
|
|
3795
|
+
message: `JSON loaded`,
|
|
3796
|
+
timing: performance.now() - firstLoad
|
|
3797
|
+
});
|
|
3798
|
+
return {
|
|
3799
|
+
tokens: processTokens({
|
|
3800
|
+
filename: sources[0].filename,
|
|
3801
|
+
document,
|
|
3802
|
+
src: momoa.print(document, { indent: 2 })
|
|
3803
|
+
}, {
|
|
3804
|
+
config,
|
|
3805
|
+
logger,
|
|
3806
|
+
refMap,
|
|
3807
|
+
sources,
|
|
3808
|
+
sourceByFilename
|
|
3809
|
+
}),
|
|
3092
3810
|
sources
|
|
3093
3811
|
};
|
|
3094
3812
|
}
|
|
@@ -3105,7 +3823,9 @@ function transformer(transform) {
|
|
|
3105
3823
|
});
|
|
3106
3824
|
if (result) document = result;
|
|
3107
3825
|
}
|
|
3108
|
-
|
|
3826
|
+
const isResolver = isLikelyResolver(document);
|
|
3827
|
+
traverse(document, { enter(node, parent, rawPath) {
|
|
3828
|
+
const path = isResolver ? filterResolverPaths(rawPath) : rawPath;
|
|
3109
3829
|
if (node.type !== "Object" || !path.length) return;
|
|
3110
3830
|
const ctx = {
|
|
3111
3831
|
filename,
|
|
@@ -3138,17 +3858,35 @@ function transformer(transform) {
|
|
|
3138
3858
|
//#endregion
|
|
3139
3859
|
//#region src/parse/index.ts
|
|
3140
3860
|
/** Parse */
|
|
3141
|
-
async function parse(_input, { logger = new Logger(), skipLint = false, config = {}, continueOnError = false, yamlToMomoa, transform } = {}) {
|
|
3861
|
+
async function parse(_input, { logger = new Logger(), req = defaultReq, skipLint = false, config = {}, continueOnError = false, yamlToMomoa, transform } = {}) {
|
|
3142
3862
|
const inputs = Array.isArray(_input) ? _input : [_input];
|
|
3863
|
+
let tokens = {};
|
|
3864
|
+
let resolver;
|
|
3865
|
+
let sources = [];
|
|
3143
3866
|
const totalStart = performance.now();
|
|
3144
3867
|
const initStart = performance.now();
|
|
3145
|
-
const
|
|
3146
|
-
logger,
|
|
3868
|
+
const resolverResult = await loadResolver(inputs, {
|
|
3147
3869
|
config,
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3870
|
+
logger,
|
|
3871
|
+
req,
|
|
3872
|
+
yamlToMomoa
|
|
3151
3873
|
});
|
|
3874
|
+
if (resolverResult.resolver) {
|
|
3875
|
+
tokens = resolverResult.tokens;
|
|
3876
|
+
sources = resolverResult.sources;
|
|
3877
|
+
resolver = resolverResult.resolver;
|
|
3878
|
+
} else {
|
|
3879
|
+
const tokenResult = await loadSources(inputs, {
|
|
3880
|
+
req,
|
|
3881
|
+
logger,
|
|
3882
|
+
config,
|
|
3883
|
+
continueOnError,
|
|
3884
|
+
yamlToMomoa,
|
|
3885
|
+
transform
|
|
3886
|
+
});
|
|
3887
|
+
tokens = tokenResult.tokens;
|
|
3888
|
+
sources = tokenResult.sources;
|
|
3889
|
+
}
|
|
3152
3890
|
logger.debug({
|
|
3153
3891
|
message: "Loaded tokens",
|
|
3154
3892
|
group: "parser",
|
|
@@ -3185,10 +3923,27 @@ async function parse(_input, { logger = new Logger(), skipLint = false, config =
|
|
|
3185
3923
|
}
|
|
3186
3924
|
return {
|
|
3187
3925
|
tokens,
|
|
3188
|
-
sources
|
|
3926
|
+
sources,
|
|
3927
|
+
resolver: resolver || await createSyntheticResolver(tokens, {
|
|
3928
|
+
config,
|
|
3929
|
+
logger,
|
|
3930
|
+
req,
|
|
3931
|
+
sources
|
|
3932
|
+
})
|
|
3189
3933
|
};
|
|
3190
3934
|
}
|
|
3935
|
+
let fs;
|
|
3936
|
+
/** Fallback req */
|
|
3937
|
+
async function defaultReq(src, _origin) {
|
|
3938
|
+
if (src.protocol === "file:") {
|
|
3939
|
+
if (!fs) fs = await import("node:fs/promises");
|
|
3940
|
+
return await fs.readFile(src, "utf8");
|
|
3941
|
+
}
|
|
3942
|
+
const res = await fetch(src);
|
|
3943
|
+
if (!res.ok) throw new Error(`${src} responded with ${res.status}\n${await res.text()}`);
|
|
3944
|
+
return await res.text();
|
|
3945
|
+
}
|
|
3191
3946
|
|
|
3192
3947
|
//#endregion
|
|
3193
|
-
export { LOG_ORDER, Logger, MULTI_VALUE, RECOMMENDED_CONFIG, SINGLE_VALUE, TokensJSONError, build, defineConfig, formatMessage, lintRunner, mergeConfigs, parse };
|
|
3948
|
+
export { LOG_ORDER, Logger, MULTI_VALUE, RECOMMENDED_CONFIG, SINGLE_VALUE, TokensJSONError, build, calculatePermutations, createResolver, defineConfig, formatMessage, isLikelyResolver, lintRunner, loadResolver, mergeConfigs, normalizeResolver, parse, validateModifier, validateResolver, validateSet };
|
|
3194
3949
|
//# sourceMappingURL=index.js.map
|