@terrazzo/parser 2.0.0-alpha.2 → 2.0.0-alpha.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrazzo/parser",
3
- "version": "2.0.0-alpha.2",
3
+ "version": "2.0.0-alpha.3",
4
4
  "description": "Parser/validator for the Design Tokens Community Group (DTCG) standard.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,23 +27,24 @@
27
27
  ".": "./dist/index.js",
28
28
  "./package.json": "./package.json"
29
29
  },
30
- "homepage": "https://terrazzo.app/docs/cli/api/js",
30
+ "homepage": "https://terrazzo.app/docs/reference/js-api",
31
31
  "repository": {
32
32
  "type": "git",
33
33
  "url": "https://github.com/terrazzoapp/terrazzo.git",
34
34
  "directory": "./packages/parser/"
35
35
  },
36
36
  "dependencies": {
37
- "@humanwhocodes/momoa": "^3.3.9",
37
+ "@humanwhocodes/momoa": "^3.3.10",
38
38
  "@types/babel__code-frame": "^7.0.6",
39
39
  "@types/culori": "^4.0.1",
40
40
  "culori": "^4.0.2",
41
+ "fast-deep-equal": "^3.1.3",
41
42
  "merge-anything": "^5.1.7",
42
43
  "picocolors": "^1.1.1",
43
44
  "scule": "^1.3.0",
44
45
  "wildcard-match": "^5.1.4",
45
- "@terrazzo/json-schema-tools": "^0.0.1",
46
- "@terrazzo/token-tools": "^2.0.0-alpha.2"
46
+ "@terrazzo/json-schema-tools": "^0.1.0",
47
+ "@terrazzo/token-tools": "^2.0.0-alpha.3"
47
48
  },
48
49
  "devDependencies": {
49
50
  "yaml-to-momoa": "0.0.6"
package/src/config.ts CHANGED
@@ -206,7 +206,6 @@ function normalizeLint({ config, logger }: { config: ConfigInit; logger: Logger
206
206
  // Note: sometimes plugins will be loaded multiple times, in which case it’s expected
207
207
  // they’re register rules again for lint(). Only throw an error if plugin A and plugin B’s
208
208
  // rules conflict.
209
-
210
209
  if (allRules.get(rule) && allRules.get(rule) !== plugin.name) {
211
210
  logger.error({
212
211
  group: 'config',
@@ -229,7 +228,7 @@ function normalizeLint({ config, logger }: { config: ConfigInit; logger: Logger
229
228
 
230
229
  const value = config.lint.rules[id];
231
230
  let severity: LintRuleSeverity = 'off';
232
- let options: any;
231
+ let options: any = {};
233
232
  if (typeof value === 'number' || typeof value === 'string') {
234
233
  severity = value;
235
234
  } else if (Array.isArray(value)) {
package/src/index.ts CHANGED
@@ -90,4 +90,6 @@ export { default as Logger } from './logger.js';
90
90
  export * from './parse/index.js';
91
91
  export { default as parse } from './parse/index.js';
92
92
 
93
+ export * from './resolver/index.js';
94
+
93
95
  export * from './types.js';
@@ -8,9 +8,9 @@ export * from './rules/consistent-naming.js';
8
8
  export * from './rules/descriptions.js';
9
9
  export * from './rules/duplicate-values.js';
10
10
  export * from './rules/max-gamut.js';
11
- export * from './rules/no-type-on-alias.js';
12
11
  export * from './rules/required-children.js';
13
12
  export * from './rules/required-modes.js';
13
+ export * from './rules/required-type.js';
14
14
  export * from './rules/required-typography-properties.js';
15
15
 
16
16
  import a11yMinContrast, { A11Y_MIN_CONTRAST } from './rules/a11y-min-contrast.js';
@@ -20,9 +20,9 @@ import consistentNaming, { CONSISTENT_NAMING } from './rules/consistent-naming.j
20
20
  import descriptions, { DESCRIPTIONS } from './rules/descriptions.js';
21
21
  import duplicateValues, { DUPLICATE_VALUES } from './rules/duplicate-values.js';
22
22
  import maxGamut, { MAX_GAMUT } from './rules/max-gamut.js';
23
- import noTypeOnAlias, { NO_TYPE_ON_ALIAS } from './rules/no-type-on-alias.js';
24
23
  import requiredChildren, { REQUIRED_CHILDREN } from './rules/required-children.js';
25
24
  import requiredModes, { REQUIRED_MODES } from './rules/required-modes.js';
25
+ import requiredType, { REQUIRED_TYPE } from './rules/required-type.js';
26
26
  import requiredTypographyProperties, {
27
27
  REQUIRED_TYPOGRAPHY_PROPERTIES,
28
28
  } from './rules/required-typography-properties.js';
@@ -65,9 +65,9 @@ const ALL_RULES = {
65
65
  [DESCRIPTIONS]: descriptions,
66
66
  [DUPLICATE_VALUES]: duplicateValues,
67
67
  [MAX_GAMUT]: maxGamut,
68
- [NO_TYPE_ON_ALIAS]: noTypeOnAlias,
69
68
  [REQUIRED_CHILDREN]: requiredChildren,
70
69
  [REQUIRED_MODES]: requiredModes,
70
+ [REQUIRED_TYPE]: requiredType,
71
71
  [REQUIRED_TYPOGRAPHY_PROPERTIES]: requiredTypographyProperties,
72
72
  [A11Y_MIN_CONTRAST]: a11yMinContrast,
73
73
  [A11Y_MIN_FONT_SIZE]: a11yMinFontSize,
@@ -100,5 +100,4 @@ export const RECOMMENDED_CONFIG: Record<string, LintRuleLonghand> = {
100
100
  [VALID_GRADIENT]: ['error', {}],
101
101
  [VALID_TYPOGRAPHY]: ['error', {}],
102
102
  [CONSISTENT_NAMING]: ['warn', { format: 'kebab-case' }],
103
- [NO_TYPE_ON_ALIAS]: ['warn', {}],
104
103
  };
@@ -1,3 +1,3 @@
1
1
  export function docsLink(ruleName: string): string {
2
- return `https://terrazzo.app/docs/cli/lint#${ruleName.replaceAll('/', '')}`;
2
+ return `https://terrazzo.app/docs/linting#${ruleName.replaceAll('/', '')}`;
3
3
  }
@@ -1,25 +1,24 @@
1
- import { isAlias } from '@terrazzo/token-tools';
2
1
  import type { LintRule } from '../../../types.js';
3
2
  import { docsLink } from '../lib/docs.js';
4
3
 
5
- export const NO_TYPE_ON_ALIAS = 'core/no-type-on-alias';
4
+ export const REQUIRED_TYPE = 'core/required-type';
6
5
 
7
6
  export const ERROR = 'ERROR';
8
7
 
9
8
  const rule: LintRule<typeof ERROR> = {
10
9
  meta: {
11
10
  messages: {
12
- [ERROR]: 'Remove $type from aliased value.',
11
+ [ERROR]: 'Token missing $type.',
13
12
  },
14
13
  docs: {
15
- description: 'If a $value is aliased it already has a $type defined.',
16
- url: docsLink(NO_TYPE_ON_ALIAS),
14
+ description: 'Requiring every token to have $type, even aliases, simplifies computation.',
15
+ url: docsLink(REQUIRED_TYPE),
17
16
  },
18
17
  },
19
18
  defaultOptions: {},
20
19
  create({ tokens, report }) {
21
20
  for (const t of Object.values(tokens)) {
22
- if (isAlias(t.originalValue!.$value as any) && t.originalValue?.$type) {
21
+ if (!t.originalValue?.$type) {
23
22
  report({ messageId: ERROR, node: t.source.node, filename: t.source.filename });
24
23
  }
25
24
  }
@@ -1,12 +1,15 @@
1
+ import type fsType from 'node:fs/promises';
1
2
  import { pluralize, type TokenNormalizedSet } from '@terrazzo/token-tools';
2
3
  import lintRunner from '../lint/index.js';
3
4
  import Logger from '../logger.js';
4
- import type { ConfigInit, InputSource, ParseOptions } from '../types.js';
5
+ import { loadResolver } from '../resolver/load.js';
6
+ import type { ConfigInit, InputSource, ParseOptions, Resolver } from '../types.js';
5
7
  import { loadSources } from './load.js';
6
8
 
7
9
  export interface ParseResult {
8
10
  tokens: TokenNormalizedSet;
9
11
  sources: InputSource[];
12
+ resolver?: Resolver | undefined;
10
13
  }
11
14
 
12
15
  /** Parse */
@@ -14,6 +17,7 @@ export default async function parse(
14
17
  _input: Omit<InputSource, 'document'> | Omit<InputSource, 'document'>[],
15
18
  {
16
19
  logger = new Logger(),
20
+ req = defaultReq,
17
21
  skipLint = false,
18
22
  config = {} as ConfigInit,
19
23
  continueOnError = false,
@@ -24,8 +28,20 @@ export default async function parse(
24
28
  const inputs = Array.isArray(_input) ? _input : [_input];
25
29
 
26
30
  const totalStart = performance.now();
31
+
32
+ // 1. Resolver
33
+ const resolver = await loadResolver(inputs, { logger, req, yamlToMomoa });
34
+
35
+ // 2. No resolver (tokens)
27
36
  const initStart = performance.now();
28
- const { tokens, sources } = await loadSources(inputs, { logger, config, continueOnError, yamlToMomoa, transform });
37
+ const { tokens, sources } = await loadSources(inputs, {
38
+ req,
39
+ logger,
40
+ config,
41
+ continueOnError,
42
+ yamlToMomoa,
43
+ transform,
44
+ });
29
45
  logger.debug({
30
46
  message: 'Loaded tokens',
31
47
  group: 'parser',
@@ -64,5 +80,23 @@ export default async function parse(
64
80
  return {
65
81
  tokens,
66
82
  sources,
83
+ resolver,
67
84
  };
68
85
  }
86
+
87
+ let fs: typeof fsType | undefined;
88
+
89
+ /** Fallback req */
90
+ async function defaultReq(src: URL, _origin: URL) {
91
+ if (src.protocol === 'file:') {
92
+ if (!fs) {
93
+ fs = await import('node:fs/promises');
94
+ }
95
+ return await fs.readFile(src, 'utf8');
96
+ }
97
+ const res = await fetch(src);
98
+ if (!res.ok) {
99
+ throw new Error(`${src} responded with ${res.status}\n${await res.text()}`);
100
+ }
101
+ return await res.text();
102
+ }
package/src/parse/load.ts CHANGED
@@ -51,14 +51,20 @@ export interface IntermediaryToken {
51
51
  }
52
52
 
53
53
  export interface LoadOptions extends Pick<ParseOptions, 'config' | 'continueOnError' | 'yamlToMomoa' | 'transform'> {
54
+ req: NonNullable<ParseOptions['req']>;
54
55
  logger: Logger;
55
56
  }
56
57
 
58
+ export interface LoadSourcesResult {
59
+ tokens: TokenNormalizedSet;
60
+ sources: InputSource[];
61
+ }
62
+
57
63
  /** Load from multiple entries, while resolving remote files */
58
64
  export async function loadSources(
59
65
  inputs: Omit<InputSource, 'document'>[],
60
- { config, logger, continueOnError, yamlToMomoa, transform }: LoadOptions,
61
- ): Promise<{ tokens: TokenNormalizedSet; sources: InputSource[] }> {
66
+ { config, logger, req, continueOnError, yamlToMomoa, transform }: LoadOptions,
67
+ ): Promise<LoadSourcesResult> {
62
68
  const entry = { group: 'parser' as const, label: 'init' };
63
69
 
64
70
  // 1. Bundle root documents together
@@ -78,6 +84,7 @@ export async function loadSources(
78
84
 
79
85
  try {
80
86
  const result = await bundle(sources, {
87
+ req,
81
88
  parse: transform ? transformer(transform) : undefined,
82
89
  yamlToMomoa,
83
90
  });
@@ -200,10 +207,7 @@ export async function loadSources(
200
207
  group.tokens.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
201
208
  }
202
209
 
203
- return {
204
- tokens: tokensSorted,
205
- sources,
206
- };
210
+ return { tokens: tokensSorted, sources };
207
211
  }
208
212
 
209
213
  function transformer(transform: TransformVisitors): BundleOptions['parse'] {
@@ -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,7 @@
1
+ import { loadResolver } from './load.js';
2
+
3
+ export * from './load.js';
4
+ export * from './normalize.js';
5
+ export * from './validate.js';
6
+
7
+ export default loadResolver;
@@ -0,0 +1,161 @@
1
+ import type { DocumentNode } from '@humanwhocodes/momoa';
2
+ import { maybeRawJSON } from '@terrazzo/json-schema-tools';
3
+ import type { TokenNormalizedSet } from '@terrazzo/token-tools';
4
+ import eq from 'fast-deep-equal';
5
+ import { merge } from 'merge-anything';
6
+ import type yamlToMomoa from 'yaml-to-momoa';
7
+ import { toMomoa } from '../lib/momoa.js';
8
+ import type Logger from '../logger.js';
9
+ import type { InputSource, Resolver, ResolverSourceNormalized } from '../types.js';
10
+ import { normalizeResolver } from './normalize.js';
11
+ import { isLikelyResolver, validateResolver } from './validate.js';
12
+
13
+ export interface LoadResolverOptions {
14
+ logger: Logger;
15
+ req: (url: URL, origin: URL) => Promise<string>;
16
+ yamlToMomoa?: typeof yamlToMomoa;
17
+ }
18
+
19
+ /** Quick-parse input sources and find a resolver */
20
+ export async function loadResolver(
21
+ inputs: Omit<InputSource, 'document'>[],
22
+ { logger, req, yamlToMomoa }: LoadResolverOptions,
23
+ ): Promise<Resolver | undefined> {
24
+ let resolverDoc: DocumentNode | undefined;
25
+ const entry = {
26
+ group: 'parser',
27
+ label: 'init',
28
+ } as const;
29
+
30
+ for (const input of inputs) {
31
+ let document: DocumentNode | undefined;
32
+ if (typeof input.src === 'string') {
33
+ if (maybeRawJSON(input.src)) {
34
+ document = toMomoa(input.src);
35
+ } else if (yamlToMomoa) {
36
+ document = yamlToMomoa(input.src);
37
+ } else {
38
+ logger.error({
39
+ ...entry,
40
+ message: `Install yaml-to-momoa package to parse YAML, and pass in as option, e.g.:
41
+
42
+ import { bundle } from '@terrazzo/json-schema-tools';
43
+ import yamlToMomoa from 'yaml-to-momoa';
44
+
45
+ bundle(yamlString, { yamlToMomoa });`,
46
+ });
47
+ }
48
+ } else if (input.src && typeof input.src === 'object') {
49
+ document = toMomoa(JSON.stringify(input.src, undefined, 2));
50
+ } else {
51
+ logger.error({ ...entry, message: `Could not parse ${input.filename}. Is this valid JSON or YAML?` });
52
+ }
53
+ if (!document || !isLikelyResolver(document)) {
54
+ continue;
55
+ }
56
+ if (inputs.length > 1) {
57
+ logger.error({ ...entry, message: `Resolver must be the only input, found ${inputs.length} sources.` });
58
+ }
59
+ resolverDoc = document;
60
+ break;
61
+ }
62
+
63
+ if (resolverDoc) {
64
+ validateResolver(resolverDoc, { logger, src: inputs[0]!.src });
65
+ const normalized = await normalizeResolver(resolverDoc, {
66
+ filename: inputs[0]!.filename!,
67
+ logger,
68
+ req,
69
+ src: inputs[0]!.src,
70
+ yamlToMomoa,
71
+ });
72
+ return createResolver(normalized, { logger });
73
+ }
74
+ }
75
+
76
+ export interface CreateResolverOptions {
77
+ logger: Logger;
78
+ }
79
+
80
+ /** Create an interface to resolve permutations */
81
+ export function createResolver(resolverSource: ResolverSourceNormalized, { logger }: CreateResolverOptions): Resolver {
82
+ 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
+ }
90
+ 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;
96
+ }
97
+ modifierPermutations.push([m.name, Object.keys(m.contexts)]);
98
+ }
99
+
100
+ const permutations = calculatePermutations(modifierPermutations);
101
+
102
+ return {
103
+ apply(inputRaw): TokenNormalizedSet {
104
+ let tokens: TokenNormalizedSet = {};
105
+ const input = { ...inputDefaults, ...inputRaw };
106
+ for (const item of resolverSource.resolutionOrder) {
107
+ switch (item.type) {
108
+ case 'set': {
109
+ for (const s of item.sources) {
110
+ tokens = merge(tokens, s) as TokenNormalizedSet;
111
+ }
112
+ break;
113
+ }
114
+ case 'modifier': {
115
+ const context = input[item.name]!;
116
+ const sources = item.contexts[context];
117
+ if (!sources) {
118
+ logger.error({
119
+ group: 'parser',
120
+ label: 'resolver',
121
+ message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`,
122
+ });
123
+ }
124
+ for (const s of sources ?? []) {
125
+ tokens = merge(tokens, s) as TokenNormalizedSet;
126
+ }
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ return tokens;
132
+ },
133
+ 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)}.` });
138
+ }
139
+ const input = { ...inputDefaults, ...inputRaw };
140
+ return permutations.findIndex((p) => eq(input, p)) !== -1;
141
+ },
142
+ };
143
+ }
144
+
145
+ /** Calculate all permutations */
146
+ export function calculatePermutations(options: [string, string[]][]) {
147
+ const permutationCount = [1];
148
+ for (const [_name, contexts] of options) {
149
+ permutationCount.push(contexts.length * (permutationCount.at(-1) || 1));
150
+ }
151
+ const permutations: Record<string, string>[] = [];
152
+ for (let i = 0; i < permutationCount.at(-1)!; i++) {
153
+ const input: Record<string, string> = {};
154
+ for (let j = 0; j < options.length; j++) {
155
+ const [name, contexts] = options[j]!;
156
+ input[name] = contexts[Math.floor(i / permutationCount[j]!) % contexts.length]!;
157
+ }
158
+ permutations.push(input);
159
+ }
160
+ return permutations.length ? permutations : [{}];
161
+ }
@@ -0,0 +1,99 @@
1
+ import { type ArrayNode, type DocumentNode, evaluate, type ObjectNode, type StringNode } 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: DocumentNode,
25
+ { filename, req, src, logger, yamlToMomoa }: NormalizeResolverOptions,
26
+ ): Promise<ResolverSourceNormalized> {
27
+ const resolverSource = evaluate(node) as unknown as ResolverSourceNormalized;
28
+ const resolutionOrder = getObjMember(node.body as ObjectNode, 'resolutionOrder') as 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 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 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
+ ...(resolvedItem as any), // Note: as long as this exists, this has already been validated to be correct
67
+ };
68
+ }
69
+ }
70
+ } else {
71
+ // 1b. remote $ref: load and validate
72
+ const result = await bundle(
73
+ [{ filename: new URL(url, filename), src: resolverSource.resolutionOrder[i]! }],
74
+ {
75
+ req,
76
+ yamlToMomoa,
77
+ },
78
+ );
79
+ if (result.document.body.type === 'Object') {
80
+ const type = getObjMember(result.document.body, 'type');
81
+ 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;
84
+ } 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;
87
+ }
88
+ }
89
+ logger.error({ ...entry, message: '$ref did not resolve to a valid Set or Modifier.' });
90
+ }
91
+ }
92
+
93
+ // 2. resolve inline sources & contexts
94
+ const finalResult = await bundle([{ filename, src: item }], { req, yamlToMomoa });
95
+ return evaluate(finalResult.document.body) as unknown as ResolverSetNormalized | ResolverModifierNormalized;
96
+ }),
97
+ ),
98
+ };
99
+ }