@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.
Files changed (53) hide show
  1. package/dist/index.d.ts +47 -9
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +755 -570
  4. package/dist/index.js.map +1 -1
  5. package/package.json +4 -4
  6. package/src/build/index.ts +0 -209
  7. package/src/config.ts +0 -304
  8. package/src/index.ts +0 -95
  9. package/src/lib/code-frame.ts +0 -177
  10. package/src/lib/momoa.ts +0 -10
  11. package/src/lib/resolver-utils.ts +0 -35
  12. package/src/lint/index.ts +0 -142
  13. package/src/lint/plugin-core/index.ts +0 -103
  14. package/src/lint/plugin-core/lib/docs.ts +0 -3
  15. package/src/lint/plugin-core/rules/a11y-min-contrast.ts +0 -91
  16. package/src/lint/plugin-core/rules/a11y-min-font-size.ts +0 -66
  17. package/src/lint/plugin-core/rules/colorspace.ts +0 -108
  18. package/src/lint/plugin-core/rules/consistent-naming.ts +0 -65
  19. package/src/lint/plugin-core/rules/descriptions.ts +0 -43
  20. package/src/lint/plugin-core/rules/duplicate-values.ts +0 -85
  21. package/src/lint/plugin-core/rules/max-gamut.ts +0 -144
  22. package/src/lint/plugin-core/rules/required-children.ts +0 -106
  23. package/src/lint/plugin-core/rules/required-modes.ts +0 -75
  24. package/src/lint/plugin-core/rules/required-type.ts +0 -28
  25. package/src/lint/plugin-core/rules/required-typography-properties.ts +0 -65
  26. package/src/lint/plugin-core/rules/valid-boolean.ts +0 -41
  27. package/src/lint/plugin-core/rules/valid-border.ts +0 -57
  28. package/src/lint/plugin-core/rules/valid-color.ts +0 -265
  29. package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +0 -83
  30. package/src/lint/plugin-core/rules/valid-dimension.ts +0 -199
  31. package/src/lint/plugin-core/rules/valid-duration.ts +0 -123
  32. package/src/lint/plugin-core/rules/valid-font-family.ts +0 -68
  33. package/src/lint/plugin-core/rules/valid-font-weight.ts +0 -89
  34. package/src/lint/plugin-core/rules/valid-gradient.ts +0 -79
  35. package/src/lint/plugin-core/rules/valid-link.ts +0 -41
  36. package/src/lint/plugin-core/rules/valid-number.ts +0 -63
  37. package/src/lint/plugin-core/rules/valid-shadow.ts +0 -67
  38. package/src/lint/plugin-core/rules/valid-string.ts +0 -41
  39. package/src/lint/plugin-core/rules/valid-stroke-style.ts +0 -104
  40. package/src/lint/plugin-core/rules/valid-transition.ts +0 -61
  41. package/src/lint/plugin-core/rules/valid-typography.ts +0 -67
  42. package/src/logger.ts +0 -213
  43. package/src/parse/index.ts +0 -124
  44. package/src/parse/load.ts +0 -171
  45. package/src/parse/normalize.ts +0 -163
  46. package/src/parse/process.ts +0 -123
  47. package/src/parse/token.ts +0 -539
  48. package/src/resolver/create-synthetic-resolver.ts +0 -86
  49. package/src/resolver/index.ts +0 -7
  50. package/src/resolver/load.ts +0 -216
  51. package/src/resolver/normalize.ts +0 -106
  52. package/src/resolver/validate.ts +0 -375
  53. package/src/types.ts +0 -461
@@ -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
- }
@@ -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
- }