@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/src/parse/token.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
-
import { getObjMember, parseRef, type RefMap } from '@terrazzo/json-schema-tools';
|
|
2
|
+
import { getObjMember, type InputSourceWithDocument, parseRef, type RefMap } from '@terrazzo/json-schema-tools';
|
|
3
3
|
import {
|
|
4
4
|
type GroupNormalized,
|
|
5
5
|
isAlias,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '@terrazzo/token-tools';
|
|
10
10
|
import wcmatch from 'wildcard-match';
|
|
11
11
|
import type { default as Logger } from '../logger.js';
|
|
12
|
-
import type { Config,
|
|
12
|
+
import type { Config, ReferenceObject } from '../types.js';
|
|
13
13
|
|
|
14
14
|
/** Convert valid DTCG alias to $ref */
|
|
15
15
|
export function aliasToRef(alias: string, mode?: string): ReferenceObject | undefined {
|
|
@@ -26,7 +26,7 @@ export function aliasToRef(alias: string, mode?: string): ReferenceObject | unde
|
|
|
26
26
|
export interface TokenFromNodeOptions {
|
|
27
27
|
groups: Record<string, GroupNormalized>;
|
|
28
28
|
path: string[];
|
|
29
|
-
source:
|
|
29
|
+
source: InputSourceWithDocument;
|
|
30
30
|
ignore: Config['ignore'];
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -51,7 +51,7 @@ export function tokenFromNode(
|
|
|
51
51
|
group.tokens.push(id);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const nodeSource = { filename: source.filename
|
|
54
|
+
const nodeSource = { filename: source.filename.href, node };
|
|
55
55
|
const token: TokenNormalized = {
|
|
56
56
|
id,
|
|
57
57
|
$type: originalToken.$type || group.$type,
|
|
@@ -211,7 +211,7 @@ export function groupFromNode(
|
|
|
211
211
|
|
|
212
212
|
export interface GraphAliasesOptions {
|
|
213
213
|
tokens: TokenNormalizedSet;
|
|
214
|
-
sources: Record<string,
|
|
214
|
+
sources: Record<string, InputSourceWithDocument>;
|
|
215
215
|
logger: Logger;
|
|
216
216
|
}
|
|
217
217
|
|
|
@@ -425,7 +425,7 @@ const EXPECTED_NESTED_ALIAS: Record<string, Record<string, string[]>> = {
|
|
|
425
425
|
*/
|
|
426
426
|
export function resolveAliases(
|
|
427
427
|
tokens: TokenNormalizedSet,
|
|
428
|
-
{ logger, refMap, sources }: { logger: Logger; refMap: RefMap; sources: Record<string,
|
|
428
|
+
{ logger, refMap, sources }: { logger: Logger; refMap: RefMap; sources: Record<string, InputSourceWithDocument> },
|
|
429
429
|
): void {
|
|
430
430
|
for (const token of Object.values(tokens)) {
|
|
431
431
|
const aliasEntry = {
|
|
@@ -437,7 +437,11 @@ export function resolveAliases(
|
|
|
437
437
|
|
|
438
438
|
for (const mode of Object.keys(token.mode)) {
|
|
439
439
|
function resolveInner(alias: string, refChain: string[]): string {
|
|
440
|
-
const nextRef = aliasToRef(alias, mode)?.$ref
|
|
440
|
+
const nextRef = aliasToRef(alias, mode)?.$ref;
|
|
441
|
+
if (!nextRef) {
|
|
442
|
+
logger.error({ ...aliasEntry, message: `Internal error resolving ${JSON.stringify(refChain)}` });
|
|
443
|
+
throw new Error('Internal error');
|
|
444
|
+
}
|
|
441
445
|
if (refChain.includes(nextRef)) {
|
|
442
446
|
logger.error({ ...aliasEntry, message: 'Circular alias detected.' });
|
|
443
447
|
}
|
|
@@ -464,6 +468,7 @@ export function resolveAliases(
|
|
|
464
468
|
continue;
|
|
465
469
|
}
|
|
466
470
|
value[i] = traverseAndResolve(value[i], {
|
|
471
|
+
// biome-ignore lint/suspicious/noNonNullAssertedOptionalChain: we checked for this earlier
|
|
467
472
|
node: (node as momoa.ArrayNode).elements?.[i]?.value!,
|
|
468
473
|
// special case: cubicBezier
|
|
469
474
|
expectedTypes: expectedTypes?.includes('cubicBezier') ? ['number'] : expectedTypes,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import type { InputSourceWithDocument } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import type Logger from '../logger.js';
|
|
4
|
+
import type { ConfigInit, Group, Resolver, TokenNormalized, TokenNormalizedSet } from '../types.js';
|
|
5
|
+
import { createResolver } from './load.js';
|
|
6
|
+
import { normalizeResolver } from './normalize.js';
|
|
7
|
+
|
|
8
|
+
export interface CreateSyntheticResolverOptions {
|
|
9
|
+
config: ConfigInit;
|
|
10
|
+
logger: Logger;
|
|
11
|
+
req: (url: URL, origin: URL) => Promise<string>;
|
|
12
|
+
sources: InputSourceWithDocument[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Interop layer upgrading legacy Terrazzo modes to resolvers
|
|
17
|
+
*/
|
|
18
|
+
export async function createSyntheticResolver(
|
|
19
|
+
tokens: TokenNormalizedSet,
|
|
20
|
+
{ config, logger, req, sources }: CreateSyntheticResolverOptions,
|
|
21
|
+
): Promise<Resolver> {
|
|
22
|
+
const contexts: Record<string, any[]> = {};
|
|
23
|
+
for (const token of Object.values(tokens)) {
|
|
24
|
+
for (const [mode, value] of Object.entries(token.mode)) {
|
|
25
|
+
if (mode === '.') {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!(mode in contexts)) {
|
|
29
|
+
contexts[mode] = [{}];
|
|
30
|
+
}
|
|
31
|
+
addToken(contexts[mode]![0], { ...token, $value: value.$value }, { logger });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const src = JSON.stringify(
|
|
36
|
+
{
|
|
37
|
+
name: 'Terrazzo',
|
|
38
|
+
version: '2025.10',
|
|
39
|
+
resolutionOrder: [{ $ref: '#/sets/allTokens' }, { $ref: '#/modifiers/tzMode' }],
|
|
40
|
+
sets: {
|
|
41
|
+
allTokens: { sources: [simpleFlatten(tokens, { logger })] },
|
|
42
|
+
},
|
|
43
|
+
modifiers: {
|
|
44
|
+
tzMode: {
|
|
45
|
+
description: 'Automatically built from $extensions.mode',
|
|
46
|
+
contexts,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
undefined,
|
|
51
|
+
2,
|
|
52
|
+
);
|
|
53
|
+
const normalized = await normalizeResolver(momoa.parse(src), {
|
|
54
|
+
filename: new URL('file:///virtual:resolver.json'),
|
|
55
|
+
logger,
|
|
56
|
+
req,
|
|
57
|
+
src,
|
|
58
|
+
});
|
|
59
|
+
return createResolver(normalized, { config, logger, sources });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Add a normalized token back into an arbitrary, hierarchial structure */
|
|
63
|
+
function addToken(structure: any, token: TokenNormalized, { logger }: { logger: Logger }): void {
|
|
64
|
+
let node = structure;
|
|
65
|
+
const parts = token.id.split('.');
|
|
66
|
+
const localID = parts.pop()!;
|
|
67
|
+
for (const part of parts) {
|
|
68
|
+
if (!(part in node)) {
|
|
69
|
+
node[part] = {};
|
|
70
|
+
}
|
|
71
|
+
node = node[part];
|
|
72
|
+
}
|
|
73
|
+
if (localID in node) {
|
|
74
|
+
logger.error({ group: 'parser', label: 'resolver', message: `${localID} already exists!` });
|
|
75
|
+
}
|
|
76
|
+
node[localID] = { $type: token.$type, $value: token.$value };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Downconvert normalized tokens back into a simplified, hierarchial shape. This is extremely lossy, and only done to build a resolver. */
|
|
80
|
+
function simpleFlatten(tokens: TokenNormalizedSet, { logger }: { logger: Logger }): Group {
|
|
81
|
+
const group: Group = {};
|
|
82
|
+
for (const token of Object.values(tokens)) {
|
|
83
|
+
addToken(group, token, { logger });
|
|
84
|
+
}
|
|
85
|
+
return group;
|
|
86
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { type InputSource, type InputSourceWithDocument, maybeRawJSON } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import type { TokenNormalizedSet } from '@terrazzo/token-tools';
|
|
4
|
+
import { merge } from 'merge-anything';
|
|
5
|
+
import type yamlToMomoa from 'yaml-to-momoa';
|
|
6
|
+
import { toMomoa } from '../lib/momoa.js';
|
|
7
|
+
import { makeInputKey } from '../lib/resolver-utils.js';
|
|
8
|
+
import type Logger from '../logger.js';
|
|
9
|
+
import { processTokens } from '../parse/process.js';
|
|
10
|
+
import type { ConfigInit, Resolver, ResolverSourceNormalized } from '../types.js';
|
|
11
|
+
import { normalizeResolver } from './normalize.js';
|
|
12
|
+
import { isLikelyResolver, validateResolver } from './validate.js';
|
|
13
|
+
|
|
14
|
+
export interface LoadResolverOptions {
|
|
15
|
+
config: ConfigInit;
|
|
16
|
+
logger: Logger;
|
|
17
|
+
req: (url: URL, origin: URL) => Promise<string>;
|
|
18
|
+
yamlToMomoa?: typeof yamlToMomoa;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Quick-parse input sources and find a resolver */
|
|
22
|
+
export async function loadResolver(
|
|
23
|
+
inputs: InputSource[],
|
|
24
|
+
{ config, logger, req, yamlToMomoa }: LoadResolverOptions,
|
|
25
|
+
): Promise<{ resolver: Resolver | undefined; tokens: TokenNormalizedSet; sources: InputSourceWithDocument[] }> {
|
|
26
|
+
let resolverDoc: momoa.DocumentNode | undefined;
|
|
27
|
+
let tokens: TokenNormalizedSet = {};
|
|
28
|
+
const entry = {
|
|
29
|
+
group: 'parser',
|
|
30
|
+
label: 'init',
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
for (const input of inputs) {
|
|
34
|
+
let document: momoa.DocumentNode | undefined;
|
|
35
|
+
if (typeof input.src === 'string') {
|
|
36
|
+
if (maybeRawJSON(input.src)) {
|
|
37
|
+
document = toMomoa(input.src);
|
|
38
|
+
} else if (yamlToMomoa) {
|
|
39
|
+
document = yamlToMomoa(input.src);
|
|
40
|
+
} else {
|
|
41
|
+
logger.error({
|
|
42
|
+
...entry,
|
|
43
|
+
message: `Install yaml-to-momoa package to parse YAML, and pass in as option, e.g.:
|
|
44
|
+
|
|
45
|
+
import { bundle } from '@terrazzo/json-schema-tools';
|
|
46
|
+
import yamlToMomoa from 'yaml-to-momoa';
|
|
47
|
+
|
|
48
|
+
bundle(yamlString, { yamlToMomoa });`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} else if (input.src && typeof input.src === 'object') {
|
|
52
|
+
document = toMomoa(JSON.stringify(input.src, undefined, 2));
|
|
53
|
+
} else {
|
|
54
|
+
logger.error({ ...entry, message: `Could not parse ${input.filename}. Is this valid JSON or YAML?` });
|
|
55
|
+
}
|
|
56
|
+
if (!document || !isLikelyResolver(document)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (inputs.length > 1) {
|
|
60
|
+
logger.error({ ...entry, message: `Resolver must be the only input, found ${inputs.length} sources.` });
|
|
61
|
+
}
|
|
62
|
+
resolverDoc = document;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let resolver: Resolver | undefined;
|
|
67
|
+
if (resolverDoc) {
|
|
68
|
+
validateResolver(resolverDoc, { logger, src: inputs[0]!.src });
|
|
69
|
+
const normalized = await normalizeResolver(resolverDoc, {
|
|
70
|
+
filename: inputs[0]!.filename!,
|
|
71
|
+
logger,
|
|
72
|
+
req,
|
|
73
|
+
src: inputs[0]!.src,
|
|
74
|
+
yamlToMomoa,
|
|
75
|
+
});
|
|
76
|
+
resolver = createResolver(normalized, { config, logger, sources: [{ ...inputs[0]!, document: resolverDoc }] });
|
|
77
|
+
|
|
78
|
+
// If a resolver is present, load a single permutation to get a base token set.
|
|
79
|
+
const firstInput: Record<string, string> = {};
|
|
80
|
+
for (const m of resolver.source.resolutionOrder) {
|
|
81
|
+
if (m.type !== 'modifier') {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
firstInput[m.name] = typeof m.default === 'string' ? m.default : Object.keys(m.contexts)[0]!;
|
|
85
|
+
}
|
|
86
|
+
tokens = resolver.apply(firstInput);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
resolver,
|
|
91
|
+
tokens,
|
|
92
|
+
sources: [{ ...inputs[0]!, document: resolverDoc! }],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface CreateResolverOptions {
|
|
97
|
+
config: ConfigInit;
|
|
98
|
+
logger: Logger;
|
|
99
|
+
sources: InputSourceWithDocument[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Create an interface to resolve permutations */
|
|
103
|
+
export function createResolver(
|
|
104
|
+
resolverSource: ResolverSourceNormalized,
|
|
105
|
+
{ config, logger, sources }: CreateResolverOptions,
|
|
106
|
+
): Resolver {
|
|
107
|
+
const inputDefaults: Record<string, string> = {};
|
|
108
|
+
const validContexts: Record<string, string[]> = {};
|
|
109
|
+
const allPermutations: Record<string, string>[] = [];
|
|
110
|
+
|
|
111
|
+
const resolverCache: Record<string, any> = {};
|
|
112
|
+
|
|
113
|
+
for (const m of resolverSource.resolutionOrder) {
|
|
114
|
+
if (m.type === 'modifier') {
|
|
115
|
+
if (typeof m.default === 'string') {
|
|
116
|
+
inputDefaults[m.name] = m.default!;
|
|
117
|
+
}
|
|
118
|
+
validContexts[m.name] = Object.keys(m.contexts);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
apply(inputRaw): TokenNormalizedSet {
|
|
124
|
+
let tokensRaw: TokenNormalizedSet = {};
|
|
125
|
+
const input = { ...inputDefaults, ...inputRaw };
|
|
126
|
+
const inputKey = makeInputKey(input);
|
|
127
|
+
|
|
128
|
+
if (resolverCache[inputKey]) {
|
|
129
|
+
return resolverCache[inputKey];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const item of resolverSource.resolutionOrder) {
|
|
133
|
+
switch (item.type) {
|
|
134
|
+
case 'set': {
|
|
135
|
+
for (const s of item.sources) {
|
|
136
|
+
tokensRaw = merge(tokensRaw, s) as TokenNormalizedSet;
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'modifier': {
|
|
141
|
+
const context = input[item.name]!;
|
|
142
|
+
const sources = item.contexts[context];
|
|
143
|
+
if (!sources) {
|
|
144
|
+
logger.error({
|
|
145
|
+
group: 'parser',
|
|
146
|
+
label: 'resolver',
|
|
147
|
+
message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
for (const s of sources ?? []) {
|
|
151
|
+
tokensRaw = merge(tokensRaw, s) as TokenNormalizedSet;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const src = JSON.stringify(tokensRaw, undefined, 2);
|
|
159
|
+
const rootSource = { filename: resolverSource._source.filename!, document: toMomoa(src), src };
|
|
160
|
+
const tokens = processTokens(rootSource, {
|
|
161
|
+
config,
|
|
162
|
+
logger,
|
|
163
|
+
sourceByFilename: {},
|
|
164
|
+
refMap: {},
|
|
165
|
+
sources,
|
|
166
|
+
});
|
|
167
|
+
resolverCache[inputKey] = tokens;
|
|
168
|
+
return tokens;
|
|
169
|
+
},
|
|
170
|
+
source: resolverSource,
|
|
171
|
+
listPermutations() {
|
|
172
|
+
// only do work on first call, then cache subsequent work. this could be thousands of possible values!
|
|
173
|
+
if (!allPermutations.length) {
|
|
174
|
+
allPermutations.push(...calculatePermutations(Object.entries(validContexts)));
|
|
175
|
+
}
|
|
176
|
+
return allPermutations;
|
|
177
|
+
},
|
|
178
|
+
isValidInput(input: Record<string, string>) {
|
|
179
|
+
if (!input || typeof input !== 'object') {
|
|
180
|
+
logger.error({ group: 'parser', label: 'resolver', message: `Invalid input: ${JSON.stringify(input)}.` });
|
|
181
|
+
}
|
|
182
|
+
if (!Object.keys(input).every((k) => k in validContexts)) {
|
|
183
|
+
return false; // 1. invalid if unknown modifier name
|
|
184
|
+
}
|
|
185
|
+
for (const [name, contexts] of Object.entries(validContexts)) {
|
|
186
|
+
// Note: empty strings are valid! Don’t check for truthiness.
|
|
187
|
+
if (name in input) {
|
|
188
|
+
if (!contexts.includes(input[name]!)) {
|
|
189
|
+
return false; // 2. invalid if unknown context
|
|
190
|
+
}
|
|
191
|
+
} else if (!(name in inputDefaults)) {
|
|
192
|
+
return false; // 3. invalid if omitted, and no default
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Calculate all permutations */
|
|
201
|
+
export function calculatePermutations(options: [string, string[]][]) {
|
|
202
|
+
const permutationCount = [1];
|
|
203
|
+
for (const [_name, contexts] of options) {
|
|
204
|
+
permutationCount.push(contexts.length * (permutationCount.at(-1) || 1));
|
|
205
|
+
}
|
|
206
|
+
const permutations: Record<string, string>[] = [];
|
|
207
|
+
for (let i = 0; i < permutationCount.at(-1)!; i++) {
|
|
208
|
+
const input: Record<string, string> = {};
|
|
209
|
+
for (let j = 0; j < options.length; j++) {
|
|
210
|
+
const [name, contexts] = options[j]!;
|
|
211
|
+
input[name] = contexts[Math.floor(i / permutationCount[j]!) % contexts.length]!;
|
|
212
|
+
}
|
|
213
|
+
permutations.push(input);
|
|
214
|
+
}
|
|
215
|
+
return permutations.length ? permutations : [{}];
|
|
216
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { bundle, getObjMember, getObjMembers, parseRef } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import type yamlToMomoa from 'yaml-to-momoa';
|
|
4
|
+
import type Logger from '../logger.js';
|
|
5
|
+
import type {
|
|
6
|
+
ResolverModifierInline,
|
|
7
|
+
ResolverModifierNormalized,
|
|
8
|
+
ResolverSetInline,
|
|
9
|
+
ResolverSetNormalized,
|
|
10
|
+
ResolverSourceNormalized,
|
|
11
|
+
} from '../types.js';
|
|
12
|
+
import { validateModifier, validateSet } from './validate.js';
|
|
13
|
+
|
|
14
|
+
export interface NormalizeResolverOptions {
|
|
15
|
+
logger: Logger;
|
|
16
|
+
yamlToMomoa?: typeof yamlToMomoa;
|
|
17
|
+
filename: URL;
|
|
18
|
+
req: (url: URL, origin: URL) => Promise<string>;
|
|
19
|
+
src?: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Normalize resolver (assuming it’s been validated) */
|
|
23
|
+
export async function normalizeResolver(
|
|
24
|
+
node: momoa.DocumentNode,
|
|
25
|
+
{ filename, req, src, logger, yamlToMomoa }: NormalizeResolverOptions,
|
|
26
|
+
): Promise<ResolverSourceNormalized> {
|
|
27
|
+
const resolverSource = momoa.evaluate(node) as unknown as ResolverSourceNormalized;
|
|
28
|
+
const resolutionOrder = getObjMember(node.body as momoa.ObjectNode, 'resolutionOrder') as momoa.ArrayNode;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: resolverSource.name,
|
|
32
|
+
version: resolverSource.version,
|
|
33
|
+
description: resolverSource.description,
|
|
34
|
+
sets: resolverSource.sets,
|
|
35
|
+
modifiers: resolverSource.modifiers,
|
|
36
|
+
resolutionOrder: await Promise.all(
|
|
37
|
+
resolutionOrder.elements.map(async (element, i) => {
|
|
38
|
+
const layer = element.value as momoa.ObjectNode;
|
|
39
|
+
const members = getObjMembers(layer);
|
|
40
|
+
|
|
41
|
+
// If this is an inline set or modifier it’s already been validated; we only need
|
|
42
|
+
// to resolve & validate $refs here which haven’t yet been parsed
|
|
43
|
+
let item = layer as unknown as ResolverSetInline | ResolverModifierInline;
|
|
44
|
+
|
|
45
|
+
// 1. $ref
|
|
46
|
+
if (members.$ref) {
|
|
47
|
+
const entry = { group: 'parser', label: 'init', node: members.$ref, src } as const;
|
|
48
|
+
const { url, subpath } = parseRef((members.$ref as unknown as momoa.StringNode).value);
|
|
49
|
+
if (url === '.') {
|
|
50
|
+
// 1a. local $ref: pull from local document
|
|
51
|
+
if (!subpath?.[0]) {
|
|
52
|
+
logger.error({ ...entry, message: '$ref can’t refer to the root document.' });
|
|
53
|
+
} else if (subpath[0] !== 'sets' && subpath[0] !== 'modifiers') {
|
|
54
|
+
// Note: technically we could allow $defs, but that’s just unnecessary shenanigans here.
|
|
55
|
+
logger.error({
|
|
56
|
+
...entry,
|
|
57
|
+
message: 'Local $ref in resolutionOrder must point to either #/sets/[set] or #/modifiers/[modifiers].',
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
const resolvedItem = resolverSource[subpath[0] as 'sets' | 'modifiers']?.[subpath[1]!];
|
|
61
|
+
if (!resolvedItem) {
|
|
62
|
+
logger.error({ ...entry, message: 'Invalid $ref' });
|
|
63
|
+
} else {
|
|
64
|
+
item = {
|
|
65
|
+
type: subpath[0] === 'sets' ? 'set' : 'modifier',
|
|
66
|
+
name: subpath[1],
|
|
67
|
+
...(resolvedItem as any), // Note: as long as this exists, this has already been validated to be correct
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
// 1b. remote $ref: load and validate
|
|
73
|
+
const result = await bundle(
|
|
74
|
+
[{ filename: new URL(url, filename), src: resolverSource.resolutionOrder[i]! }],
|
|
75
|
+
{
|
|
76
|
+
req,
|
|
77
|
+
yamlToMomoa,
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
if (result.document.body.type === 'Object') {
|
|
81
|
+
const type = getObjMember(result.document.body, 'type');
|
|
82
|
+
if (type?.type === 'String' && type.value === 'set') {
|
|
83
|
+
validateSet(result.document.body as momoa.ObjectNode, true, src);
|
|
84
|
+
item = momoa.evaluate(result.document.body) as unknown as ResolverSetInline;
|
|
85
|
+
} else if (type?.type === 'String' && type.value === 'modifier') {
|
|
86
|
+
validateModifier(result.document.body as momoa.ObjectNode, true, src);
|
|
87
|
+
item = momoa.evaluate(result.document.body) as unknown as ResolverModifierInline;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
logger.error({ ...entry, message: '$ref did not resolve to a valid Set or Modifier.' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. resolve inline sources & contexts
|
|
95
|
+
const finalResult = await bundle([{ filename, src: item }], { req, yamlToMomoa });
|
|
96
|
+
return momoa.evaluate(finalResult.document.body) as unknown as
|
|
97
|
+
| ResolverSetNormalized
|
|
98
|
+
| ResolverModifierNormalized;
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
101
|
+
_source: {
|
|
102
|
+
filename,
|
|
103
|
+
node,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|