@terrazzo/parser 2.0.0-alpha.3 → 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.
@@ -1,16 +1,18 @@
1
- import type { DocumentNode } from '@humanwhocodes/momoa';
2
- import { maybeRawJSON } from '@terrazzo/json-schema-tools';
1
+ import * as momoa from '@humanwhocodes/momoa';
2
+ import { type InputSource, type InputSourceWithDocument, maybeRawJSON } from '@terrazzo/json-schema-tools';
3
3
  import type { TokenNormalizedSet } from '@terrazzo/token-tools';
4
- import eq from 'fast-deep-equal';
5
4
  import { merge } from 'merge-anything';
6
5
  import type yamlToMomoa from 'yaml-to-momoa';
7
6
  import { toMomoa } from '../lib/momoa.js';
7
+ import { makeInputKey } from '../lib/resolver-utils.js';
8
8
  import type Logger from '../logger.js';
9
- import type { InputSource, Resolver, ResolverSourceNormalized } from '../types.js';
9
+ import { processTokens } from '../parse/process.js';
10
+ import type { ConfigInit, Resolver, ResolverSourceNormalized } from '../types.js';
10
11
  import { normalizeResolver } from './normalize.js';
11
12
  import { isLikelyResolver, validateResolver } from './validate.js';
12
13
 
13
14
  export interface LoadResolverOptions {
15
+ config: ConfigInit;
14
16
  logger: Logger;
15
17
  req: (url: URL, origin: URL) => Promise<string>;
16
18
  yamlToMomoa?: typeof yamlToMomoa;
@@ -18,17 +20,18 @@ export interface LoadResolverOptions {
18
20
 
19
21
  /** Quick-parse input sources and find a resolver */
20
22
  export async function loadResolver(
21
- inputs: Omit<InputSource, 'document'>[],
22
- { logger, req, yamlToMomoa }: LoadResolverOptions,
23
- ): Promise<Resolver | undefined> {
24
- let resolverDoc: DocumentNode | undefined;
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 = {};
25
28
  const entry = {
26
29
  group: 'parser',
27
30
  label: 'init',
28
31
  } as const;
29
32
 
30
33
  for (const input of inputs) {
31
- let document: DocumentNode | undefined;
34
+ let document: momoa.DocumentNode | undefined;
32
35
  if (typeof input.src === 'string') {
33
36
  if (maybeRawJSON(input.src)) {
34
37
  document = toMomoa(input.src);
@@ -60,6 +63,7 @@ export async function loadResolver(
60
63
  break;
61
64
  }
62
65
 
66
+ let resolver: Resolver | undefined;
63
67
  if (resolverDoc) {
64
68
  validateResolver(resolverDoc, { logger, src: inputs[0]!.src });
65
69
  const normalized = await normalizeResolver(resolverDoc, {
@@ -69,45 +73,67 @@ export async function loadResolver(
69
73
  src: inputs[0]!.src,
70
74
  yamlToMomoa,
71
75
  });
72
- return createResolver(normalized, { logger });
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);
73
87
  }
88
+
89
+ return {
90
+ resolver,
91
+ tokens,
92
+ sources: [{ ...inputs[0]!, document: resolverDoc! }],
93
+ };
74
94
  }
75
95
 
76
96
  export interface CreateResolverOptions {
97
+ config: ConfigInit;
77
98
  logger: Logger;
99
+ sources: InputSourceWithDocument[];
78
100
  }
79
101
 
80
102
  /** Create an interface to resolve permutations */
81
- export function createResolver(resolverSource: ResolverSourceNormalized, { logger }: CreateResolverOptions): Resolver {
103
+ export function createResolver(
104
+ resolverSource: ResolverSourceNormalized,
105
+ { config, logger, sources }: CreateResolverOptions,
106
+ ): Resolver {
82
107
  const inputDefaults: Record<string, string> = {};
83
- const modifierPermutations: [string, string[]][] = []; // figure out modifiers
84
- for (const [name, m] of Object.entries(resolverSource.modifiers ?? {})) {
85
- if (typeof m.default === 'string') {
86
- inputDefaults[name] = m.default;
87
- }
88
- modifierPermutations.push([name, Object.keys(m.contexts)]);
89
- }
108
+ const validContexts: Record<string, string[]> = {};
109
+ const allPermutations: Record<string, string>[] = [];
110
+
111
+ const resolverCache: Record<string, any> = {};
112
+
90
113
  for (const m of resolverSource.resolutionOrder) {
91
- if (!('type' in m) || m.type !== 'modifier') {
92
- continue;
93
- }
94
- if (typeof m.default === 'string') {
95
- inputDefaults[m.name] = m.default;
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);
96
119
  }
97
- modifierPermutations.push([m.name, Object.keys(m.contexts)]);
98
120
  }
99
121
 
100
- const permutations = calculatePermutations(modifierPermutations);
101
-
102
122
  return {
103
123
  apply(inputRaw): TokenNormalizedSet {
104
- let tokens: TokenNormalizedSet = {};
124
+ let tokensRaw: TokenNormalizedSet = {};
105
125
  const input = { ...inputDefaults, ...inputRaw };
126
+ const inputKey = makeInputKey(input);
127
+
128
+ if (resolverCache[inputKey]) {
129
+ return resolverCache[inputKey];
130
+ }
131
+
106
132
  for (const item of resolverSource.resolutionOrder) {
107
133
  switch (item.type) {
108
134
  case 'set': {
109
135
  for (const s of item.sources) {
110
- tokens = merge(tokens, s) as TokenNormalizedSet;
136
+ tokensRaw = merge(tokensRaw, s) as TokenNormalizedSet;
111
137
  }
112
138
  break;
113
139
  }
@@ -122,22 +148,51 @@ export function createResolver(resolverSource: ResolverSourceNormalized, { logge
122
148
  });
123
149
  }
124
150
  for (const s of sources ?? []) {
125
- tokens = merge(tokens, s) as TokenNormalizedSet;
151
+ tokensRaw = merge(tokensRaw, s) as TokenNormalizedSet;
126
152
  }
127
153
  break;
128
154
  }
129
155
  }
130
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;
131
168
  return tokens;
132
169
  },
133
170
  source: resolverSource,
134
- permutations,
135
- isValidInput(inputRaw: Record<string, string>) {
136
- if (!inputRaw || typeof inputRaw !== 'object') {
137
- logger.error({ group: 'parser', label: 'resolver', message: `Invalid input: ${JSON.stringify(inputRaw)}.` });
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)));
138
175
  }
139
- const input = { ...inputDefaults, ...inputRaw };
140
- return permutations.findIndex((p) => eq(input, p)) !== -1;
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;
141
196
  },
142
197
  };
143
198
  }
@@ -1,4 +1,4 @@
1
- import { type ArrayNode, type DocumentNode, evaluate, type ObjectNode, type StringNode } from '@humanwhocodes/momoa';
1
+ import * as momoa from '@humanwhocodes/momoa';
2
2
  import { bundle, getObjMember, getObjMembers, parseRef } from '@terrazzo/json-schema-tools';
3
3
  import type yamlToMomoa from 'yaml-to-momoa';
4
4
  import type Logger from '../logger.js';
@@ -21,11 +21,11 @@ export interface NormalizeResolverOptions {
21
21
 
22
22
  /** Normalize resolver (assuming it’s been validated) */
23
23
  export async function normalizeResolver(
24
- node: DocumentNode,
24
+ node: momoa.DocumentNode,
25
25
  { filename, req, src, logger, yamlToMomoa }: NormalizeResolverOptions,
26
26
  ): Promise<ResolverSourceNormalized> {
27
- const resolverSource = evaluate(node) as unknown as ResolverSourceNormalized;
28
- const resolutionOrder = getObjMember(node.body as ObjectNode, 'resolutionOrder') as ArrayNode;
27
+ const resolverSource = momoa.evaluate(node) as unknown as ResolverSourceNormalized;
28
+ const resolutionOrder = getObjMember(node.body as momoa.ObjectNode, 'resolutionOrder') as momoa.ArrayNode;
29
29
 
30
30
  return {
31
31
  name: resolverSource.name,
@@ -35,7 +35,7 @@ export async function normalizeResolver(
35
35
  modifiers: resolverSource.modifiers,
36
36
  resolutionOrder: await Promise.all(
37
37
  resolutionOrder.elements.map(async (element, i) => {
38
- const layer = element.value as ObjectNode;
38
+ const layer = element.value as momoa.ObjectNode;
39
39
  const members = getObjMembers(layer);
40
40
 
41
41
  // If this is an inline set or modifier it’s already been validated; we only need
@@ -45,7 +45,7 @@ export async function normalizeResolver(
45
45
  // 1. $ref
46
46
  if (members.$ref) {
47
47
  const entry = { group: 'parser', label: 'init', node: members.$ref, src } as const;
48
- const { url, subpath } = parseRef((members.$ref as unknown as StringNode).value);
48
+ const { url, subpath } = parseRef((members.$ref as unknown as momoa.StringNode).value);
49
49
  if (url === '.') {
50
50
  // 1a. local $ref: pull from local document
51
51
  if (!subpath?.[0]) {
@@ -63,6 +63,7 @@ export async function normalizeResolver(
63
63
  } else {
64
64
  item = {
65
65
  type: subpath[0] === 'sets' ? 'set' : 'modifier',
66
+ name: subpath[1],
66
67
  ...(resolvedItem as any), // Note: as long as this exists, this has already been validated to be correct
67
68
  };
68
69
  }
@@ -79,11 +80,11 @@ export async function normalizeResolver(
79
80
  if (result.document.body.type === 'Object') {
80
81
  const type = getObjMember(result.document.body, 'type');
81
82
  if (type?.type === 'String' && type.value === 'set') {
82
- validateSet(result.document.body as ObjectNode, true, src);
83
- item = evaluate(result.document.body) as unknown as ResolverSetInline;
83
+ validateSet(result.document.body as momoa.ObjectNode, true, src);
84
+ item = momoa.evaluate(result.document.body) as unknown as ResolverSetInline;
84
85
  } else if (type?.type === 'String' && type.value === 'modifier') {
85
- validateModifier(result.document.body as ObjectNode, true, src);
86
- item = evaluate(result.document.body) as unknown as ResolverModifierInline;
86
+ validateModifier(result.document.body as momoa.ObjectNode, true, src);
87
+ item = momoa.evaluate(result.document.body) as unknown as ResolverModifierInline;
87
88
  }
88
89
  }
89
90
  logger.error({ ...entry, message: '$ref did not resolve to a valid Set or Modifier.' });
@@ -92,8 +93,14 @@ export async function normalizeResolver(
92
93
 
93
94
  // 2. resolve inline sources & contexts
94
95
  const finalResult = await bundle([{ filename, src: item }], { req, yamlToMomoa });
95
- return evaluate(finalResult.document.body) as unknown as ResolverSetNormalized | ResolverModifierNormalized;
96
+ return momoa.evaluate(finalResult.document.body) as unknown as
97
+ | ResolverSetNormalized
98
+ | ResolverModifierNormalized;
96
99
  }),
97
100
  ),
101
+ _source: {
102
+ filename,
103
+ node,
104
+ },
98
105
  };
99
106
  }
@@ -1,4 +1,4 @@
1
- import type { DocumentNode, ObjectNode } from '@humanwhocodes/momoa';
1
+ import type * as momoa from '@humanwhocodes/momoa';
2
2
  import { getObjMember, getObjMembers } from '@terrazzo/json-schema-tools';
3
3
  import type { LogEntry, default as Logger } from '../logger.js';
4
4
 
@@ -10,7 +10,7 @@ import type { LogEntry, default as Logger } from '../logger.js';
10
10
  * guesswork here, but we try and find a reasonable edge case where we sniff out
11
11
  * invalid DTCG syntax that a resolver doc would have.
12
12
  */
13
- export function isLikelyResolver(doc: DocumentNode): boolean {
13
+ export function isLikelyResolver(doc: momoa.DocumentNode): boolean {
14
14
  if (doc.body.type !== 'Object') {
15
15
  return false;
16
16
  }
@@ -79,7 +79,7 @@ const MESSAGE_EXPECTED = {
79
79
  * Validate a resolver document.
80
80
  * There’s a ton of boilerplate here, only to surface detailed code frames. Is there a better abstraction?
81
81
  */
82
- export function validateResolver(node: DocumentNode, { logger, src }: ValidateResolverOptions) {
82
+ export function validateResolver(node: momoa.DocumentNode, { logger, src }: ValidateResolverOptions) {
83
83
  const entry = { group: 'parser', label: 'resolver', src } as const;
84
84
  if (node.body.type !== 'Object') {
85
85
  logger.error({ ...entry, message: MESSAGE_EXPECTED.OBJECT, node });
@@ -89,7 +89,7 @@ export function validateResolver(node: DocumentNode, { logger, src }: ValidateRe
89
89
  let hasVersion = false;
90
90
  let hasResolutionOrder = false;
91
91
 
92
- for (const member of (node.body as ObjectNode).members) {
92
+ for (const member of (node.body as momoa.ObjectNode).members) {
93
93
  if (member.name.type !== 'String') {
94
94
  continue; // IDK, don’t ask
95
95
  }
@@ -202,7 +202,7 @@ export function validateResolver(node: DocumentNode, { logger, src }: ValidateRe
202
202
  }
203
203
  }
204
204
 
205
- export function validateSet(node: ObjectNode, isInline = false, { src }: ValidateResolverOptions): LogEntry[] {
205
+ export function validateSet(node: momoa.ObjectNode, isInline = false, { src }: ValidateResolverOptions): LogEntry[] {
206
206
  const entry = { group: 'parser', label: 'resolver', src } as const;
207
207
  const errors: LogEntry[] = [];
208
208
  let hasName = !isInline;
@@ -277,7 +277,11 @@ export function validateSet(node: ObjectNode, isInline = false, { src }: Validat
277
277
  return errors;
278
278
  }
279
279
 
280
- export function validateModifier(node: ObjectNode, isInline = false, { src }: ValidateResolverOptions): LogEntry[] {
280
+ export function validateModifier(
281
+ node: momoa.ObjectNode,
282
+ isInline = false,
283
+ { src }: ValidateResolverOptions,
284
+ ): LogEntry[] {
281
285
  const errors: LogEntry[] = [];
282
286
  const entry = { group: 'parser', label: 'resolver', src } as const;
283
287
  let hasName = !isInline;
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type * as momoa from '@humanwhocodes/momoa';
2
+ import type { InputSourceWithDocument } from '@terrazzo/json-schema-tools';
2
3
  import type {
3
4
  Group,
4
5
  TokenNormalized,
@@ -30,7 +31,7 @@ export interface BuildHookOptions {
30
31
  /** Query transformed values */
31
32
  getTransforms(params: TransformParams): TokenTransformed[];
32
33
  /** Momoa documents */
33
- sources: InputSource[];
34
+ sources: InputSourceWithDocument[];
34
35
  outputFile: (
35
36
  /** Filename to output (relative to outDir) */
36
37
  filename: string,
@@ -51,7 +52,7 @@ export interface BuildEndHookOptions {
51
52
  /** Query transformed values */
52
53
  getTransforms(params: TransformParams): TokenTransformed[];
53
54
  /** Momoa documents */
54
- sources: InputSource[];
55
+ sources: InputSourceWithDocument[];
55
56
  /** Final files to be written */
56
57
  outputFiles: OutputFileExpanded[];
57
58
  }
@@ -147,12 +148,6 @@ export interface ConfigOptions {
147
148
  cwd: URL;
148
149
  }
149
150
 
150
- export interface InputSource {
151
- filename?: URL;
152
- src: any;
153
- document: momoa.DocumentNode;
154
- }
155
-
156
151
  export interface LintNotice {
157
152
  /** Lint message shown to the user */
158
153
  message: string;
@@ -226,7 +221,7 @@ export interface LintRuleContext<MessageIds extends string, LintRuleOptions exte
226
221
  * All source files present in this run. To find the original source, match a
227
222
  * token’s `source.loc` filename to one of the source’s `filename`s.
228
223
  */
229
- sources: InputSource[];
224
+ sources: InputSourceWithDocument[];
230
225
  /** Source file location. */
231
226
  filename?: URL;
232
227
  /** ID:Token map of all tokens. */
@@ -309,7 +304,7 @@ export interface ParseOptions {
309
304
  */
310
305
  transform?: TransformVisitors;
311
306
  /** (internal cache; do not use) */
312
- _sources?: Record<string, InputSource>;
307
+ _sources?: Record<string, InputSourceWithDocument>;
313
308
  }
314
309
 
315
310
  export interface Plugin {
@@ -342,7 +337,7 @@ export interface Resolver<
342
337
  /** Supply values to modifiers to produce a final tokens set */
343
338
  apply: (input: Partial<Input>) => TokenNormalizedSet;
344
339
  /** List all possible valid input combinations. Ignores default values, as they would duplicate some other permutations. */
345
- permutations: Input[];
340
+ listPermutations: () => Input[];
346
341
  /** The original resolver document, simplified */
347
342
  source: ResolverSourceNormalized;
348
343
  /** Helper function for permutations—see if a particular input is valid. Automatically applies default values. */
@@ -378,6 +373,10 @@ export interface ResolverSourceNormalized {
378
373
  * pass over the resolutionOrder array is needed.
379
374
  */
380
375
  resolutionOrder: (ResolverSetNormalized | ResolverModifierNormalized)[];
376
+ _source: {
377
+ filename?: URL;
378
+ node: momoa.DocumentNode;
379
+ };
381
380
  }
382
381
 
383
382
  export interface ResolverModifier<Context extends string = string> {
@@ -454,5 +453,5 @@ export interface TransformHookOptions {
454
453
  },
455
454
  ): void;
456
455
  /** Momoa documents */
457
- sources: InputSource[];
456
+ sources: InputSourceWithDocument[];
458
457
  }