@terrazzo/parser 0.1.3 → 0.2.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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +17 -0
- package/CONTRIBUTING.md +0 -12
- package/dist/build/index.d.ts +19 -0
- package/dist/build/index.js +165 -0
- package/dist/build/index.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +269 -0
- package/dist/config.js.map +1 -0
- package/{index.d.ts → dist/index.d.ts} +1 -5
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/code-frame.d.ts +30 -0
- package/dist/lib/code-frame.js +108 -0
- package/dist/lib/code-frame.js.map +1 -0
- package/dist/lint/index.d.ts +11 -0
- package/dist/lint/index.js +102 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/plugin-core/index.d.ts +12 -0
- package/dist/lint/plugin-core/index.js +40 -0
- package/dist/lint/plugin-core/index.js.map +1 -0
- package/dist/lint/plugin-core/lib/docs.d.ts +1 -0
- package/dist/lint/plugin-core/lib/docs.js +4 -0
- package/dist/lint/plugin-core/lib/docs.js.map +1 -0
- package/dist/lint/plugin-core/rules/a11y-min-contrast.d.ts +39 -0
- package/dist/lint/plugin-core/rules/a11y-min-contrast.js +58 -0
- package/dist/lint/plugin-core/rules/a11y-min-contrast.js.map +1 -0
- package/dist/lint/plugin-core/rules/a11y-min-font-size.d.ts +13 -0
- package/dist/lint/plugin-core/rules/a11y-min-font-size.js +45 -0
- package/dist/lint/plugin-core/rules/a11y-min-font-size.js.map +1 -0
- package/dist/lint/plugin-core/rules/colorspace.d.ts +14 -0
- package/dist/lint/plugin-core/rules/colorspace.js +85 -0
- package/dist/lint/plugin-core/rules/colorspace.js.map +1 -0
- package/dist/lint/plugin-core/rules/consistent-naming.d.ts +11 -0
- package/dist/lint/plugin-core/rules/consistent-naming.js +49 -0
- package/dist/lint/plugin-core/rules/consistent-naming.js.map +1 -0
- package/dist/lint/plugin-core/rules/descriptions.d.ts +9 -0
- package/dist/lint/plugin-core/rules/descriptions.js +32 -0
- package/dist/lint/plugin-core/rules/descriptions.js.map +1 -0
- package/dist/lint/plugin-core/rules/duplicate-values.d.ts +9 -0
- package/dist/lint/plugin-core/rules/duplicate-values.js +65 -0
- package/dist/lint/plugin-core/rules/duplicate-values.js.map +1 -0
- package/dist/lint/plugin-core/rules/max-gamut.d.ts +14 -0
- package/dist/lint/plugin-core/rules/max-gamut.js +101 -0
- package/dist/lint/plugin-core/rules/max-gamut.js.map +1 -0
- package/dist/lint/plugin-core/rules/required-children.d.ts +18 -0
- package/dist/lint/plugin-core/rules/required-children.js +78 -0
- package/dist/lint/plugin-core/rules/required-children.js.map +1 -0
- package/dist/lint/plugin-core/rules/required-modes.d.ts +13 -0
- package/dist/lint/plugin-core/rules/required-modes.js +52 -0
- package/dist/lint/plugin-core/rules/required-modes.js.map +1 -0
- package/dist/lint/plugin-core/rules/required-typography-properties.d.ts +10 -0
- package/dist/lint/plugin-core/rules/required-typography-properties.js +38 -0
- package/dist/lint/plugin-core/rules/required-typography-properties.js.map +1 -0
- package/dist/logger.d.ts +76 -0
- package/dist/logger.js +123 -0
- package/dist/logger.js.map +1 -0
- package/dist/parse/alias.d.ts +51 -0
- package/dist/parse/alias.js +188 -0
- package/dist/parse/alias.js.map +1 -0
- package/dist/parse/index.d.ts +27 -0
- package/dist/parse/index.js +379 -0
- package/dist/parse/index.js.map +1 -0
- package/dist/parse/json.d.ts +36 -0
- package/dist/parse/json.js +88 -0
- package/dist/parse/json.js.map +1 -0
- package/dist/parse/normalize.d.ts +23 -0
- package/dist/parse/normalize.js +163 -0
- package/dist/parse/normalize.js.map +1 -0
- package/dist/parse/validate.d.ts +45 -0
- package/dist/parse/validate.js +601 -0
- package/dist/parse/validate.js.map +1 -0
- package/dist/types.d.ts +264 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +7 -7
- package/{build/index.js → src/build/index.ts} +47 -63
- package/src/config.ts +280 -0
- package/src/index.ts +18 -0
- package/{lib/code-frame.js → src/lib/code-frame.ts} +41 -8
- package/src/lint/index.ts +135 -0
- package/src/lint/plugin-core/index.ts +47 -0
- package/src/lint/plugin-core/lib/docs.ts +3 -0
- package/src/lint/plugin-core/rules/a11y-min-contrast.ts +91 -0
- package/src/lint/plugin-core/rules/a11y-min-font-size.ts +64 -0
- package/src/lint/plugin-core/rules/colorspace.ts +101 -0
- package/src/lint/plugin-core/rules/consistent-naming.ts +65 -0
- package/src/lint/plugin-core/rules/descriptions.ts +41 -0
- package/src/lint/plugin-core/rules/duplicate-values.ts +80 -0
- package/src/lint/plugin-core/rules/max-gamut.ts +121 -0
- package/src/lint/plugin-core/rules/required-children.ts +104 -0
- package/src/lint/plugin-core/rules/required-modes.ts +71 -0
- package/src/lint/plugin-core/rules/required-typography-properties.ts +53 -0
- package/{logger.js → src/logger.ts} +55 -16
- package/src/parse/alias.ts +224 -0
- package/src/parse/index.ts +457 -0
- package/src/parse/json.ts +106 -0
- package/{parse/normalize.js → src/parse/normalize.ts} +70 -24
- package/{parse/validate.js → src/parse/validate.ts} +154 -236
- package/src/types.ts +310 -0
- package/build/index.d.ts +0 -113
- package/config.d.ts +0 -64
- package/config.js +0 -206
- package/index.js +0 -35
- package/lib/code-frame.d.ts +0 -56
- package/lint/index.d.ts +0 -44
- package/lint/index.js +0 -59
- package/lint/plugin-core/index.d.ts +0 -3
- package/lint/plugin-core/index.js +0 -12
- package/lint/plugin-core/rules/duplicate-values.d.ts +0 -10
- package/lint/plugin-core/rules/duplicate-values.js +0 -68
- package/logger.d.ts +0 -71
- package/parse/index.d.ts +0 -45
- package/parse/index.js +0 -592
- package/parse/json.d.ts +0 -30
- package/parse/json.js +0 -94
- package/parse/normalize.d.ts +0 -3
- package/parse/validate.d.ts +0 -43
package/src/config.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { merge } from 'merge-anything';
|
|
2
|
+
import coreLintPlugin from './lint/plugin-core/index.js';
|
|
3
|
+
import Logger from './logger.js';
|
|
4
|
+
import type { Config, ConfigInit, ConfigOptions, LintRuleSeverity } from './types.js';
|
|
5
|
+
|
|
6
|
+
const TRAILING_SLASH_RE = /\/*$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate and normalize a config
|
|
10
|
+
*/
|
|
11
|
+
export default function defineConfig(
|
|
12
|
+
rawConfig: Config,
|
|
13
|
+
{ logger = new Logger(), cwd }: ConfigOptions = {} as ConfigOptions,
|
|
14
|
+
): ConfigInit {
|
|
15
|
+
const configStart = performance.now();
|
|
16
|
+
|
|
17
|
+
if (!cwd) {
|
|
18
|
+
logger.error({ label: 'core', message: 'defineConfig() missing `cwd` for JS API' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
logger.debug({ group: 'parser', label: 'config', message: 'Start config validation' });
|
|
22
|
+
|
|
23
|
+
const config = merge({}, rawConfig) as unknown as ConfigInit;
|
|
24
|
+
|
|
25
|
+
// 1. normalize and init
|
|
26
|
+
normalizeTokens({ rawConfig, config, logger, cwd });
|
|
27
|
+
normalizeOutDir({ config, cwd, logger });
|
|
28
|
+
normalizePlugins({ config, logger });
|
|
29
|
+
normalizeLint({ config, logger });
|
|
30
|
+
normalizeIgnore({ config, logger });
|
|
31
|
+
|
|
32
|
+
// 2. Start build by calling config()
|
|
33
|
+
for (const plugin of config.plugins) {
|
|
34
|
+
plugin.config?.({ ...config });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. finish
|
|
38
|
+
logger.debug({
|
|
39
|
+
group: 'parser',
|
|
40
|
+
label: 'config',
|
|
41
|
+
message: 'Finish config validation',
|
|
42
|
+
timing: performance.now() - configStart,
|
|
43
|
+
});
|
|
44
|
+
return config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Normalize config.tokens */
|
|
48
|
+
function normalizeTokens({
|
|
49
|
+
rawConfig,
|
|
50
|
+
config,
|
|
51
|
+
logger,
|
|
52
|
+
cwd,
|
|
53
|
+
}: { rawConfig: Config; config: ConfigInit; logger: Logger; cwd: URL }) {
|
|
54
|
+
if (rawConfig.tokens === undefined) {
|
|
55
|
+
config.tokens = [
|
|
56
|
+
// @ts-ignore we’ll normalize in next step
|
|
57
|
+
'./tokens.json',
|
|
58
|
+
];
|
|
59
|
+
} else if (typeof rawConfig.tokens === 'string') {
|
|
60
|
+
config.tokens = [
|
|
61
|
+
// @ts-ignore we’ll normalize in next step
|
|
62
|
+
rawConfig.tokens,
|
|
63
|
+
];
|
|
64
|
+
} else if (Array.isArray(rawConfig.tokens)) {
|
|
65
|
+
config.tokens = [];
|
|
66
|
+
for (const file of rawConfig.tokens) {
|
|
67
|
+
if (typeof file === 'string' || (file as URL) instanceof URL) {
|
|
68
|
+
config.tokens.push(
|
|
69
|
+
// @ts-ignore we’ll normalize in next step
|
|
70
|
+
file,
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
logger.error({
|
|
74
|
+
label: '[config] tokens',
|
|
75
|
+
message: `Expected array of strings, encountered ${JSON.stringify(file)}`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
logger.error({
|
|
81
|
+
label: '[config] tokens',
|
|
82
|
+
message: `Expected string or array of strings, received ${typeof rawConfig.tokens}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
for (let i = 0; i < config.tokens!.length; i++) {
|
|
86
|
+
const filepath = config.tokens[i]!;
|
|
87
|
+
if (filepath instanceof URL) {
|
|
88
|
+
continue; // skip if already resolved
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
config.tokens[i] = new URL(filepath, cwd);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
logger.error({ label: '[config] tokens', message: `Invalid URL ${filepath}` });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Normalize config.outDir */
|
|
99
|
+
function normalizeOutDir({ config, cwd, logger }: { config: ConfigInit; logger: Logger; cwd: URL }) {
|
|
100
|
+
if (config.outDir instanceof URL) {
|
|
101
|
+
// noop
|
|
102
|
+
} else if (typeof config.outDir === 'undefined') {
|
|
103
|
+
config.outDir = new URL('./tokens/', cwd);
|
|
104
|
+
} else if (typeof config.outDir !== 'string') {
|
|
105
|
+
logger.error({ label: '[config] outDir', message: `Expected string, received ${JSON.stringify(config.outDir)}` });
|
|
106
|
+
} else {
|
|
107
|
+
config.outDir = new URL(config.outDir, cwd);
|
|
108
|
+
// always add trailing slash so URL treats it as a directory.
|
|
109
|
+
// do AFTER it has been normalized to POSIX paths with `href` (don’t use Node internals here! This may run in the browser)
|
|
110
|
+
config.outDir = new URL(config.outDir.href.replace(TRAILING_SLASH_RE, '/'));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Normalize config.plugins */
|
|
115
|
+
function normalizePlugins({ config, logger }: { config: ConfigInit; logger: Logger }) {
|
|
116
|
+
if (typeof config.plugins === 'undefined') {
|
|
117
|
+
config.plugins = [];
|
|
118
|
+
}
|
|
119
|
+
if (!Array.isArray(config.plugins)) {
|
|
120
|
+
logger.error({
|
|
121
|
+
label: '[config] plugins',
|
|
122
|
+
message: `Expected array of plugins, received ${JSON.stringify(config.plugins)}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
config.plugins.push(coreLintPlugin());
|
|
126
|
+
for (let n = 0; n < config.plugins.length; n++) {
|
|
127
|
+
const plugin = config.plugins[n];
|
|
128
|
+
if (typeof plugin !== 'object') {
|
|
129
|
+
logger.error({ label: `plugin[${n}]`, message: `Expected output plugin, received ${JSON.stringify(plugin)}` });
|
|
130
|
+
} else if (!plugin.name) {
|
|
131
|
+
logger.error({ label: `plugin[${n}]`, message: `Missing "name"` });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// order plugins with "enforce"
|
|
135
|
+
config.plugins.sort((a, b) => {
|
|
136
|
+
if (a.enforce === 'pre' && b.enforce !== 'pre') {
|
|
137
|
+
return -1;
|
|
138
|
+
} else if (a.enforce === 'post' && b.enforce !== 'post') {
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
return 0;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeLint({ config, logger }: { config: ConfigInit; logger: Logger }) {
|
|
146
|
+
if (config.lint !== undefined) {
|
|
147
|
+
if (config.lint === null || typeof config.lint !== 'object' || Array.isArray(config.lint)) {
|
|
148
|
+
logger.error({ label: '[config] lint', message: 'Must be an object' });
|
|
149
|
+
}
|
|
150
|
+
if (!config.lint.build) {
|
|
151
|
+
config.lint.build = { enabled: true };
|
|
152
|
+
}
|
|
153
|
+
if (config.lint.build.enabled !== undefined) {
|
|
154
|
+
if (typeof config.lint.build.enabled !== 'boolean') {
|
|
155
|
+
logger.error({
|
|
156
|
+
label: '[config] lint › build › enabled',
|
|
157
|
+
message: `Expected boolean, received ${JSON.stringify(config.lint.build)}`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
config.lint.build.enabled = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (config.lint.rules === undefined) {
|
|
165
|
+
config.lint.rules = {};
|
|
166
|
+
} else {
|
|
167
|
+
if (config.lint.rules === null || typeof config.lint.rules !== 'object' || Array.isArray(config.lint.rules)) {
|
|
168
|
+
logger.error({
|
|
169
|
+
label: '[config] lint › rules',
|
|
170
|
+
message: `Expected object, received ${JSON.stringify(config.lint.rules)}`,
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const allRules = new Map<string, string>();
|
|
176
|
+
for (const plugin of config.plugins) {
|
|
177
|
+
if (typeof plugin.lint !== 'function') {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const pluginRules = plugin.lint();
|
|
181
|
+
if (!pluginRules || Array.isArray(pluginRules) || typeof pluginRules !== 'object') {
|
|
182
|
+
logger.error({
|
|
183
|
+
label: `[config] plugin › ${plugin.name}`,
|
|
184
|
+
message: `Expected object for lint() received ${JSON.stringify(pluginRules)}`,
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
for (const rule of Object.keys(pluginRules)) {
|
|
189
|
+
// Note: sometimes plugins will be loaded multiple times, in which case it’s expected
|
|
190
|
+
// they’re register rules again for lint(). Only throw an error if plugin A and plugin B’s
|
|
191
|
+
// rules conflict.
|
|
192
|
+
|
|
193
|
+
if (allRules.get(rule) && allRules.get(rule) !== plugin.name) {
|
|
194
|
+
logger.error({
|
|
195
|
+
label: `[config] plugin › ${plugin.name}`,
|
|
196
|
+
message: `Duplicate rule ${rule} already registered by plugin ${allRules.get(rule)}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
allRules.set(rule, plugin.name);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const id of Object.keys(config.lint.rules)) {
|
|
204
|
+
if (!allRules.has(id)) {
|
|
205
|
+
logger.error({
|
|
206
|
+
label: `[config] lint › rule › ${id}`,
|
|
207
|
+
message: 'Unknown rule. Is the plugin installed?',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const value = config.lint.rules[id];
|
|
212
|
+
let severity: LintRuleSeverity = 'off';
|
|
213
|
+
let options: any;
|
|
214
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
215
|
+
severity = value;
|
|
216
|
+
} else if (Array.isArray(value)) {
|
|
217
|
+
severity = value[0] as LintRuleSeverity;
|
|
218
|
+
options = value[1];
|
|
219
|
+
} else if (value !== undefined) {
|
|
220
|
+
logger.error({
|
|
221
|
+
label: `[config] lint › rule › ${id}`,
|
|
222
|
+
message: `Invalid eyntax. Expected \`string | number | Array\`, received ${JSON.stringify(value)}}`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
config.lint.rules[id] = [severity, options];
|
|
226
|
+
if (typeof severity === 'number') {
|
|
227
|
+
if (severity !== 0 && severity !== 1 && severity !== 2) {
|
|
228
|
+
logger.error({
|
|
229
|
+
label: `[config] lint › rule › ${id}`,
|
|
230
|
+
message: `Invalid number ${severity}. Specify 0 (off), 1 (warn), or 2 (error).`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
config.lint.rules[id]![0] = (['off', 'warn', 'error'] as const)[severity]!;
|
|
234
|
+
} else if (typeof severity === 'string') {
|
|
235
|
+
if (severity !== 'off' && severity !== 'warn' && severity !== 'error') {
|
|
236
|
+
logger.error({
|
|
237
|
+
label: `[config] lint › rule › ${id}`,
|
|
238
|
+
message: `Invalid string ${JSON.stringify(severity)}. Specify "off", "warn", or "error".`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
} else if (value !== null) {
|
|
242
|
+
logger.error({
|
|
243
|
+
label: `[config] lint › rule › ${id}`,
|
|
244
|
+
message: `Expected string or number, received ${JSON.stringify(value)}`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
config.lint = {
|
|
251
|
+
build: { enabled: true },
|
|
252
|
+
rules: {},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeIgnore({ config, logger }: { config: ConfigInit; logger: Logger }) {
|
|
258
|
+
if (!config.ignore) {
|
|
259
|
+
config.ignore = {} as typeof config.ignore;
|
|
260
|
+
}
|
|
261
|
+
config.ignore.tokens ??= [];
|
|
262
|
+
config.ignore.deprecated ??= false;
|
|
263
|
+
if (!Array.isArray(config.ignore.tokens) || config.ignore.tokens.some((x) => typeof x !== 'string')) {
|
|
264
|
+
logger.error({
|
|
265
|
+
label: '[config] ignore › tokens',
|
|
266
|
+
message: `Expected array of strings, received ${JSON.stringify(config.ignore.tokens)}`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (typeof config.ignore.deprecated !== 'boolean') {
|
|
270
|
+
logger.error({
|
|
271
|
+
label: '[config] ignore › deprecated',
|
|
272
|
+
message: `Expected boolean, received ${JSON.stringify(config.ignore.deprecated)}`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Merge configs */
|
|
278
|
+
export function mergeConfigs(a: Config, b: Config): Config {
|
|
279
|
+
return merge(a, b);
|
|
280
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { default as build } from './build/index.js';
|
|
2
|
+
export * from './build/index.js';
|
|
3
|
+
|
|
4
|
+
export { default as defineConfig } from './config.js';
|
|
5
|
+
export * from './config.js';
|
|
6
|
+
|
|
7
|
+
export { default as lintRunner } from './lint/index.js';
|
|
8
|
+
export * from './lint/index.js';
|
|
9
|
+
|
|
10
|
+
export { default as Logger } from './logger.js';
|
|
11
|
+
export * from './logger.js';
|
|
12
|
+
|
|
13
|
+
export { default as parse } from './parse/index.js';
|
|
14
|
+
export * from './parse/index.js';
|
|
15
|
+
|
|
16
|
+
export * from './types.js';
|
|
17
|
+
|
|
18
|
+
export * from '@terrazzo/token-tools/dist/types.js';
|
|
@@ -25,17 +25,50 @@
|
|
|
25
25
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
26
26
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
27
27
|
|
|
28
|
+
export interface Location {
|
|
29
|
+
line: number;
|
|
30
|
+
column: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface NodeLocation {
|
|
34
|
+
end?: Location;
|
|
35
|
+
start: Location;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Options {
|
|
39
|
+
/** Syntax highlight the code as JavaScript for terminals. default: false */
|
|
40
|
+
highlightCode?: boolean;
|
|
41
|
+
/** The number of lines to show above the error. default: 2 */
|
|
42
|
+
linesAbove?: number;
|
|
43
|
+
/** The number of lines to show below the error. default: 3 */
|
|
44
|
+
linesBelow?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Forcibly syntax highlight the code as JavaScript (for non-terminals);
|
|
47
|
+
* overrides highlightCode.
|
|
48
|
+
* default: false
|
|
49
|
+
*/
|
|
50
|
+
forceColor?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Pass in a string to be displayed inline (if possible) next to the
|
|
53
|
+
* highlighted location in the code. If it can't be positioned inline,
|
|
54
|
+
* it will be placed above the code frame.
|
|
55
|
+
* default: nothing
|
|
56
|
+
*/
|
|
57
|
+
message?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
/**
|
|
29
61
|
* Extract what lines should be marked and highlighted.
|
|
30
62
|
*/
|
|
31
|
-
|
|
32
|
-
function getMarkerLines(loc, source, opts = {}) {
|
|
63
|
+
function getMarkerLines(loc: NodeLocation, source: string[], opts: Options = {} as Options) {
|
|
33
64
|
const startLoc = {
|
|
65
|
+
// @ts-ignore this is fine
|
|
34
66
|
column: 0,
|
|
67
|
+
// @ts-ignore this is fine
|
|
35
68
|
line: -1,
|
|
36
69
|
...loc.start,
|
|
37
|
-
};
|
|
38
|
-
const endLoc = {
|
|
70
|
+
} as Location;
|
|
71
|
+
const endLoc: Location = {
|
|
39
72
|
...startLoc,
|
|
40
73
|
...loc.end,
|
|
41
74
|
};
|
|
@@ -57,7 +90,7 @@ function getMarkerLines(loc, source, opts = {}) {
|
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
const lineDiff = endLine - startLine;
|
|
60
|
-
const markerLines = {};
|
|
93
|
+
const markerLines: Record<string, any> = {};
|
|
61
94
|
|
|
62
95
|
if (lineDiff) {
|
|
63
96
|
for (let i = 0; i <= lineDiff; i++) {
|
|
@@ -66,13 +99,13 @@ function getMarkerLines(loc, source, opts = {}) {
|
|
|
66
99
|
if (!startColumn) {
|
|
67
100
|
markerLines[lineNumber] = true;
|
|
68
101
|
} else if (i === 0) {
|
|
69
|
-
const sourceLength = source[lineNumber - 1]
|
|
102
|
+
const sourceLength = source[lineNumber - 1]!.length;
|
|
70
103
|
|
|
71
104
|
markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
|
|
72
105
|
} else if (i === lineDiff) {
|
|
73
106
|
markerLines[lineNumber] = [0, endColumn];
|
|
74
107
|
} else {
|
|
75
|
-
const sourceLength = source[lineNumber - i]
|
|
108
|
+
const sourceLength = source[lineNumber - i]!.length;
|
|
76
109
|
|
|
77
110
|
markerLines[lineNumber] = [0, sourceLength];
|
|
78
111
|
}
|
|
@@ -98,7 +131,7 @@ function getMarkerLines(loc, source, opts = {}) {
|
|
|
98
131
|
|
|
99
132
|
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
|
|
100
133
|
|
|
101
|
-
export function codeFrameColumns(rawLines, loc, opts = {}) {
|
|
134
|
+
export function codeFrameColumns(rawLines: string, loc: NodeLocation, opts: Options = {} as Options) {
|
|
102
135
|
const lines = rawLines.split(NEWLINE);
|
|
103
136
|
const { start, end, markerLines } = getMarkerLines(loc, lines, opts);
|
|
104
137
|
const hasColumns = loc.start && typeof loc.start.column === 'number';
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type TokenNormalized, pluralize } from '@terrazzo/token-tools';
|
|
2
|
+
import { merge } from 'merge-anything';
|
|
3
|
+
import type { LogEntry, default as Logger } from '../logger.js';
|
|
4
|
+
import type { ConfigInit } from '../types.js';
|
|
5
|
+
|
|
6
|
+
const listFormat = new Intl.ListFormat('en-us');
|
|
7
|
+
|
|
8
|
+
export interface LintRunnerOptions {
|
|
9
|
+
tokens: Record<string, TokenNormalized>;
|
|
10
|
+
filename?: URL;
|
|
11
|
+
config: ConfigInit;
|
|
12
|
+
src: string;
|
|
13
|
+
logger: Logger;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default async function lintRunner({
|
|
17
|
+
tokens,
|
|
18
|
+
filename,
|
|
19
|
+
config = {} as ConfigInit,
|
|
20
|
+
src,
|
|
21
|
+
logger,
|
|
22
|
+
}: LintRunnerOptions): Promise<void> {
|
|
23
|
+
const { plugins = [], lint } = config;
|
|
24
|
+
const unusedLintRules = Object.keys(lint?.rules ?? {});
|
|
25
|
+
|
|
26
|
+
for (const plugin of plugins) {
|
|
27
|
+
if (typeof plugin.lint === 'function') {
|
|
28
|
+
const s = performance.now();
|
|
29
|
+
|
|
30
|
+
logger.debug({ group: 'plugin', label: plugin.name, message: 'Start linting' });
|
|
31
|
+
|
|
32
|
+
const linter = plugin.lint();
|
|
33
|
+
const errors: LogEntry[] = [];
|
|
34
|
+
const warnings: LogEntry[] = [];
|
|
35
|
+
|
|
36
|
+
await Promise.all(
|
|
37
|
+
Object.entries(linter).map(async ([id, rule]) => {
|
|
38
|
+
if (!(id in lint.rules) || lint.rules[id] === null) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const [severity, options] = lint.rules[id]!;
|
|
42
|
+
if (severity === 'off') {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// note: this usually isn’t a Promise, but it _might_ be!
|
|
47
|
+
await rule.create({
|
|
48
|
+
id,
|
|
49
|
+
report(descriptor) {
|
|
50
|
+
let message = '';
|
|
51
|
+
if (!descriptor.message && !descriptor.messageId) {
|
|
52
|
+
logger.error({
|
|
53
|
+
label: `[plugin] ${plugin.name} › lint › ${id}`,
|
|
54
|
+
message: 'Unable to report error: missing message or messageId',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// handle message or messageId
|
|
59
|
+
if (descriptor.message) {
|
|
60
|
+
message = descriptor.message;
|
|
61
|
+
} else {
|
|
62
|
+
if (!(descriptor.messageId! in (rule.meta?.messages ?? {}))) {
|
|
63
|
+
logger.error({
|
|
64
|
+
label: `[plugin] ${plugin.name} › lint › ${id}`,
|
|
65
|
+
message: `messageId "${descriptor.messageId}" does not exist`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
message = rule.meta?.messages?.[descriptor.messageId as keyof typeof rule.meta.messages] ?? '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// replace with descriptor.data (if any)
|
|
72
|
+
if (descriptor.data && typeof descriptor.data === 'object') {
|
|
73
|
+
for (const [k, v] of Object.entries(descriptor.data)) {
|
|
74
|
+
// lazy formatting
|
|
75
|
+
const formatted = ['string', 'number', 'boolean'].includes(typeof v) ? String(v) : JSON.stringify(v);
|
|
76
|
+
message = message.replace(/{{[^}]+}}/g, (inner) => {
|
|
77
|
+
const key = inner.substring(2, inner.length - 2).trim();
|
|
78
|
+
return key === k ? formatted : inner;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
(severity === 'error' ? errors : warnings).push({
|
|
84
|
+
label: id,
|
|
85
|
+
message,
|
|
86
|
+
filename,
|
|
87
|
+
node: descriptor.node,
|
|
88
|
+
src: descriptor.source?.src,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
tokens,
|
|
92
|
+
filename,
|
|
93
|
+
src,
|
|
94
|
+
options: merge(
|
|
95
|
+
rule.meta?.defaultOptions ?? [],
|
|
96
|
+
rule.defaultOptions ?? [], // Note: is this the correct order to merge in?
|
|
97
|
+
options,
|
|
98
|
+
),
|
|
99
|
+
});
|
|
100
|
+
// tick off used rule
|
|
101
|
+
const unusedLintRuleI = unusedLintRules.indexOf(id);
|
|
102
|
+
if (unusedLintRuleI !== -1) {
|
|
103
|
+
unusedLintRules.splice(unusedLintRuleI, 1);
|
|
104
|
+
}
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
for (const error of errors) {
|
|
109
|
+
logger.error({ ...error, continueOnError: true }); // print out all errors before exiting here
|
|
110
|
+
}
|
|
111
|
+
for (const warning of warnings) {
|
|
112
|
+
logger.warn(warning);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
logger.debug({ group: 'plugin', label: plugin.name, message: 'Finish linting', timing: performance.now() - s });
|
|
116
|
+
|
|
117
|
+
if (errors.length) {
|
|
118
|
+
const counts = [pluralize(errors.length, 'error', 'errors')];
|
|
119
|
+
if (warnings.length) {
|
|
120
|
+
counts.push(pluralize(warnings.length, 'warning', 'warnings'));
|
|
121
|
+
}
|
|
122
|
+
logger.error({
|
|
123
|
+
message: `Lint failed with ${listFormat.format(counts)}`,
|
|
124
|
+
label: plugin.name,
|
|
125
|
+
continueOnError: false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// warn user if they have unused lint rules (they might have meant to configure something!)
|
|
132
|
+
for (const unusedRule of unusedLintRules) {
|
|
133
|
+
logger.warn({ group: 'parser', label: 'lint', message: `Unknown lint rule "${unusedRule}"` });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Terrazzo internal plugin that powers lint rules. Always enabled (but all
|
|
2
|
+
// rules are opt-in).
|
|
3
|
+
import type { Plugin } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export * from './rules/a11y-min-contrast.js';
|
|
6
|
+
export * from './rules/a11y-min-font-size.js';
|
|
7
|
+
export * from './rules/colorspace.js';
|
|
8
|
+
export * from './rules/consistent-naming.js';
|
|
9
|
+
export * from './rules/descriptions.js';
|
|
10
|
+
export * from './rules/duplicate-values.js';
|
|
11
|
+
export * from './rules/max-gamut.js';
|
|
12
|
+
export * from './rules/required-children.js';
|
|
13
|
+
export * from './rules/required-modes.js';
|
|
14
|
+
export * from './rules/required-typography-properties.js';
|
|
15
|
+
|
|
16
|
+
import a11yMinContrast, { A11Y_MIN_CONTRAST } from './rules/a11y-min-contrast.js';
|
|
17
|
+
import a11yMinFontSize, { A11Y_MIN_FONT_SIZE } from './rules/a11y-min-font-size.js';
|
|
18
|
+
import colorspace, { COLORSPACE } from './rules/colorspace.js';
|
|
19
|
+
import consistentNaming, { CONSISTENT_NAMING } from './rules/consistent-naming.js';
|
|
20
|
+
import descriptions, { DESCRIPTIONS } from './rules/descriptions.js';
|
|
21
|
+
import duplicateValues, { DUPLICATE_VALUES } from './rules/duplicate-values.js';
|
|
22
|
+
import maxGamut, { MAX_GAMUT } from './rules/max-gamut.js';
|
|
23
|
+
import requiredChidlren, { REQUIRED_CHILDREN } from './rules/required-children.js';
|
|
24
|
+
import requiredModes, { REQUIRED_MODES } from './rules/required-modes.js';
|
|
25
|
+
import requiredTypographyProperties, {
|
|
26
|
+
REQUIRED_TYPOGRAPHY_PROPERTIES,
|
|
27
|
+
} from './rules/required-typography-properties.js';
|
|
28
|
+
|
|
29
|
+
export default function coreLintPlugin(): Plugin {
|
|
30
|
+
return {
|
|
31
|
+
name: '@terrazzo/plugin-lint-core',
|
|
32
|
+
lint() {
|
|
33
|
+
return {
|
|
34
|
+
[COLORSPACE]: colorspace,
|
|
35
|
+
[CONSISTENT_NAMING]: consistentNaming,
|
|
36
|
+
[DESCRIPTIONS]: descriptions,
|
|
37
|
+
[DUPLICATE_VALUES]: duplicateValues,
|
|
38
|
+
[MAX_GAMUT]: maxGamut,
|
|
39
|
+
[REQUIRED_CHILDREN]: requiredChidlren,
|
|
40
|
+
[REQUIRED_MODES]: requiredModes,
|
|
41
|
+
[REQUIRED_TYPOGRAPHY_PROPERTIES]: requiredTypographyProperties,
|
|
42
|
+
[A11Y_MIN_CONTRAST]: a11yMinContrast,
|
|
43
|
+
[A11Y_MIN_FONT_SIZE]: a11yMinFontSize,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { tokenToCulori } from '@terrazzo/token-tools';
|
|
2
|
+
import { wcagContrast } from 'culori';
|
|
3
|
+
import type { LintRule } from '../../../types.js';
|
|
4
|
+
import { docsLink } from '../lib/docs.js';
|
|
5
|
+
|
|
6
|
+
export const A11Y_MIN_CONTRAST = 'a11y/min-contrast';
|
|
7
|
+
|
|
8
|
+
export interface RuleA11yMinContrastOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Whether to adhere to AA (minimum) or AAA (enhanced) contrast levels.
|
|
11
|
+
* @default "AA"
|
|
12
|
+
*/
|
|
13
|
+
level?: 'AA' | 'AAA';
|
|
14
|
+
/** Pairs of color tokens (and optionally typography) to test */
|
|
15
|
+
pairs: ContrastPair[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ContrastPair {
|
|
19
|
+
/** The foreground color token ID */
|
|
20
|
+
foreground: string;
|
|
21
|
+
/** The background color token ID */
|
|
22
|
+
background: string;
|
|
23
|
+
/**
|
|
24
|
+
* Is this pair for large text? Large text allows a smaller contrast ratio.
|
|
25
|
+
*
|
|
26
|
+
* Note: while WCAG has _suggested_ sizes and weights, those are merely
|
|
27
|
+
* suggestions. It’s always more reliable to determine what constitutes “large
|
|
28
|
+
* text” for your designs yourself, based on your typographic stack.
|
|
29
|
+
* @see https://www.w3.org/WAI/WCAG22/quickref/#contrast-minimum
|
|
30
|
+
*/
|
|
31
|
+
largeText?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const WCAG2_MIN_CONTRAST = {
|
|
35
|
+
AA: { default: 4.5, large: 3 },
|
|
36
|
+
AAA: { default: 7, large: 4.5 },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const ERROR_INSUFFICIENT_CONTRAST = 'INSUFFICIENT_CONTRAST';
|
|
40
|
+
|
|
41
|
+
const rule: LintRule<typeof ERROR_INSUFFICIENT_CONTRAST, RuleA11yMinContrastOptions> = {
|
|
42
|
+
meta: {
|
|
43
|
+
messages: {
|
|
44
|
+
[ERROR_INSUFFICIENT_CONTRAST]: 'Pair {{ index }} failed; expected {{ expected }}, got {{ actual }} ({{ level }})',
|
|
45
|
+
},
|
|
46
|
+
docs: {
|
|
47
|
+
description: 'Enforce colors meet minimum contrast checks for WCAG 2.',
|
|
48
|
+
url: docsLink(A11Y_MIN_CONTRAST),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
defaultOptions: { level: 'AA', pairs: [] },
|
|
52
|
+
create({ tokens, options, report }) {
|
|
53
|
+
for (let i = 0; i < options.pairs.length; i++) {
|
|
54
|
+
const { foreground, background, largeText } = options.pairs[i]!;
|
|
55
|
+
if (!tokens[foreground]) {
|
|
56
|
+
throw new Error(`Token ${foreground} does not exist`);
|
|
57
|
+
}
|
|
58
|
+
if (tokens[foreground].$type !== 'color') {
|
|
59
|
+
throw new Error(`Token ${foreground} isn’t a color`);
|
|
60
|
+
}
|
|
61
|
+
if (!tokens[background]) {
|
|
62
|
+
throw new Error(`Token ${background} does not exist`);
|
|
63
|
+
}
|
|
64
|
+
if (tokens[background].$type !== 'color') {
|
|
65
|
+
throw new Error(`Token ${background} isn’t a color`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Note: if these culors were unparseable, they would have already thrown an error before the linter
|
|
69
|
+
const a = tokenToCulori(tokens[foreground].$value)!;
|
|
70
|
+
const b = tokenToCulori(tokens[background].$value)!;
|
|
71
|
+
|
|
72
|
+
// Note: for the purposes of WCAG 2, foreground and background don’t
|
|
73
|
+
// matter. But in other contrast algorithms, they do.
|
|
74
|
+
const contrast = wcagContrast(a, b);
|
|
75
|
+
const min = WCAG2_MIN_CONTRAST[options.level ?? 'AA'][largeText ? 'large' : 'default'];
|
|
76
|
+
if (contrast < min) {
|
|
77
|
+
report({
|
|
78
|
+
messageId: ERROR_INSUFFICIENT_CONTRAST,
|
|
79
|
+
data: {
|
|
80
|
+
index: i + 1,
|
|
81
|
+
expected: min,
|
|
82
|
+
actual: Math.round(contrast * 100) / 100,
|
|
83
|
+
level: options.level,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default rule;
|