@terrazzo/parser 0.10.4 → 2.0.0-alpha.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.
- package/dist/index.d.ts +82 -307
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2186 -3621
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/build/index.ts +32 -41
- package/src/config.ts +13 -6
- package/src/lib/code-frame.ts +3 -0
- package/src/lib/momoa.ts +10 -0
- package/src/lint/index.ts +41 -37
- package/src/lint/plugin-core/index.ts +73 -16
- package/src/lint/plugin-core/rules/colorspace.ts +4 -0
- package/src/lint/plugin-core/rules/duplicate-values.ts +2 -0
- package/src/lint/plugin-core/rules/max-gamut.ts +24 -4
- package/src/lint/plugin-core/rules/no-type-on-alias.ts +29 -0
- package/src/lint/plugin-core/rules/required-modes.ts +2 -0
- package/src/lint/plugin-core/rules/required-typography-properties.ts +13 -3
- package/src/lint/plugin-core/rules/valid-boolean.ts +41 -0
- package/src/lint/plugin-core/rules/valid-border.ts +57 -0
- package/src/lint/plugin-core/rules/valid-color.ts +265 -0
- package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +83 -0
- package/src/lint/plugin-core/rules/valid-dimension.ts +199 -0
- package/src/lint/plugin-core/rules/valid-duration.ts +123 -0
- package/src/lint/plugin-core/rules/valid-font-family.ts +68 -0
- package/src/lint/plugin-core/rules/valid-font-weight.ts +89 -0
- package/src/lint/plugin-core/rules/valid-gradient.ts +79 -0
- package/src/lint/plugin-core/rules/valid-link.ts +41 -0
- package/src/lint/plugin-core/rules/valid-number.ts +63 -0
- package/src/lint/plugin-core/rules/valid-shadow.ts +67 -0
- package/src/lint/plugin-core/rules/valid-string.ts +41 -0
- package/src/lint/plugin-core/rules/valid-stroke-style.ts +104 -0
- package/src/lint/plugin-core/rules/valid-transition.ts +61 -0
- package/src/lint/plugin-core/rules/valid-typography.ts +67 -0
- package/src/logger.ts +70 -59
- package/src/parse/index.ts +23 -328
- package/src/parse/load.ts +257 -0
- package/src/parse/normalize.ts +134 -170
- package/src/parse/token.ts +530 -0
- package/src/types.ts +76 -10
- package/src/parse/alias.ts +0 -369
- package/src/parse/json.ts +0 -211
- package/src/parse/validate.ts +0 -961
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import { TRANSITION_REQUIRED_PROPERTIES } from '@terrazzo/token-tools';
|
|
4
|
+
import type { LintRule } from '../../../types.js';
|
|
5
|
+
import { docsLink } from '../lib/docs.js';
|
|
6
|
+
|
|
7
|
+
export const VALID_TRANSITION = 'core/valid-transition';
|
|
8
|
+
|
|
9
|
+
const ERROR = 'ERROR';
|
|
10
|
+
const ERROR_INVALID_PROP = 'ERROR_INVALID_PROP';
|
|
11
|
+
|
|
12
|
+
const rule: LintRule<typeof ERROR | typeof ERROR_INVALID_PROP> = {
|
|
13
|
+
meta: {
|
|
14
|
+
messages: {
|
|
15
|
+
[ERROR]: `Missing required properties: ${new Intl.ListFormat(undefined, { type: 'conjunction' }).format(TRANSITION_REQUIRED_PROPERTIES)}.`,
|
|
16
|
+
[ERROR_INVALID_PROP]: 'Unknown property: {{ key }}.',
|
|
17
|
+
},
|
|
18
|
+
docs: {
|
|
19
|
+
description: 'Require transition tokens to follow the format.',
|
|
20
|
+
url: docsLink(VALID_TRANSITION),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultOptions: {},
|
|
24
|
+
create({ tokens, report }) {
|
|
25
|
+
for (const t of Object.values(tokens)) {
|
|
26
|
+
if (t.aliasOf || !t.originalValue || t.$type !== 'transition') {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
validateTransition(t.originalValue.$value, {
|
|
31
|
+
node: getObjMember(t.source.node, '$value') as momoa.ObjectNode,
|
|
32
|
+
filename: t.source.filename,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Note: we validate sub-properties using other checks like valid-dimension, valid-font-family, etc.
|
|
37
|
+
// The only thing remaining is to check that all properties exist (since missing properties won’t appear as invalid)
|
|
38
|
+
function validateTransition(value: unknown, { node, filename }: { node: momoa.ObjectNode; filename?: string }) {
|
|
39
|
+
if (
|
|
40
|
+
!value ||
|
|
41
|
+
typeof value !== 'object' ||
|
|
42
|
+
!TRANSITION_REQUIRED_PROPERTIES.every((property) => property in value)
|
|
43
|
+
) {
|
|
44
|
+
report({ messageId: ERROR, node, filename });
|
|
45
|
+
} else {
|
|
46
|
+
for (const key of Object.keys(value)) {
|
|
47
|
+
if (!TRANSITION_REQUIRED_PROPERTIES.includes(key as (typeof TRANSITION_REQUIRED_PROPERTIES)[number])) {
|
|
48
|
+
report({
|
|
49
|
+
messageId: ERROR_INVALID_PROP,
|
|
50
|
+
data: { key: JSON.stringify(key) },
|
|
51
|
+
node: getObjMember(node, key),
|
|
52
|
+
filename,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default rule;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import wcmatch from 'wildcard-match';
|
|
4
|
+
import type { LintRule } from '../../../types.js';
|
|
5
|
+
import { docsLink } from '../lib/docs.js';
|
|
6
|
+
|
|
7
|
+
export const VALID_TYPOGRAPHY = 'core/valid-typography';
|
|
8
|
+
|
|
9
|
+
const ERROR = 'ERROR';
|
|
10
|
+
const ERROR_MISSING = 'ERROR_MISSING';
|
|
11
|
+
|
|
12
|
+
export interface RuleRequiredTypographyPropertiesOptions {
|
|
13
|
+
/** Required typography properties */
|
|
14
|
+
requiredProperties: string[];
|
|
15
|
+
/** Token globs to ignore */
|
|
16
|
+
ignore?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const rule: LintRule<typeof ERROR | typeof ERROR_MISSING, RuleRequiredTypographyPropertiesOptions> = {
|
|
20
|
+
meta: {
|
|
21
|
+
messages: {
|
|
22
|
+
[ERROR]: `Expected object, received {{ value }}.`,
|
|
23
|
+
[ERROR_MISSING]: `Missing required property "{{ property }}".`,
|
|
24
|
+
},
|
|
25
|
+
docs: {
|
|
26
|
+
description: 'Require typography tokens to follow the format.',
|
|
27
|
+
url: docsLink(VALID_TYPOGRAPHY),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultOptions: {
|
|
31
|
+
requiredProperties: ['fontFamily', 'fontSize', 'fontWeight', 'letterSpacing', 'lineHeight'],
|
|
32
|
+
},
|
|
33
|
+
create({ tokens, options, report }) {
|
|
34
|
+
const isIgnored = options.ignore ? wcmatch(options.ignore) : () => false;
|
|
35
|
+
for (const t of Object.values(tokens)) {
|
|
36
|
+
if (t.aliasOf || !t.originalValue || t.$type !== 'typography' || isIgnored(t.id)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
validateTypography(t.originalValue.$value, {
|
|
41
|
+
node: getObjMember(t.source.node, '$value') as momoa.ObjectNode,
|
|
42
|
+
filename: t.source.filename,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Note: we validate sub-properties using other checks like valid-dimension, valid-font-family, etc.
|
|
46
|
+
// The only thing remaining is to check that all properties exist (since missing properties won’t appear as invalid)
|
|
47
|
+
function validateTypography(value: unknown, { node, filename }: { node: momoa.ObjectNode; filename?: string }) {
|
|
48
|
+
if (value && typeof value === 'object') {
|
|
49
|
+
for (const property of options.requiredProperties) {
|
|
50
|
+
if (!(property in value)) {
|
|
51
|
+
report({ messageId: ERROR_MISSING, data: { property }, node, filename });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
report({
|
|
56
|
+
messageId: ERROR,
|
|
57
|
+
data: { value: JSON.stringify(value) },
|
|
58
|
+
node,
|
|
59
|
+
filename,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default rule;
|
package/src/logger.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as momoa from '@humanwhocodes/momoa';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import wcmatch from 'wildcard-match';
|
|
4
4
|
import { codeFrameColumns } from './lib/code-frame.js';
|
|
5
5
|
|
|
6
6
|
export const LOG_ORDER = ['error', 'warn', 'info', 'debug'] as const;
|
|
7
|
-
|
|
8
7
|
export type LogSeverity = 'error' | 'warn' | 'info' | 'debug';
|
|
9
|
-
|
|
10
8
|
export type LogLevel = LogSeverity | 'silent';
|
|
11
|
-
|
|
12
9
|
export type LogGroup = 'config' | 'parser' | 'lint' | 'plugin' | 'server';
|
|
13
10
|
|
|
14
11
|
export interface LogEntry {
|
|
@@ -26,7 +23,7 @@ export interface LogEntry {
|
|
|
26
23
|
*/
|
|
27
24
|
continueOnError?: boolean;
|
|
28
25
|
/** Show a code frame for the erring node */
|
|
29
|
-
node?: AnyNode;
|
|
26
|
+
node?: momoa.AnyNode;
|
|
30
27
|
/** To show a code frame, provide the original source code */
|
|
31
28
|
src?: string;
|
|
32
29
|
}
|
|
@@ -64,13 +61,17 @@ export function formatMessage(entry: LogEntry, severity: LogSeverity) {
|
|
|
64
61
|
if (severity in MESSAGE_COLOR) {
|
|
65
62
|
message = MESSAGE_COLOR[severity]!(message);
|
|
66
63
|
}
|
|
67
|
-
if (entry.
|
|
64
|
+
if (entry.node) {
|
|
68
65
|
const start = entry.node?.loc?.start ?? { line: 0, column: 0 };
|
|
69
66
|
// strip "file://" protocol, but not href
|
|
70
67
|
const loc = entry.filename
|
|
71
68
|
? `${entry.filename?.href.replace(/^file:\/\//, '')}:${start?.line ?? 0}:${start?.column ?? 0}\n\n`
|
|
72
69
|
: '';
|
|
73
|
-
const codeFrame = codeFrameColumns(
|
|
70
|
+
const codeFrame = codeFrameColumns(
|
|
71
|
+
entry.src ?? momoa.print(entry.node, { indent: 2 }),
|
|
72
|
+
{ start },
|
|
73
|
+
{ highlightCode: false },
|
|
74
|
+
);
|
|
74
75
|
message = `${message}\n\n${loc}${codeFrame}`;
|
|
75
76
|
}
|
|
76
77
|
return message;
|
|
@@ -98,77 +99,87 @@ export default class Logger {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/** Log an error message (always; can’t be silenced) */
|
|
101
|
-
error(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
error(...entries: LogEntry[]) {
|
|
103
|
+
const message: string[] = [];
|
|
104
|
+
let firstNode: momoa.AnyNode | undefined;
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
this.errorCount++;
|
|
107
|
+
message.push(formatMessage(entry, 'error'));
|
|
108
|
+
if (entry.node) {
|
|
109
|
+
firstNode = entry.node;
|
|
110
|
+
}
|
|
108
111
|
}
|
|
109
|
-
if (
|
|
110
|
-
|
|
112
|
+
if (entries.every((e) => e.continueOnError)) {
|
|
113
|
+
// biome-ignore lint/suspicious/noConsole: this is a logger
|
|
114
|
+
console.error(message.join('\n\n'));
|
|
111
115
|
} else {
|
|
112
|
-
|
|
116
|
+
const e = firstNode ? new TokensJSONError(message.join('\n\n')) : new Error(message.join('\n\n'));
|
|
117
|
+
throw e;
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
/** Log an info message (if logging level permits) */
|
|
117
|
-
info(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
info(...entries: LogEntry[]) {
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
this.infoCount++;
|
|
125
|
+
if (this.level === 'silent' || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf('info')) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const message = formatMessage(entry, 'info');
|
|
129
|
+
// biome-ignore lint/suspicious/noConsole: this is a logger
|
|
130
|
+
console.log(message);
|
|
121
131
|
}
|
|
122
|
-
const message = formatMessage(entry, 'info');
|
|
123
|
-
// biome-ignore lint/suspicious/noConsole: this is a logger
|
|
124
|
-
console.log(message);
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
/** Log a warning message (if logging level permits) */
|
|
128
|
-
warn(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
warn(...entries: LogEntry[]) {
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
this.warnCount++;
|
|
138
|
+
if (this.level === 'silent' || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf('warn')) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const message = formatMessage(entry, 'warn');
|
|
142
|
+
// biome-ignore lint/suspicious/noConsole: this is a logger
|
|
143
|
+
console.warn(message);
|
|
132
144
|
}
|
|
133
|
-
const message = formatMessage(entry, 'warn');
|
|
134
|
-
// biome-ignore lint/suspicious/noConsole: this is a logger
|
|
135
|
-
console.warn(message);
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
/** Log a diagnostics message (if logging level permits) */
|
|
139
|
-
debug(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
debug(...entries: DebugEntry[]) {
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
if (this.level === 'silent' || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf('debug')) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.debugCount++;
|
|
144
154
|
|
|
145
|
-
|
|
155
|
+
let message = formatMessage(entry, 'debug');
|
|
146
156
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
const debugPrefix = entry.label ? `${entry.group}:${entry.label}` : entry.group;
|
|
158
|
+
if (this.debugScope !== '*' && !wcmatch(this.debugScope)(debugPrefix)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
151
161
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
// debug color
|
|
163
|
+
message
|
|
164
|
+
.replace(/\[config[^\]]+\]/, (match) => pc.green(match))
|
|
165
|
+
.replace(/\[parser[^\]]+\]/, (match) => pc.magenta(match))
|
|
166
|
+
.replace(/\[lint[^\]]+\]/, (match) => pc.yellow(match))
|
|
167
|
+
.replace(/\[plugin[^\]]+\]/, (match) => pc.cyan(match));
|
|
168
|
+
|
|
169
|
+
message = `${pc.dim(timeFormatter.format(performance.now()))} ${message}`;
|
|
170
|
+
if (typeof entry.timing === 'number') {
|
|
171
|
+
let timing = '';
|
|
172
|
+
if (entry.timing < 1_000) {
|
|
173
|
+
timing = `${Math.round(entry.timing * 100) / 100}ms`;
|
|
174
|
+
} else if (entry.timing < 60_000) {
|
|
175
|
+
timing = `${Math.round(entry.timing * 100) / 100_000}s`;
|
|
176
|
+
}
|
|
177
|
+
message = `${message}${timing ? pc.dim(` [${timing}]`) : ''}`;
|
|
166
178
|
}
|
|
167
|
-
message = `${message} ${pc.dim(`[${timing}]`)}`;
|
|
168
|
-
}
|
|
169
179
|
|
|
170
|
-
|
|
171
|
-
|
|
180
|
+
// biome-ignore lint/suspicious/noConsole: this is a logger
|
|
181
|
+
console.log(message);
|
|
182
|
+
}
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
/** Get stats for current logger instance */
|
package/src/parse/index.ts
CHANGED
|
@@ -1,46 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { pluralize, splitID, type Token, type TokenNormalized } from '@terrazzo/token-tools';
|
|
3
|
-
import type ytm from 'yaml-to-momoa';
|
|
1
|
+
import { pluralize, type TokenNormalizedSet } from '@terrazzo/token-tools';
|
|
4
2
|
import lintRunner from '../lint/index.js';
|
|
5
3
|
import Logger from '../logger.js';
|
|
6
|
-
import type { ConfigInit, InputSource } from '../types.js';
|
|
7
|
-
import
|
|
8
|
-
import { getObjMembers, parseJSON, replaceObjMembers, toMomoa, traverse } from './json.js';
|
|
9
|
-
import normalize from './normalize.js';
|
|
10
|
-
import validateTokenNode, { computeInheritedProperty, isGroupNode, type Visitors } from './validate.js';
|
|
11
|
-
|
|
12
|
-
export * from './alias.js';
|
|
13
|
-
export * from './json.js';
|
|
14
|
-
export * from './normalize.js';
|
|
15
|
-
export * from './validate.js';
|
|
16
|
-
export { normalize, validateTokenNode };
|
|
17
|
-
|
|
18
|
-
export interface ParseOptions {
|
|
19
|
-
logger?: Logger;
|
|
20
|
-
config: ConfigInit;
|
|
21
|
-
/**
|
|
22
|
-
* Skip lint step
|
|
23
|
-
* @default false
|
|
24
|
-
*/
|
|
25
|
-
skipLint?: boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Continue on error? (Useful for `tz check`)
|
|
28
|
-
* @default false
|
|
29
|
-
*/
|
|
30
|
-
continueOnError?: boolean;
|
|
31
|
-
/** Provide yamlToMomoa module to parse YAML (by default, this isn’t shipped to cut down on package weight) */
|
|
32
|
-
yamlToMomoa?: typeof ytm;
|
|
33
|
-
/**
|
|
34
|
-
* Transform API
|
|
35
|
-
* @see https://terrazzo.app/docs/api/js#transform-api
|
|
36
|
-
*/
|
|
37
|
-
transform?: Visitors;
|
|
38
|
-
/** (internal cache; do not use) */
|
|
39
|
-
_sources?: Record<string, InputSource>;
|
|
40
|
-
}
|
|
4
|
+
import type { ConfigInit, InputSource, ParseOptions } from '../types.js';
|
|
5
|
+
import { loadSources } from './load.js';
|
|
41
6
|
|
|
42
7
|
export interface ParseResult {
|
|
43
|
-
tokens:
|
|
8
|
+
tokens: TokenNormalizedSet;
|
|
44
9
|
sources: InputSource[];
|
|
45
10
|
}
|
|
46
11
|
|
|
@@ -54,93 +19,31 @@ export default async function parse(
|
|
|
54
19
|
continueOnError = false,
|
|
55
20
|
yamlToMomoa,
|
|
56
21
|
transform,
|
|
57
|
-
_sources = {},
|
|
58
22
|
}: ParseOptions = {} as ParseOptions,
|
|
59
23
|
): Promise<ParseResult> {
|
|
60
|
-
const
|
|
61
|
-
let tokensSet: Record<string, TokenNormalized> = {};
|
|
62
|
-
|
|
63
|
-
if (!Array.isArray(input)) {
|
|
64
|
-
logger.error({ group: 'parser', label: 'init', message: 'Input must be an array of input objects.' });
|
|
65
|
-
}
|
|
66
|
-
await Promise.all(
|
|
67
|
-
input.map(async (src, i) => {
|
|
68
|
-
if (!src || typeof src !== 'object') {
|
|
69
|
-
logger.error({ group: 'parser', label: 'init', message: `Input (${i}) must be an object.` });
|
|
70
|
-
}
|
|
71
|
-
if (!src.src || (typeof src.src !== 'string' && typeof src.src !== 'object')) {
|
|
72
|
-
logger.error({
|
|
73
|
-
message: `Input (${i}) missing "src" with a JSON/YAML string, or JSON object.`,
|
|
74
|
-
group: 'parser',
|
|
75
|
-
label: 'init',
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
if (src.filename) {
|
|
79
|
-
if (!(src.filename instanceof URL)) {
|
|
80
|
-
logger.error({
|
|
81
|
-
message: `Input (${i}) "filename" must be a URL (remote or file URL).`,
|
|
82
|
-
group: 'parser',
|
|
83
|
-
label: 'init',
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// if already parsed/scanned, skip
|
|
88
|
-
if (_sources[src.filename.href]) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const result = await parseSingle(src.src, {
|
|
94
|
-
filename: src.filename!,
|
|
95
|
-
logger,
|
|
96
|
-
config,
|
|
97
|
-
skipLint,
|
|
98
|
-
continueOnError,
|
|
99
|
-
yamlToMomoa,
|
|
100
|
-
transform,
|
|
101
|
-
});
|
|
102
|
-
tokensSet = Object.assign(tokensSet, result.tokens);
|
|
103
|
-
if (src.filename) {
|
|
104
|
-
_sources[src.filename.href] = {
|
|
105
|
-
filename: src.filename,
|
|
106
|
-
src: result.src,
|
|
107
|
-
document: result.document,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}),
|
|
111
|
-
);
|
|
24
|
+
const inputs = Array.isArray(_input) ? _input : [_input];
|
|
112
25
|
|
|
113
26
|
const totalStart = performance.now();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const aliasesStart = performance.now();
|
|
117
|
-
let aliasCount = 0;
|
|
118
|
-
for (const [id, token] of Object.entries(tokensSet)) {
|
|
119
|
-
applyAliases(token, {
|
|
120
|
-
tokensSet,
|
|
121
|
-
filename: _sources[token.source.loc!]?.filename!,
|
|
122
|
-
src: _sources[token.source.loc!]?.src as string,
|
|
123
|
-
node: (getObjMembers(token.source.node).$value as any) || token.source.node,
|
|
124
|
-
logger,
|
|
125
|
-
});
|
|
126
|
-
aliasCount++;
|
|
127
|
-
const { group: parentGroup } = splitID(id);
|
|
128
|
-
if (parentGroup) {
|
|
129
|
-
for (const siblingID of Object.keys(tokensSet)) {
|
|
130
|
-
const { group: siblingGroup } = splitID(siblingID);
|
|
131
|
-
if (siblingGroup?.startsWith(parentGroup)) {
|
|
132
|
-
token.group.tokens.push(siblingID);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
27
|
+
const initStart = performance.now();
|
|
28
|
+
const { tokens, sources } = await loadSources(inputs, { logger, config, continueOnError, yamlToMomoa, transform });
|
|
137
29
|
logger.debug({
|
|
138
|
-
message:
|
|
30
|
+
message: 'Loaded tokens',
|
|
139
31
|
group: 'parser',
|
|
140
|
-
label: '
|
|
141
|
-
timing: performance.now() -
|
|
32
|
+
label: 'core',
|
|
33
|
+
timing: performance.now() - initStart,
|
|
142
34
|
});
|
|
143
35
|
|
|
36
|
+
if (skipLint !== true && config?.plugins?.length) {
|
|
37
|
+
const lintStart = performance.now();
|
|
38
|
+
await lintRunner({ tokens, sources, config, logger });
|
|
39
|
+
logger.debug({
|
|
40
|
+
message: 'Lint finished',
|
|
41
|
+
group: 'plugin',
|
|
42
|
+
label: 'lint',
|
|
43
|
+
timing: performance.now() - lintStart,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
144
47
|
logger.debug({
|
|
145
48
|
message: 'Finish all parser tasks',
|
|
146
49
|
group: 'parser',
|
|
@@ -159,215 +62,7 @@ export default async function parse(
|
|
|
159
62
|
}
|
|
160
63
|
|
|
161
64
|
return {
|
|
162
|
-
tokens
|
|
163
|
-
sources
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Parse a single input */
|
|
168
|
-
async function parseSingle(
|
|
169
|
-
input: string | Record<string, any>,
|
|
170
|
-
{
|
|
171
|
-
filename,
|
|
172
|
-
logger,
|
|
173
|
-
config,
|
|
174
|
-
skipLint,
|
|
175
|
-
continueOnError = false,
|
|
176
|
-
transform,
|
|
177
|
-
yamlToMomoa, // optional dependency, declared here so the parser itself doesn’t have to load a heavy dep in-browser
|
|
178
|
-
}: {
|
|
179
|
-
filename: URL;
|
|
180
|
-
logger: Logger;
|
|
181
|
-
config: ConfigInit;
|
|
182
|
-
skipLint: boolean;
|
|
183
|
-
continueOnError?: boolean;
|
|
184
|
-
transform: ParseOptions['transform'] | undefined;
|
|
185
|
-
yamlToMomoa?: typeof ytm;
|
|
186
|
-
},
|
|
187
|
-
): Promise<{ tokens: Record<string, Token>; document: DocumentNode; src?: string }> {
|
|
188
|
-
// 1. Build AST
|
|
189
|
-
const startParsing = performance.now();
|
|
190
|
-
let { src, document } = toMomoa(input, { filename, logger, continueOnError, yamlToMomoa });
|
|
191
|
-
logger.debug({
|
|
192
|
-
group: 'parser',
|
|
193
|
-
label: 'json',
|
|
194
|
-
message: 'Finish JSON parsing',
|
|
195
|
-
timing: performance.now() - startParsing,
|
|
196
|
-
});
|
|
197
|
-
const tokensSet: Record<string, TokenNormalized> = {};
|
|
198
|
-
|
|
199
|
-
// 1a. if there’s a root() transformer, then re-parse
|
|
200
|
-
if (transform?.root) {
|
|
201
|
-
const json = typeof input === 'string' ? JSON.parse(input) : input;
|
|
202
|
-
const result = transform?.root(json, '.', document);
|
|
203
|
-
if (result) {
|
|
204
|
-
const reRunResult = toMomoa(result, { filename, logger, continueOnError /* YAML not needed in transform() */ });
|
|
205
|
-
src = reRunResult.src;
|
|
206
|
-
document = reRunResult.document;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// 2. Walk AST to validate tokens
|
|
211
|
-
let tokenCount = 0;
|
|
212
|
-
const startValidate = performance.now();
|
|
213
|
-
// $type and $deprecated can be applied at group level to target all child tokens,
|
|
214
|
-
// these two objects keep track of inherited prop values as we traverse the token tree
|
|
215
|
-
const $typeInheritance: Record<string, MemberNode> = {};
|
|
216
|
-
const $deprecatedInheritance: Record<string, MemberNode> = {};
|
|
217
|
-
traverse(document, {
|
|
218
|
-
enter(node, parent, subpath) {
|
|
219
|
-
// if $type appears at root of tokens.json, collect it
|
|
220
|
-
if (node.type === 'Document' && node.body.type === 'Object' && node.body.members) {
|
|
221
|
-
const { members: rootMembers } = node.body;
|
|
222
|
-
if (isGroupNode(node.body)) {
|
|
223
|
-
const root$type = rootMembers.find((m) => m.name.type === 'String' && m.name.value === '$type');
|
|
224
|
-
if (root$type) {
|
|
225
|
-
$typeInheritance['.'] = root$type;
|
|
226
|
-
}
|
|
227
|
-
const root$deprecated = rootMembers.find((m) => m.name.type === 'String' && m.name.value === '$deprecated');
|
|
228
|
-
if (root$deprecated) {
|
|
229
|
-
$deprecatedInheritance['.'] = root$deprecated;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// for transform() visitors, all non-tokens MUST be handled at this point (besides "root", which was handled above)
|
|
235
|
-
if (
|
|
236
|
-
node.type === 'Object' && // JSON object
|
|
237
|
-
subpath.length &&
|
|
238
|
-
!node.members.some((m) => m.name.type === 'String' && m.name.value === '$value') && // not the token node itself
|
|
239
|
-
!subpath.includes('$value') && // not a child of a token node, either
|
|
240
|
-
!subpath.includes('$extensions') // not metadata
|
|
241
|
-
) {
|
|
242
|
-
if (transform?.group) {
|
|
243
|
-
const newJSON = transform?.group(evaluate(node), subpath.join('.'), node);
|
|
244
|
-
if (newJSON) {
|
|
245
|
-
replaceObjMembers(node, parseJSON(newJSON));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// handle tokens
|
|
251
|
-
if (node.type === 'Member') {
|
|
252
|
-
const inheritedDeprecatedNode = computeInheritedProperty(node, '$deprecated', {
|
|
253
|
-
subpath,
|
|
254
|
-
inherited: $deprecatedInheritance,
|
|
255
|
-
});
|
|
256
|
-
const inheritedTypeNode = computeInheritedProperty(node, '$type', { subpath, inherited: $typeInheritance });
|
|
257
|
-
if (node.value.type === 'Object') {
|
|
258
|
-
const $value = node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$value');
|
|
259
|
-
|
|
260
|
-
// Call transform using either inherited type or token-level type
|
|
261
|
-
let typeNode = inheritedTypeNode;
|
|
262
|
-
if (!typeNode) {
|
|
263
|
-
const local$type = node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$type');
|
|
264
|
-
if (local$type) {
|
|
265
|
-
typeNode = local$type as MemberNode;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if ($value && typeNode?.value.type === 'String' && transform?.[typeNode.value.value]) {
|
|
270
|
-
const result = transform[typeNode.value.value]?.(evaluate(node.value), subpath.join('.'), node);
|
|
271
|
-
if (result) {
|
|
272
|
-
node.value = parseJSON(result).body;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const token = validateTokenNode(node, {
|
|
277
|
-
filename,
|
|
278
|
-
src,
|
|
279
|
-
config,
|
|
280
|
-
logger,
|
|
281
|
-
parent,
|
|
282
|
-
subpath,
|
|
283
|
-
transform,
|
|
284
|
-
inheritedDeprecatedNode,
|
|
285
|
-
inheritedTypeNode,
|
|
286
|
-
});
|
|
287
|
-
if (token) {
|
|
288
|
-
tokensSet[token.id] = token;
|
|
289
|
-
tokenCount++;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
},
|
|
294
|
-
});
|
|
295
|
-
logger.debug({
|
|
296
|
-
message: `Validated ${tokenCount} tokens`,
|
|
297
|
-
group: 'parser',
|
|
298
|
-
label: 'validate',
|
|
299
|
-
timing: performance.now() - startValidate,
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// 3. normalize values
|
|
303
|
-
const normalizeStart = performance.now();
|
|
304
|
-
for (const [id, token] of Object.entries(tokensSet)) {
|
|
305
|
-
try {
|
|
306
|
-
tokensSet[id]!.$value = normalize(token);
|
|
307
|
-
} catch (err) {
|
|
308
|
-
let { node } = token.source;
|
|
309
|
-
const members = getObjMembers(node);
|
|
310
|
-
if (members.$value) {
|
|
311
|
-
node = members.$value as ObjectNode;
|
|
312
|
-
}
|
|
313
|
-
logger.error({
|
|
314
|
-
group: 'parser',
|
|
315
|
-
label: 'normalize',
|
|
316
|
-
message: (err as Error).message,
|
|
317
|
-
filename,
|
|
318
|
-
src,
|
|
319
|
-
node,
|
|
320
|
-
continueOnError,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
for (const [mode, modeValue] of Object.entries(token.mode)) {
|
|
324
|
-
if (mode === '.') {
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
try {
|
|
328
|
-
tokensSet[id]!.mode[mode]!.$value = normalize({ $type: token.$type, ...modeValue });
|
|
329
|
-
} catch (err) {
|
|
330
|
-
let { node } = token.source;
|
|
331
|
-
const members = getObjMembers(node);
|
|
332
|
-
if (members.$value) {
|
|
333
|
-
node = members.$value as ObjectNode;
|
|
334
|
-
}
|
|
335
|
-
logger.error({
|
|
336
|
-
group: 'parser',
|
|
337
|
-
label: 'normalize',
|
|
338
|
-
message: (err as Error).message,
|
|
339
|
-
filename,
|
|
340
|
-
src,
|
|
341
|
-
node: modeValue.source.node,
|
|
342
|
-
continueOnError,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
logger.debug({
|
|
348
|
-
message: `Normalized ${tokenCount} tokens`,
|
|
349
|
-
group: 'parser',
|
|
350
|
-
label: 'normalize',
|
|
351
|
-
timing: performance.now() - normalizeStart,
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// 4. Execute lint runner with loaded plugins
|
|
355
|
-
if (!skipLint && config?.plugins?.length) {
|
|
356
|
-
const lintStart = performance.now();
|
|
357
|
-
await lintRunner({ tokens: tokensSet, src, config, logger });
|
|
358
|
-
logger.debug({
|
|
359
|
-
message: `Linted ${tokenCount} tokens`,
|
|
360
|
-
group: 'parser',
|
|
361
|
-
label: 'lint',
|
|
362
|
-
timing: performance.now() - lintStart,
|
|
363
|
-
});
|
|
364
|
-
} else {
|
|
365
|
-
logger.debug({ message: 'Linting skipped', group: 'parser', label: 'lint' });
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return {
|
|
369
|
-
tokens: tokensSet,
|
|
370
|
-
document,
|
|
371
|
-
src,
|
|
65
|
+
tokens,
|
|
66
|
+
sources,
|
|
372
67
|
};
|
|
373
68
|
}
|