@terrazzo/parser 2.0.0-alpha.2 → 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.
- package/CHANGELOG.md +5 -12
- package/README.md +1 -1
- package/dist/index.d.ts +181 -57
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +877 -122
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/build/index.ts +2 -2
- package/src/config.ts +1 -2
- package/src/index.ts +2 -0
- package/src/lib/resolver-utils.ts +35 -0
- package/src/lint/index.ts +4 -3
- package/src/lint/plugin-core/index.ts +3 -4
- package/src/lint/plugin-core/lib/docs.ts +1 -1
- package/src/lint/plugin-core/rules/{no-type-on-alias.ts → required-type.ts} +5 -6
- package/src/parse/index.ts +51 -4
- package/src/parse/load.ts +25 -111
- package/src/parse/process.ts +124 -0
- package/src/parse/token.ts +12 -7
- package/src/resolver/create-synthetic-resolver.ts +86 -0
- package/src/resolver/index.ts +7 -0
- package/src/resolver/load.ts +216 -0
- package/src/resolver/normalize.ts +106 -0
- package/src/resolver/validate.ts +363 -0
- package/src/types.ts +113 -44
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrazzo/parser",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.4",
|
|
4
4
|
"description": "Parser/validator for the Design Tokens Community Group (DTCG) standard.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,26 +27,27 @@
|
|
|
27
27
|
".": "./dist/index.js",
|
|
28
28
|
"./package.json": "./package.json"
|
|
29
29
|
},
|
|
30
|
-
"homepage": "https://terrazzo.app/docs/
|
|
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.
|
|
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.
|
|
46
|
-
"@terrazzo/token-tools": "^2.0.0-alpha.
|
|
46
|
+
"@terrazzo/json-schema-tools": "^0.1.0-alpha.0",
|
|
47
|
+
"@terrazzo/token-tools": "^2.0.0-alpha.4"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
|
-
"yaml-to-momoa": "0.0.
|
|
50
|
+
"yaml-to-momoa": "0.0.8"
|
|
50
51
|
},
|
|
51
52
|
"scripts": {
|
|
52
53
|
"build": "rolldown -c && attw --profile esm-only --pack .",
|
package/src/build/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type { InputSourceWithDocument } from '@terrazzo/json-schema-tools';
|
|
2
2
|
import type { TokenNormalized } from '@terrazzo/token-tools';
|
|
3
3
|
import wcmatch from 'wildcard-match';
|
|
4
4
|
import Logger, { type LogEntry } from '../logger.js';
|
|
5
5
|
import type { BuildRunnerResult, ConfigInit, TokenTransformed, TransformParams } from '../types.js';
|
|
6
6
|
|
|
7
7
|
export interface BuildRunnerOptions {
|
|
8
|
-
sources:
|
|
8
|
+
sources: InputSourceWithDocument[];
|
|
9
9
|
config: ConfigInit;
|
|
10
10
|
logger?: Logger;
|
|
11
11
|
}
|
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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* If tokens are found inside a resolver, strip out the resolver paths (don’t
|
|
3
|
+
* include "sets"/"modifiers" in the token ID etc.)
|
|
4
|
+
*/
|
|
5
|
+
export function filterResolverPaths(path: string[]): string[] {
|
|
6
|
+
switch (path[0]) {
|
|
7
|
+
case 'sets': {
|
|
8
|
+
return path.slice(4);
|
|
9
|
+
}
|
|
10
|
+
case 'modifiers': {
|
|
11
|
+
return path.slice(5);
|
|
12
|
+
}
|
|
13
|
+
case 'resolutionOrder': {
|
|
14
|
+
switch (path[2]) {
|
|
15
|
+
case 'sources': {
|
|
16
|
+
return path.slice(4);
|
|
17
|
+
}
|
|
18
|
+
case 'contexts': {
|
|
19
|
+
return path.slice(5);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Make a deterministic string from an object
|
|
30
|
+
*/
|
|
31
|
+
export function makeInputKey(input: Record<string, string | undefined>): string {
|
|
32
|
+
return JSON.stringify(
|
|
33
|
+
Object.fromEntries(Object.entries(input).sort((a, b) => a[0].localeCompare(b[0], 'en-us', { numeric: true }))),
|
|
34
|
+
);
|
|
35
|
+
}
|
package/src/lint/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { InputSourceWithDocument } from '@terrazzo/json-schema-tools';
|
|
1
2
|
import { pluralize, type TokenNormalizedSet } from '@terrazzo/token-tools';
|
|
2
3
|
import { merge } from 'merge-anything';
|
|
3
4
|
import type { LogEntry, default as Logger } from '../logger.js';
|
|
4
|
-
import type { ConfigInit
|
|
5
|
+
import type { ConfigInit } from '../types.js';
|
|
5
6
|
|
|
6
7
|
export { RECOMMENDED_CONFIG } from './plugin-core/index.js';
|
|
7
8
|
|
|
@@ -9,7 +10,7 @@ export interface LintRunnerOptions {
|
|
|
9
10
|
tokens: TokenNormalizedSet;
|
|
10
11
|
filename?: URL;
|
|
11
12
|
config: ConfigInit;
|
|
12
|
-
sources:
|
|
13
|
+
sources: InputSourceWithDocument[];
|
|
13
14
|
logger: Logger;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -21,7 +22,7 @@ export default async function lintRunner({
|
|
|
21
22
|
logger,
|
|
22
23
|
}: LintRunnerOptions): Promise<void> {
|
|
23
24
|
const { plugins = [], lint } = config;
|
|
24
|
-
const sourceByFilename: Record<string,
|
|
25
|
+
const sourceByFilename: Record<string, InputSourceWithDocument> = {};
|
|
25
26
|
for (const source of sources) {
|
|
26
27
|
sourceByFilename[source.filename!.href] = source;
|
|
27
28
|
}
|
|
@@ -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,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
|
|
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]: '
|
|
11
|
+
[ERROR]: 'Token missing $type.',
|
|
13
12
|
},
|
|
14
13
|
docs: {
|
|
15
|
-
description: '
|
|
16
|
-
url: docsLink(
|
|
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 (
|
|
21
|
+
if (!t.originalValue?.$type) {
|
|
23
22
|
report({ messageId: ERROR, node: t.source.node, filename: t.source.filename });
|
|
24
23
|
}
|
|
25
24
|
}
|
package/src/parse/index.ts
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
+
import type fsType from 'node:fs/promises';
|
|
2
|
+
import type { InputSource, InputSourceWithDocument } from '@terrazzo/json-schema-tools';
|
|
1
3
|
import { pluralize, type TokenNormalizedSet } from '@terrazzo/token-tools';
|
|
2
4
|
import lintRunner from '../lint/index.js';
|
|
3
5
|
import Logger from '../logger.js';
|
|
4
|
-
import
|
|
6
|
+
import { createSyntheticResolver } from '../resolver/create-synthetic-resolver.js';
|
|
7
|
+
import { loadResolver } from '../resolver/load.js';
|
|
8
|
+
import type { ConfigInit, ParseOptions, Resolver } from '../types.js';
|
|
5
9
|
import { loadSources } from './load.js';
|
|
6
10
|
|
|
7
11
|
export interface ParseResult {
|
|
8
12
|
tokens: TokenNormalizedSet;
|
|
9
|
-
sources:
|
|
13
|
+
sources: InputSourceWithDocument[];
|
|
14
|
+
resolver: Resolver;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
/** Parse */
|
|
13
18
|
export default async function parse(
|
|
14
|
-
_input:
|
|
19
|
+
_input: InputSource | InputSource[],
|
|
15
20
|
{
|
|
16
21
|
logger = new Logger(),
|
|
22
|
+
req = defaultReq,
|
|
17
23
|
skipLint = false,
|
|
18
24
|
config = {} as ConfigInit,
|
|
19
25
|
continueOnError = false,
|
|
@@ -22,10 +28,33 @@ export default async function parse(
|
|
|
22
28
|
}: ParseOptions = {} as ParseOptions,
|
|
23
29
|
): Promise<ParseResult> {
|
|
24
30
|
const inputs = Array.isArray(_input) ? _input : [_input];
|
|
31
|
+
let tokens: TokenNormalizedSet = {};
|
|
32
|
+
let resolver: Resolver | undefined;
|
|
33
|
+
let sources: InputSourceWithDocument[] = [];
|
|
25
34
|
|
|
26
35
|
const totalStart = performance.now();
|
|
36
|
+
|
|
37
|
+
// 1. Load tokens
|
|
27
38
|
const initStart = performance.now();
|
|
28
|
-
const
|
|
39
|
+
const resolverResult = await loadResolver(inputs, { config, logger, req, yamlToMomoa });
|
|
40
|
+
// 1a. Resolver
|
|
41
|
+
if (resolverResult.resolver) {
|
|
42
|
+
tokens = resolverResult.tokens;
|
|
43
|
+
sources = resolverResult.sources;
|
|
44
|
+
resolver = resolverResult.resolver;
|
|
45
|
+
} else {
|
|
46
|
+
// 1b. No resolver
|
|
47
|
+
const tokenResult = await loadSources(inputs, {
|
|
48
|
+
req,
|
|
49
|
+
logger,
|
|
50
|
+
config,
|
|
51
|
+
continueOnError,
|
|
52
|
+
yamlToMomoa,
|
|
53
|
+
transform,
|
|
54
|
+
});
|
|
55
|
+
tokens = tokenResult.tokens;
|
|
56
|
+
sources = tokenResult.sources;
|
|
57
|
+
}
|
|
29
58
|
logger.debug({
|
|
30
59
|
message: 'Loaded tokens',
|
|
31
60
|
group: 'parser',
|
|
@@ -64,5 +93,23 @@ export default async function parse(
|
|
|
64
93
|
return {
|
|
65
94
|
tokens,
|
|
66
95
|
sources,
|
|
96
|
+
resolver: resolver || (await createSyntheticResolver(tokens, { config, logger, req, sources })),
|
|
67
97
|
};
|
|
68
98
|
}
|
|
99
|
+
|
|
100
|
+
let fs: typeof fsType | undefined;
|
|
101
|
+
|
|
102
|
+
/** Fallback req */
|
|
103
|
+
async function defaultReq(src: URL, _origin: URL) {
|
|
104
|
+
if (src.protocol === 'file:') {
|
|
105
|
+
if (!fs) {
|
|
106
|
+
fs = await import('node:fs/promises');
|
|
107
|
+
}
|
|
108
|
+
return await fs.readFile(src, 'utf8');
|
|
109
|
+
}
|
|
110
|
+
const res = await fetch(src);
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
throw new Error(`${src} responded with ${res.status}\n${await res.text()}`);
|
|
113
|
+
}
|
|
114
|
+
return await res.text();
|
|
115
|
+
}
|
package/src/parse/load.ts
CHANGED
|
@@ -3,23 +3,19 @@ import {
|
|
|
3
3
|
type BundleOptions,
|
|
4
4
|
bundle,
|
|
5
5
|
getObjMember,
|
|
6
|
+
type InputSource,
|
|
7
|
+
type InputSourceWithDocument,
|
|
6
8
|
type RefMap,
|
|
7
9
|
replaceNode,
|
|
8
|
-
|
|
10
|
+
traverse,
|
|
9
11
|
} from '@terrazzo/json-schema-tools';
|
|
10
|
-
import type {
|
|
12
|
+
import type { TokenNormalized, TokenNormalizedSet } from '@terrazzo/token-tools';
|
|
11
13
|
import { toMomoa } from '../lib/momoa.js';
|
|
14
|
+
import { filterResolverPaths } from '../lib/resolver-utils.js';
|
|
12
15
|
import type Logger from '../logger.js';
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
graphAliases,
|
|
17
|
-
groupFromNode,
|
|
18
|
-
refToTokenID,
|
|
19
|
-
resolveAliases,
|
|
20
|
-
tokenFromNode,
|
|
21
|
-
tokenRawValuesFromNode,
|
|
22
|
-
} from './token.js';
|
|
16
|
+
import { isLikelyResolver } from '../resolver/validate.js';
|
|
17
|
+
import type { ParseOptions, TransformVisitors } from '../types.js';
|
|
18
|
+
import { processTokens } from './process.js';
|
|
23
19
|
|
|
24
20
|
/** Ephemeral format that only exists while parsing the document. This is not confirmed to be DTCG yet. */
|
|
25
21
|
export interface IntermediaryToken {
|
|
@@ -51,14 +47,20 @@ export interface IntermediaryToken {
|
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
export interface LoadOptions extends Pick<ParseOptions, 'config' | 'continueOnError' | 'yamlToMomoa' | 'transform'> {
|
|
50
|
+
req: NonNullable<ParseOptions['req']>;
|
|
54
51
|
logger: Logger;
|
|
55
52
|
}
|
|
56
53
|
|
|
54
|
+
export interface LoadSourcesResult {
|
|
55
|
+
tokens: TokenNormalizedSet;
|
|
56
|
+
sources: InputSourceWithDocument[];
|
|
57
|
+
}
|
|
58
|
+
|
|
57
59
|
/** Load from multiple entries, while resolving remote files */
|
|
58
60
|
export async function loadSources(
|
|
59
|
-
inputs:
|
|
60
|
-
{ config, logger, continueOnError, yamlToMomoa, transform }: LoadOptions,
|
|
61
|
-
): Promise<
|
|
61
|
+
inputs: InputSource[],
|
|
62
|
+
{ config, logger, req, continueOnError, yamlToMomoa, transform }: LoadOptions,
|
|
63
|
+
): Promise<LoadSourcesResult> {
|
|
62
64
|
const entry = { group: 'parser' as const, label: 'init' };
|
|
63
65
|
|
|
64
66
|
// 1. Bundle root documents together
|
|
@@ -72,12 +74,13 @@ export async function loadSources(
|
|
|
72
74
|
filename: input.filename || new URL(`virtual:${i}`), // for objects created in memory, an index-based ID helps associate tokens with these
|
|
73
75
|
}));
|
|
74
76
|
/** The sources array, indexed by filename */
|
|
75
|
-
let sourceByFilename: Record<string,
|
|
77
|
+
let sourceByFilename: Record<string, InputSourceWithDocument> = {};
|
|
76
78
|
/** Mapping of all final $ref resolutions. This will be used to generate the graph later. */
|
|
77
79
|
let refMap: RefMap = {};
|
|
78
80
|
|
|
79
81
|
try {
|
|
80
82
|
const result = await bundle(sources, {
|
|
83
|
+
req,
|
|
81
84
|
parse: transform ? transformer(transform) : undefined,
|
|
82
85
|
yamlToMomoa,
|
|
83
86
|
});
|
|
@@ -106,102 +109,11 @@ export async function loadSources(
|
|
|
106
109
|
src,
|
|
107
110
|
});
|
|
108
111
|
}
|
|
109
|
-
|
|
110
112
|
logger.debug({ ...entry, message: `JSON loaded`, timing: performance.now() - firstLoad });
|
|
111
|
-
const artificialSource = { src: momoa.print(document, { indent: 2 }), document };
|
|
112
|
-
|
|
113
|
-
// 2. Parse
|
|
114
|
-
const firstPass = performance.now();
|
|
115
|
-
const tokens: TokenNormalizedSet = {};
|
|
116
|
-
// micro-optimization: while we’re iterating over tokens, keeping a “hot”
|
|
117
|
-
// array in memory saves recreating arrays from object keys over and over again.
|
|
118
|
-
// it does produce a noticeable speedup > 1,000 tokens.
|
|
119
|
-
const tokenIDs: string[] = [];
|
|
120
|
-
const groups: Record<string, GroupNormalized> = {};
|
|
121
|
-
|
|
122
|
-
// 2a. Token & group population
|
|
123
|
-
await traverseAsync(document, {
|
|
124
|
-
async enter(node, _parent, path) {
|
|
125
|
-
if (node.type !== 'Object') {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
groupFromNode(node, { path, groups });
|
|
129
|
-
const token = tokenFromNode(node, {
|
|
130
|
-
groups,
|
|
131
|
-
ignore: config.ignore,
|
|
132
|
-
path,
|
|
133
|
-
source: { src: artificialSource, document },
|
|
134
|
-
});
|
|
135
|
-
if (token) {
|
|
136
|
-
tokenIDs.push(token.jsonID);
|
|
137
|
-
tokens[token.jsonID] = token;
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
logger.debug({ ...entry, message: 'Parsing: 1st pass', timing: performance.now() - firstPass });
|
|
143
|
-
const secondPass = performance.now();
|
|
144
|
-
|
|
145
|
-
// 2b. Resolve originalValue and original sources
|
|
146
|
-
for (const source of Object.values(sourceByFilename)) {
|
|
147
|
-
await traverseAsync(source.document, {
|
|
148
|
-
async enter(node, _parent, path) {
|
|
149
|
-
if (node.type !== 'Object') {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const tokenRawValues = tokenRawValuesFromNode(node, { filename: source.filename!.href, path });
|
|
154
|
-
if (tokenRawValues && tokens[tokenRawValues?.jsonID]) {
|
|
155
|
-
tokens[tokenRawValues.jsonID]!.originalValue = tokenRawValues.originalValue;
|
|
156
|
-
tokens[tokenRawValues.jsonID]!.source = tokenRawValues.source;
|
|
157
|
-
for (const mode of Object.keys(tokenRawValues.mode)) {
|
|
158
|
-
tokens[tokenRawValues.jsonID]!.mode[mode]!.originalValue = tokenRawValues.mode[mode]!.originalValue;
|
|
159
|
-
tokens[tokenRawValues.jsonID]!.mode[mode]!.source = tokenRawValues.mode[mode]!.source;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// 2c. DTCG alias resolution
|
|
167
|
-
// Unlike $refs which can be resolved as we go, these can’t happen until the final, flattened set
|
|
168
|
-
resolveAliases(tokens, { logger, sources: sourceByFilename, refMap });
|
|
169
|
-
logger.debug({ ...entry, message: 'Parsing: 2nd pass', timing: performance.now() - secondPass });
|
|
170
|
-
|
|
171
|
-
// 3. Alias graph
|
|
172
|
-
// We’ve resolved aliases, but we need this pass for reverse linking i.e. “aliasedBy”
|
|
173
|
-
const aliasStart = performance.now();
|
|
174
|
-
graphAliases(refMap, { tokens, logger, sources: sourceByFilename });
|
|
175
|
-
logger.debug({ ...entry, message: 'Alias graph built', timing: performance.now() - aliasStart });
|
|
176
|
-
|
|
177
|
-
// 4. normalize
|
|
178
|
-
// Allow for some minor variance in inputs, and be nice to folks.
|
|
179
|
-
const normalizeStart = performance.now();
|
|
180
|
-
for (const id of tokenIDs) {
|
|
181
|
-
const token = tokens[id]!;
|
|
182
|
-
normalize(token as any, { logger, src: sourceByFilename[token.source.filename!]?.src });
|
|
183
|
-
}
|
|
184
|
-
logger.debug({ ...entry, message: 'Normalized values', timing: performance.now() - normalizeStart });
|
|
185
|
-
|
|
186
|
-
// 5. alphabetize & filter
|
|
187
|
-
// This can’t happen until the last step, where we’re 100% sure we’ve resolved everything.
|
|
188
|
-
const tokensSorted: TokenNormalizedSet = {};
|
|
189
|
-
tokenIDs.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
|
|
190
|
-
for (const path of tokenIDs) {
|
|
191
|
-
// Filter out any tokens in $defs (we needed to reference them earlier, but shouldn’t include them in the final assortment)
|
|
192
|
-
if (path.includes('/$defs/')) {
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
const id = refToTokenID(path)!;
|
|
196
|
-
tokensSorted[id] = tokens[path]!;
|
|
197
|
-
}
|
|
198
|
-
// Sort group IDs once, too
|
|
199
|
-
for (const group of Object.values(groups)) {
|
|
200
|
-
group.tokens.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
|
|
201
|
-
}
|
|
202
113
|
|
|
114
|
+
const rootSource = { filename: sources[0]!.filename!, document, src: momoa.print(document, { indent: 2 }) };
|
|
203
115
|
return {
|
|
204
|
-
tokens:
|
|
116
|
+
tokens: processTokens(rootSource, { config, logger, refMap, sources, sourceByFilename }),
|
|
205
117
|
sources,
|
|
206
118
|
};
|
|
207
119
|
}
|
|
@@ -219,8 +131,10 @@ function transformer(transform: TransformVisitors): BundleOptions['parse'] {
|
|
|
219
131
|
}
|
|
220
132
|
}
|
|
221
133
|
|
|
222
|
-
|
|
223
|
-
|
|
134
|
+
const isResolver = isLikelyResolver(document);
|
|
135
|
+
traverse(document, {
|
|
136
|
+
enter(node, parent, rawPath) {
|
|
137
|
+
const path = isResolver ? filterResolverPaths(rawPath) : rawPath;
|
|
224
138
|
if (node.type !== 'Object' || !path.length) {
|
|
225
139
|
return;
|
|
226
140
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type InputSourceWithDocument, type RefMap, traverse } from '@terrazzo/json-schema-tools';
|
|
2
|
+
import type { GroupNormalized, TokenNormalizedSet } from '@terrazzo/token-tools';
|
|
3
|
+
import { filterResolverPaths } from '../lib/resolver-utils.js';
|
|
4
|
+
import type Logger from '../logger.js';
|
|
5
|
+
import { isLikelyResolver } from '../resolver/validate.js';
|
|
6
|
+
import type { ConfigInit } from '../types.js';
|
|
7
|
+
import { normalize } from './normalize.js';
|
|
8
|
+
import {
|
|
9
|
+
graphAliases,
|
|
10
|
+
groupFromNode,
|
|
11
|
+
refToTokenID,
|
|
12
|
+
resolveAliases,
|
|
13
|
+
tokenFromNode,
|
|
14
|
+
tokenRawValuesFromNode,
|
|
15
|
+
} from './token.js';
|
|
16
|
+
|
|
17
|
+
export interface ProcessTokensOptions {
|
|
18
|
+
config: ConfigInit;
|
|
19
|
+
logger: Logger;
|
|
20
|
+
sourceByFilename: Record<string, InputSourceWithDocument>;
|
|
21
|
+
refMap: RefMap;
|
|
22
|
+
sources: InputSourceWithDocument[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function processTokens(
|
|
26
|
+
rootSource: InputSourceWithDocument,
|
|
27
|
+
{ config, logger, sourceByFilename, refMap }: ProcessTokensOptions,
|
|
28
|
+
): TokenNormalizedSet {
|
|
29
|
+
const entry = { group: 'parser' as const, label: 'init' };
|
|
30
|
+
|
|
31
|
+
// 2. Parse
|
|
32
|
+
const firstPass = performance.now();
|
|
33
|
+
const tokens: TokenNormalizedSet = {};
|
|
34
|
+
// micro-optimization: while we’re iterating over tokens, keeping a “hot”
|
|
35
|
+
// array in memory saves recreating arrays from object keys over and over again.
|
|
36
|
+
// it does produce a noticeable speedup > 1,000 tokens.
|
|
37
|
+
const tokenIDs: string[] = [];
|
|
38
|
+
const groups: Record<string, GroupNormalized> = {};
|
|
39
|
+
|
|
40
|
+
// 2a. Token & group population
|
|
41
|
+
const isResolver = isLikelyResolver(rootSource.document);
|
|
42
|
+
traverse(rootSource.document, {
|
|
43
|
+
enter(node, _parent, rawPath) {
|
|
44
|
+
if (node.type !== 'Object') {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const path = isResolver ? filterResolverPaths(rawPath) : rawPath;
|
|
48
|
+
groupFromNode(node, { path, groups });
|
|
49
|
+
const token = tokenFromNode(node, {
|
|
50
|
+
groups,
|
|
51
|
+
ignore: config.ignore,
|
|
52
|
+
path,
|
|
53
|
+
source: rootSource,
|
|
54
|
+
});
|
|
55
|
+
if (token) {
|
|
56
|
+
tokenIDs.push(token.jsonID);
|
|
57
|
+
tokens[token.jsonID] = token;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
logger.debug({ ...entry, message: 'Parsing: 1st pass', timing: performance.now() - firstPass });
|
|
63
|
+
const secondPass = performance.now();
|
|
64
|
+
|
|
65
|
+
// 2b. Resolve originalValue and original sources
|
|
66
|
+
for (const source of Object.values(sourceByFilename)) {
|
|
67
|
+
traverse(source.document, {
|
|
68
|
+
enter(node, _parent, path) {
|
|
69
|
+
if (node.type !== 'Object') {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const tokenRawValues = tokenRawValuesFromNode(node, { filename: source.filename!.href, path });
|
|
74
|
+
if (tokenRawValues && tokens[tokenRawValues?.jsonID]) {
|
|
75
|
+
tokens[tokenRawValues.jsonID]!.originalValue = tokenRawValues.originalValue;
|
|
76
|
+
tokens[tokenRawValues.jsonID]!.source = tokenRawValues.source;
|
|
77
|
+
for (const mode of Object.keys(tokenRawValues.mode)) {
|
|
78
|
+
tokens[tokenRawValues.jsonID]!.mode[mode]!.originalValue = tokenRawValues.mode[mode]!.originalValue;
|
|
79
|
+
tokens[tokenRawValues.jsonID]!.mode[mode]!.source = tokenRawValues.mode[mode]!.source;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2c. DTCG alias resolution
|
|
87
|
+
// Unlike $refs which can be resolved as we go, these can’t happen until the final, flattened set
|
|
88
|
+
resolveAliases(tokens, { logger, sources: sourceByFilename, refMap });
|
|
89
|
+
logger.debug({ ...entry, message: 'Parsing: 2nd pass', timing: performance.now() - secondPass });
|
|
90
|
+
|
|
91
|
+
// 3. Alias graph
|
|
92
|
+
// We’ve resolved aliases, but we need this pass for reverse linking i.e. “aliasedBy”
|
|
93
|
+
const aliasStart = performance.now();
|
|
94
|
+
graphAliases(refMap, { tokens, logger, sources: sourceByFilename });
|
|
95
|
+
logger.debug({ ...entry, message: 'Alias graph built', timing: performance.now() - aliasStart });
|
|
96
|
+
|
|
97
|
+
// 4. normalize
|
|
98
|
+
// Allow for some minor variance in inputs, and be nice to folks.
|
|
99
|
+
const normalizeStart = performance.now();
|
|
100
|
+
for (const id of tokenIDs) {
|
|
101
|
+
const token = tokens[id]!;
|
|
102
|
+
normalize(token as any, { logger, src: sourceByFilename[token.source.filename!]?.src });
|
|
103
|
+
}
|
|
104
|
+
logger.debug({ ...entry, message: 'Normalized values', timing: performance.now() - normalizeStart });
|
|
105
|
+
|
|
106
|
+
// 5. alphabetize & filter
|
|
107
|
+
// This can’t happen until the last step, where we’re 100% sure we’ve resolved everything.
|
|
108
|
+
const tokensSorted: TokenNormalizedSet = {};
|
|
109
|
+
tokenIDs.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
|
|
110
|
+
for (const path of tokenIDs) {
|
|
111
|
+
// Filter out any tokens in $defs (we needed to reference them earlier, but shouldn’t include them in the final assortment)
|
|
112
|
+
if (path.includes('/$defs/')) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const id = refToTokenID(path)!;
|
|
116
|
+
tokensSorted[id] = tokens[path]!;
|
|
117
|
+
}
|
|
118
|
+
// Sort group IDs once, too
|
|
119
|
+
for (const group of Object.values(groups)) {
|
|
120
|
+
group.tokens.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return tokensSorted;
|
|
124
|
+
}
|