@terrazzo/parser 2.0.0-alpha.6 → 2.0.0-beta.0
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/dist/index.d.ts +47 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +755 -570
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/build/index.ts +0 -209
- package/src/config.ts +0 -304
- package/src/index.ts +0 -95
- package/src/lib/code-frame.ts +0 -177
- package/src/lib/momoa.ts +0 -10
- package/src/lib/resolver-utils.ts +0 -35
- package/src/lint/index.ts +0 -142
- package/src/lint/plugin-core/index.ts +0 -103
- package/src/lint/plugin-core/lib/docs.ts +0 -3
- package/src/lint/plugin-core/rules/a11y-min-contrast.ts +0 -91
- package/src/lint/plugin-core/rules/a11y-min-font-size.ts +0 -66
- package/src/lint/plugin-core/rules/colorspace.ts +0 -108
- package/src/lint/plugin-core/rules/consistent-naming.ts +0 -65
- package/src/lint/plugin-core/rules/descriptions.ts +0 -43
- package/src/lint/plugin-core/rules/duplicate-values.ts +0 -85
- package/src/lint/plugin-core/rules/max-gamut.ts +0 -144
- package/src/lint/plugin-core/rules/required-children.ts +0 -106
- package/src/lint/plugin-core/rules/required-modes.ts +0 -75
- package/src/lint/plugin-core/rules/required-type.ts +0 -28
- package/src/lint/plugin-core/rules/required-typography-properties.ts +0 -65
- package/src/lint/plugin-core/rules/valid-boolean.ts +0 -41
- package/src/lint/plugin-core/rules/valid-border.ts +0 -57
- package/src/lint/plugin-core/rules/valid-color.ts +0 -265
- package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +0 -83
- package/src/lint/plugin-core/rules/valid-dimension.ts +0 -199
- package/src/lint/plugin-core/rules/valid-duration.ts +0 -123
- package/src/lint/plugin-core/rules/valid-font-family.ts +0 -68
- package/src/lint/plugin-core/rules/valid-font-weight.ts +0 -89
- package/src/lint/plugin-core/rules/valid-gradient.ts +0 -79
- package/src/lint/plugin-core/rules/valid-link.ts +0 -41
- package/src/lint/plugin-core/rules/valid-number.ts +0 -63
- package/src/lint/plugin-core/rules/valid-shadow.ts +0 -67
- package/src/lint/plugin-core/rules/valid-string.ts +0 -41
- package/src/lint/plugin-core/rules/valid-stroke-style.ts +0 -104
- package/src/lint/plugin-core/rules/valid-transition.ts +0 -61
- package/src/lint/plugin-core/rules/valid-typography.ts +0 -67
- package/src/logger.ts +0 -213
- package/src/parse/index.ts +0 -124
- package/src/parse/load.ts +0 -171
- package/src/parse/normalize.ts +0 -163
- package/src/parse/process.ts +0 -123
- package/src/parse/token.ts +0 -539
- package/src/resolver/create-synthetic-resolver.ts +0 -86
- package/src/resolver/index.ts +0 -7
- package/src/resolver/load.ts +0 -216
- package/src/resolver/normalize.ts +0 -106
- package/src/resolver/validate.ts +0 -375
- package/src/types.ts +0 -461
package/src/resolver/load.ts
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import type * 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: { [resolverSource._source.filename!.href]: rootSource },
|
|
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
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
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
|
-
}
|
package/src/resolver/validate.ts
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
-
import { getObjMember, getObjMembers } from '@terrazzo/json-schema-tools';
|
|
3
|
-
import type { LogEntry, default as Logger } from '../logger.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Determine whether this is likely a resolver
|
|
7
|
-
* We use terms the word “likely” because this occurs before validation. Since
|
|
8
|
-
* we may be dealing with a doc _intended_ to be a resolver, but may be lacking
|
|
9
|
-
* some critical information, how can we determine intent? There’s a bit of
|
|
10
|
-
* guesswork here, but we try and find a reasonable edge case where we sniff out
|
|
11
|
-
* invalid DTCG syntax that a resolver doc would have.
|
|
12
|
-
*/
|
|
13
|
-
export function isLikelyResolver(doc: momoa.DocumentNode): boolean {
|
|
14
|
-
if (doc.body.type !== 'Object') {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
// This is a resolver if…
|
|
18
|
-
for (const member of doc.body.members) {
|
|
19
|
-
if (member.name.type !== 'String') {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
switch (member.name.value) {
|
|
23
|
-
case 'name':
|
|
24
|
-
case 'description':
|
|
25
|
-
case 'version': {
|
|
26
|
-
// 1. name, description, or version are a string
|
|
27
|
-
if (member.value.type === 'String') {
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
case 'sets':
|
|
33
|
-
case 'modifiers': {
|
|
34
|
-
if (member.value.type !== 'Object') {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
// 2. sets.description or modifiers.description is a string
|
|
38
|
-
if (getObjMember(member.value, 'description')?.type === 'String') {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
// 3. sets.sources is an array
|
|
42
|
-
if (member.name.value === 'sets' && getObjMember(member.value, 'sources')?.type === 'Array') {
|
|
43
|
-
return true;
|
|
44
|
-
} else if (member.name.value === 'modifiers') {
|
|
45
|
-
const contexts = getObjMember(member.value, 'contexts');
|
|
46
|
-
if (contexts?.type === 'Object' && contexts.members.some((m) => m.value.type === 'Array')) {
|
|
47
|
-
// 4. contexts[key] is an array
|
|
48
|
-
// (note: modifiers.contexts as an object is technically valid token format! We need to check for the array)
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
case 'resolutionOrder': {
|
|
55
|
-
// 4. resolutionOrder is an array
|
|
56
|
-
if (member.value.type === 'Array') {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface ValidateResolverOptions {
|
|
68
|
-
logger: Logger;
|
|
69
|
-
src: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const MESSAGE_EXPECTED = {
|
|
73
|
-
STRING: 'Expected string.',
|
|
74
|
-
OBJECT: 'Expected object.',
|
|
75
|
-
ARRAY: 'Expected array.',
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Validate a resolver document.
|
|
80
|
-
* There’s a ton of boilerplate here, only to surface detailed code frames. Is there a better abstraction?
|
|
81
|
-
*/
|
|
82
|
-
export function validateResolver(node: momoa.DocumentNode, { logger, src }: ValidateResolverOptions) {
|
|
83
|
-
const entry = { group: 'parser', label: 'resolver', src } as const;
|
|
84
|
-
if (node.body.type !== 'Object') {
|
|
85
|
-
logger.error({ ...entry, message: MESSAGE_EXPECTED.OBJECT, node });
|
|
86
|
-
}
|
|
87
|
-
const errors: LogEntry[] = [];
|
|
88
|
-
|
|
89
|
-
let hasVersion = false;
|
|
90
|
-
let hasResolutionOrder = false;
|
|
91
|
-
|
|
92
|
-
for (const member of (node.body as momoa.ObjectNode).members) {
|
|
93
|
-
if (member.name.type !== 'String') {
|
|
94
|
-
continue; // IDK, don’t ask
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
switch (member.name.value) {
|
|
98
|
-
case 'name':
|
|
99
|
-
case 'description': {
|
|
100
|
-
if (member.value.type !== 'String') {
|
|
101
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.STRING });
|
|
102
|
-
}
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
case 'version': {
|
|
107
|
-
hasVersion = true;
|
|
108
|
-
if (member.value.type !== 'String' || member.value.value !== '2025.10') {
|
|
109
|
-
errors.push({ ...entry, message: `Expected "version" to be "2025.10".`, node: member.value });
|
|
110
|
-
}
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
case 'sets':
|
|
115
|
-
case 'modifiers': {
|
|
116
|
-
if (member.value.type !== 'Object') {
|
|
117
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.OBJECT, node: member.value });
|
|
118
|
-
} else {
|
|
119
|
-
for (const item of member.value.members) {
|
|
120
|
-
if (item.value.type !== 'Object') {
|
|
121
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.OBJECT, node: item.value });
|
|
122
|
-
} else {
|
|
123
|
-
const validator = member.name.value === 'sets' ? validateSet : validateModifier;
|
|
124
|
-
errors.push(...validator(item.value, false, { logger, src }));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
case 'resolutionOrder': {
|
|
132
|
-
hasResolutionOrder = true;
|
|
133
|
-
if (member.value.type !== 'Array') {
|
|
134
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.ARRAY, node: member.value });
|
|
135
|
-
} else if (member.value.elements.length === 0) {
|
|
136
|
-
errors.push({ ...entry, message: `"resolutionOrder" can’t be empty array.`, node: member.value });
|
|
137
|
-
} else {
|
|
138
|
-
for (const item of member.value.elements) {
|
|
139
|
-
if (item.value.type !== 'Object') {
|
|
140
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.OBJECT, node: item.value });
|
|
141
|
-
} else {
|
|
142
|
-
const itemMembers = getObjMembers(item.value);
|
|
143
|
-
if (itemMembers.$ref?.type === 'String') {
|
|
144
|
-
continue; // we can’t validate this just yet, assume it’s correct
|
|
145
|
-
}
|
|
146
|
-
// Validate "type"
|
|
147
|
-
if (itemMembers.type?.type === 'String') {
|
|
148
|
-
if (itemMembers.type.value === 'set') {
|
|
149
|
-
validateSet(item.value, true, { logger, src });
|
|
150
|
-
} else if (itemMembers.type.value === 'modifier') {
|
|
151
|
-
validateModifier(item.value, true, { logger, src });
|
|
152
|
-
} else {
|
|
153
|
-
errors.push({
|
|
154
|
-
...entry,
|
|
155
|
-
message: `Unknown type ${JSON.stringify(itemMembers.type.value)}`,
|
|
156
|
-
node: itemMembers.type,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// validate sets & modifiers if they’re missing "type"
|
|
161
|
-
if (itemMembers.sources?.type === 'Array') {
|
|
162
|
-
validateSet(item.value, true, { logger, src });
|
|
163
|
-
} else if (itemMembers.contexts?.type === 'Object') {
|
|
164
|
-
validateModifier(item.value, true, { logger, src });
|
|
165
|
-
} else if (itemMembers.name?.type === 'String' || itemMembers.description?.type === 'String') {
|
|
166
|
-
validateSet(item.value, true, { logger, src }); // if this has a "name" or "description", guess set
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
case '$defs':
|
|
174
|
-
case '$extensions':
|
|
175
|
-
if (member.value.type !== 'Object') {
|
|
176
|
-
errors.push({ ...entry, message: `Expected object`, node: member.value });
|
|
177
|
-
}
|
|
178
|
-
break;
|
|
179
|
-
case '$schema':
|
|
180
|
-
case '$ref': {
|
|
181
|
-
if (member.value.type !== 'String') {
|
|
182
|
-
errors.push({ ...entry, message: `Expected string`, node: member.value });
|
|
183
|
-
}
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
default: {
|
|
187
|
-
errors.push({ ...entry, message: `Unknown key ${JSON.stringify(member.name.value)}`, node: member.name, src });
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// handle required keys
|
|
194
|
-
if (!hasVersion) {
|
|
195
|
-
errors.push({ ...entry, message: `Missing "version".`, node, src });
|
|
196
|
-
}
|
|
197
|
-
if (!hasResolutionOrder) {
|
|
198
|
-
errors.push({ ...entry, message: `Missing "resolutionOrder".`, node, src });
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (errors.length) {
|
|
202
|
-
logger.error(...errors);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function validateSet(node: momoa.ObjectNode, isInline = false, { src }: ValidateResolverOptions): LogEntry[] {
|
|
207
|
-
const entry = { group: 'parser', label: 'resolver', src } as const;
|
|
208
|
-
const errors: LogEntry[] = [];
|
|
209
|
-
let hasName = !isInline;
|
|
210
|
-
let hasType = !isInline;
|
|
211
|
-
let hasSources = false;
|
|
212
|
-
for (const member of node.members) {
|
|
213
|
-
if (member.name.type !== 'String') {
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
switch (member.name.value) {
|
|
217
|
-
case 'name': {
|
|
218
|
-
hasName = true;
|
|
219
|
-
if (member.value.type !== 'String') {
|
|
220
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.STRING, node: member.value });
|
|
221
|
-
}
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
case 'description': {
|
|
225
|
-
if (member.value.type !== 'String') {
|
|
226
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.STRING, node: member.value });
|
|
227
|
-
}
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
case 'type': {
|
|
231
|
-
hasType = true;
|
|
232
|
-
if (member.value.type !== 'String') {
|
|
233
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.STRING, node: member.value });
|
|
234
|
-
} else if (member.value.value !== 'set') {
|
|
235
|
-
errors.push({ ...entry, message: '"type" must be "set".' });
|
|
236
|
-
}
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
case 'sources': {
|
|
240
|
-
hasSources = true;
|
|
241
|
-
if (member.value.type !== 'Array') {
|
|
242
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.ARRAY, node: member.value });
|
|
243
|
-
} else if (member.value.elements.length === 0) {
|
|
244
|
-
errors.push({ ...entry, message: `"sources" can’t be empty array.`, node: member.value });
|
|
245
|
-
}
|
|
246
|
-
break;
|
|
247
|
-
}
|
|
248
|
-
case '$defs':
|
|
249
|
-
case '$extensions':
|
|
250
|
-
if (member.value.type !== 'Object') {
|
|
251
|
-
errors.push({ ...entry, message: `Expected object`, node: member.value });
|
|
252
|
-
}
|
|
253
|
-
break;
|
|
254
|
-
case '$ref': {
|
|
255
|
-
if (member.value.type !== 'String') {
|
|
256
|
-
errors.push({ ...entry, message: `Expected string`, node: member.value });
|
|
257
|
-
}
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
default: {
|
|
261
|
-
errors.push({ ...entry, message: `Unknown key ${JSON.stringify(member.name.value)}`, node: member.name });
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// handle required keys
|
|
268
|
-
if (!hasName) {
|
|
269
|
-
errors.push({ ...entry, message: `Missing "name".`, node });
|
|
270
|
-
}
|
|
271
|
-
if (!hasType) {
|
|
272
|
-
errors.push({ ...entry, message: `"type": "set" missing.`, node });
|
|
273
|
-
}
|
|
274
|
-
if (!hasSources) {
|
|
275
|
-
errors.push({ ...entry, message: `Missing "sources".`, node });
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return errors;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export function validateModifier(
|
|
282
|
-
node: momoa.ObjectNode,
|
|
283
|
-
isInline = false,
|
|
284
|
-
{ src }: ValidateResolverOptions,
|
|
285
|
-
): LogEntry[] {
|
|
286
|
-
const errors: LogEntry[] = [];
|
|
287
|
-
const entry = { group: 'parser', label: 'resolver', src } as const;
|
|
288
|
-
let hasName = !isInline;
|
|
289
|
-
let hasType = !isInline;
|
|
290
|
-
let hasContexts = false;
|
|
291
|
-
for (const member of node.members) {
|
|
292
|
-
if (member.name.type !== 'String') {
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
switch (member.name.value) {
|
|
296
|
-
case 'name': {
|
|
297
|
-
hasName = true;
|
|
298
|
-
if (member.value.type !== 'String') {
|
|
299
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.STRING, node: member.value });
|
|
300
|
-
}
|
|
301
|
-
break;
|
|
302
|
-
}
|
|
303
|
-
case 'description': {
|
|
304
|
-
if (member.value.type !== 'String') {
|
|
305
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.STRING, node: member.value });
|
|
306
|
-
}
|
|
307
|
-
break;
|
|
308
|
-
}
|
|
309
|
-
case 'type': {
|
|
310
|
-
hasType = true;
|
|
311
|
-
if (member.value.type !== 'String') {
|
|
312
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.STRING, node: member.value });
|
|
313
|
-
} else if (member.value.value !== 'modifier') {
|
|
314
|
-
errors.push({ ...entry, message: '"type" must be "modifier".' });
|
|
315
|
-
}
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
case 'contexts': {
|
|
319
|
-
hasContexts = true;
|
|
320
|
-
if (member.value.type !== 'Object') {
|
|
321
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.OBJECT, node: member.value });
|
|
322
|
-
} else if (member.value.members.length === 0) {
|
|
323
|
-
errors.push({ ...entry, message: `"contexts" can’t be empty object.`, node: member.value });
|
|
324
|
-
} else {
|
|
325
|
-
for (const context of member.value.members) {
|
|
326
|
-
if (context.value.type !== 'Array') {
|
|
327
|
-
errors.push({ ...entry, message: MESSAGE_EXPECTED.ARRAY, node: context.value });
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
case 'default': {
|
|
334
|
-
if (member.value.type !== 'String') {
|
|
335
|
-
errors.push({ ...entry, message: `Expected string`, node: member.value });
|
|
336
|
-
} else {
|
|
337
|
-
const contexts = getObjMember(node, 'contexts') as momoa.ObjectNode | undefined;
|
|
338
|
-
if (!contexts || !getObjMember(contexts, member.value.value)) {
|
|
339
|
-
errors.push({ ...entry, message: 'Invalid default context', node: member.value });
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
case '$defs':
|
|
345
|
-
case '$extensions':
|
|
346
|
-
if (member.value.type !== 'Object') {
|
|
347
|
-
errors.push({ ...entry, message: `Expected object`, node: member.value });
|
|
348
|
-
}
|
|
349
|
-
break;
|
|
350
|
-
case '$ref': {
|
|
351
|
-
if (member.value.type !== 'String') {
|
|
352
|
-
errors.push({ ...entry, message: `Expected string`, node: member.value });
|
|
353
|
-
}
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
default: {
|
|
357
|
-
errors.push({ ...entry, message: `Unknown key ${JSON.stringify(member.name.value)}`, node: member.name });
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// handle required keys
|
|
364
|
-
if (!hasName) {
|
|
365
|
-
errors.push({ ...entry, message: `Missing "name".`, node });
|
|
366
|
-
}
|
|
367
|
-
if (!hasType) {
|
|
368
|
-
errors.push({ ...entry, message: `"type": "modifier" missing.`, node });
|
|
369
|
-
}
|
|
370
|
-
if (!hasContexts) {
|
|
371
|
-
errors.push({ ...entry, message: `Missing "contexts".`, node });
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return errors;
|
|
375
|
-
}
|