@terrazzo/parser 2.0.0-alpha.5 → 2.0.0-alpha.7
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 +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +213 -94
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/lint/plugin-core/rules/valid-border.ts +1 -1
- package/src/lint/plugin-core/rules/valid-color.ts +1 -1
- package/src/lint/plugin-core/rules/valid-font-weight.ts +1 -1
- package/src/lint/plugin-core/rules/valid-shadow.ts +1 -1
- package/src/lint/plugin-core/rules/valid-stroke-style.ts +3 -3
- package/src/lint/plugin-core/rules/valid-transition.ts +1 -1
- package/src/parse/load.ts +9 -8
- package/src/parse/process.ts +144 -16
- package/src/parse/token.ts +24 -10
- package/src/resolver/load.ts +0 -1
- package/src/resolver/normalize.ts +106 -79
- package/src/types.ts +8 -1
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
-
import { bundle,
|
|
2
|
+
import { bundle, encodeFragment, parseRef, replaceNode } from '@terrazzo/json-schema-tools';
|
|
3
3
|
import type yamlToMomoa from 'yaml-to-momoa';
|
|
4
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';
|
|
5
|
+
import type { Group, ReferenceObject, ResolverSourceNormalized } from '../types.js';
|
|
13
6
|
|
|
14
7
|
export interface NormalizeResolverOptions {
|
|
15
8
|
logger: Logger;
|
|
@@ -21,11 +14,46 @@ export interface NormalizeResolverOptions {
|
|
|
21
14
|
|
|
22
15
|
/** Normalize resolver (assuming it’s been validated) */
|
|
23
16
|
export async function normalizeResolver(
|
|
24
|
-
|
|
25
|
-
{ filename, req, src,
|
|
17
|
+
document: momoa.DocumentNode,
|
|
18
|
+
{ logger, filename, req, src, yamlToMomoa }: NormalizeResolverOptions,
|
|
26
19
|
): Promise<ResolverSourceNormalized> {
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
// Important note: think about sets, modifiers, and resolutionOrder all
|
|
21
|
+
// containing their own partial tokens documents. Now think about JSON $refs
|
|
22
|
+
// inside those. Because we want to treat them all as one _eventual_ document,
|
|
23
|
+
// we defer resolving $refs until the very last step. In most setups, this has
|
|
24
|
+
// no effect on the final result, however, in the scenario where remote
|
|
25
|
+
// documents are loaded and they conflict in unexpected ways, resolving too
|
|
26
|
+
// early will produce incorrect results.
|
|
27
|
+
//
|
|
28
|
+
// To prevent this, we bundle ONCE at the very top level, with the `$defs` at
|
|
29
|
+
// the top level now containing all partial documents (as opposed to bundling
|
|
30
|
+
// every sub document individually). So all that said, we are deciding to
|
|
31
|
+
// choose the “all-in-one“ method for closer support with DTCG aliases, but at
|
|
32
|
+
// the expense of some edge cases of $refs behaving unexpectedly.
|
|
33
|
+
const resolverBundle = await bundle([{ filename, src }], { req, yamlToMomoa });
|
|
34
|
+
const resolverSource = momoa.evaluate(resolverBundle.document) as unknown as ResolverSourceNormalized;
|
|
35
|
+
|
|
36
|
+
// Resolve $refs, but in a very different way than everywhere else These are
|
|
37
|
+
// all _evaluated_, meaning initialized in JS memory. Unlike in the AST, when
|
|
38
|
+
// we resolve these they’ll share memory points (which isn’t possible in the
|
|
39
|
+
// AST—values must be duplicated). This code is unique because it’s the only
|
|
40
|
+
// place where we’re dealing with shared, initialized JS memory.
|
|
41
|
+
replaceNode(document, resolverBundle.document); // inject $defs into the root document
|
|
42
|
+
for (const set of Object.values(resolverSource.sets ?? {})) {
|
|
43
|
+
for (const source of set.sources) {
|
|
44
|
+
resolvePartials(source, { resolver: resolverSource, logger });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const modifier of Object.values(resolverSource.modifiers ?? {})) {
|
|
48
|
+
for (const context of Object.values(modifier.contexts)) {
|
|
49
|
+
for (const source of context) {
|
|
50
|
+
resolvePartials(source, { resolver: resolverSource, logger });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const item of resolverSource.resolutionOrder ?? []) {
|
|
55
|
+
resolvePartials(item, { resolver: resolverSource, logger });
|
|
56
|
+
}
|
|
29
57
|
|
|
30
58
|
return {
|
|
31
59
|
name: resolverSource.name,
|
|
@@ -33,74 +61,73 @@ export async function normalizeResolver(
|
|
|
33
61
|
description: resolverSource.description,
|
|
34
62
|
sets: resolverSource.sets,
|
|
35
63
|
modifiers: resolverSource.modifiers,
|
|
36
|
-
resolutionOrder:
|
|
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
|
-
),
|
|
64
|
+
resolutionOrder: resolverSource.resolutionOrder,
|
|
101
65
|
_source: {
|
|
102
66
|
filename,
|
|
103
|
-
|
|
67
|
+
document,
|
|
104
68
|
},
|
|
105
69
|
};
|
|
106
70
|
}
|
|
71
|
+
|
|
72
|
+
/** Resolve $refs for already-initialized JS */
|
|
73
|
+
function resolvePartials(
|
|
74
|
+
source: Group | ReferenceObject,
|
|
75
|
+
{
|
|
76
|
+
resolver,
|
|
77
|
+
logger,
|
|
78
|
+
}: {
|
|
79
|
+
resolver: any;
|
|
80
|
+
logger: Logger;
|
|
81
|
+
},
|
|
82
|
+
) {
|
|
83
|
+
if (!source) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const entry = { group: 'parser' as const, label: 'resolver' };
|
|
87
|
+
if (Array.isArray(source)) {
|
|
88
|
+
for (const item of source) {
|
|
89
|
+
resolvePartials(item, { resolver, logger });
|
|
90
|
+
}
|
|
91
|
+
} else if (typeof source === 'object') {
|
|
92
|
+
for (const k of Object.keys(source)) {
|
|
93
|
+
if (k === '$ref') {
|
|
94
|
+
const $ref = (source as any)[k] as string;
|
|
95
|
+
const { url, subpath = [] } = parseRef($ref);
|
|
96
|
+
if (url !== '.' || !subpath.length) {
|
|
97
|
+
logger.error({ ...entry, message: `Could not load $ref ${JSON.stringify($ref)}` });
|
|
98
|
+
}
|
|
99
|
+
const found = findObject(resolver, subpath ?? [], logger);
|
|
100
|
+
if (subpath[0] === 'sets' || subpath[0] === 'modifiers') {
|
|
101
|
+
found.type = subpath[0].replace(/s$/, '');
|
|
102
|
+
found.name = subpath[1];
|
|
103
|
+
}
|
|
104
|
+
if (found) {
|
|
105
|
+
for (const k2 of Object.keys(found)) {
|
|
106
|
+
(source as any)[k2] = found[k2];
|
|
107
|
+
}
|
|
108
|
+
delete (source as any).$ref;
|
|
109
|
+
} else {
|
|
110
|
+
logger.error({ ...entry, message: `Could not find ${JSON.stringify($ref)}` });
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
resolvePartials((source as any)[k], { resolver, logger });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function findObject(dict: Record<string, any>, path: string[], logger: Logger): any {
|
|
120
|
+
let node = dict;
|
|
121
|
+
for (const idRaw of path) {
|
|
122
|
+
const id = idRaw.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
123
|
+
if (!(id in node)) {
|
|
124
|
+
logger.error({
|
|
125
|
+
group: 'parser',
|
|
126
|
+
label: 'resolver',
|
|
127
|
+
message: `Could not load $ref ${encodeFragment(path)}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
node = node[id];
|
|
131
|
+
}
|
|
132
|
+
return node;
|
|
133
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -377,7 +377,7 @@ export interface ResolverSourceNormalized {
|
|
|
377
377
|
resolutionOrder: (ResolverSetNormalized | ResolverModifierNormalized)[];
|
|
378
378
|
_source: {
|
|
379
379
|
filename?: URL;
|
|
380
|
-
|
|
380
|
+
document: momoa.DocumentNode;
|
|
381
381
|
};
|
|
382
382
|
}
|
|
383
383
|
|
|
@@ -459,3 +459,10 @@ export interface TransformHookOptions {
|
|
|
459
459
|
/** Momoa documents */
|
|
460
460
|
sources: InputSourceWithDocument[];
|
|
461
461
|
}
|
|
462
|
+
|
|
463
|
+
export interface RefMapEntry {
|
|
464
|
+
filename: string;
|
|
465
|
+
refChain: string[];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export type RefMap = Record<string, RefMapEntry>;
|