@terrazzo/parser 0.0.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/LICENSE +21 -0
- package/build/index.d.ts +104 -0
- package/build/index.js +182 -0
- package/config.d.ts +64 -0
- package/config.js +196 -0
- package/index.d.ts +16 -0
- package/index.js +37 -0
- package/lint/index.d.ts +41 -0
- package/lint/index.js +59 -0
- package/lint/plugin-core/index.d.ts +3 -0
- package/lint/plugin-core/index.js +12 -0
- package/lint/plugin-core/rules/duplicate-values.d.ts +10 -0
- package/lint/plugin-core/rules/duplicate-values.js +69 -0
- package/logger.d.ts +66 -0
- package/logger.js +121 -0
- package/package.json +52 -0
- package/parse/index.d.ts +32 -0
- package/parse/index.js +372 -0
- package/parse/json.d.ts +30 -0
- package/parse/json.js +94 -0
- package/parse/normalize.d.ts +3 -0
- package/parse/normalize.js +114 -0
- package/parse/validate.d.ts +42 -0
- package/parse/validate.js +620 -0
- package/parse/yaml.d.ts +11 -0
- package/parse/yaml.js +45 -0
- package/types.d.ts +519 -0
- package/types.js +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Drew Powers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { DocumentNode } from '@humanwhocodes/momoa';
|
|
2
|
+
import type { ConfigInit } from '../config.js';
|
|
3
|
+
import type Logger from '../logger.js';
|
|
4
|
+
import type { TokenNormalized } from '../types.js';
|
|
5
|
+
|
|
6
|
+
export interface BuildRunnerOptions {
|
|
7
|
+
ast: DocumentNode;
|
|
8
|
+
config: ConfigInit;
|
|
9
|
+
logger?: Logger;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface OutputFile {
|
|
13
|
+
filename: string;
|
|
14
|
+
contents: string | Buffer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Transformed token with a single value. Note that this may be any type! */
|
|
18
|
+
export interface TokenTransformedSingleValue {
|
|
19
|
+
/** ID unique to this format. If missing, use `token.id`. */
|
|
20
|
+
localID?: string;
|
|
21
|
+
type: 'SINGLE_VALUE';
|
|
22
|
+
value: string;
|
|
23
|
+
/** The mode of this value (default: `"."`) */
|
|
24
|
+
mode: string;
|
|
25
|
+
/** The original token */
|
|
26
|
+
token: TokenNormalized;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Transformed token with multiple values. Note that this may be any type! */
|
|
30
|
+
export interface TokenTransformedMultiValue {
|
|
31
|
+
/** ID unique to this format. If missing, use `token.id` */
|
|
32
|
+
localID?: string;
|
|
33
|
+
type: 'MULTI_VALUE';
|
|
34
|
+
value: Record<string, string>;
|
|
35
|
+
/** The mode of this value (default: `"."`) */
|
|
36
|
+
mode: string;
|
|
37
|
+
/** The original token */
|
|
38
|
+
token: TokenNormalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type TokenTransformed = TokenTransformedSingleValue | TokenTransformedMultiValue;
|
|
42
|
+
|
|
43
|
+
export interface TransformParams {
|
|
44
|
+
/** ID of an existing format */
|
|
45
|
+
format: string;
|
|
46
|
+
/** Glob of tokens to select (e.g. `"color.*"` to select all tokens starting with `"color."`) */
|
|
47
|
+
select?: string | string[];
|
|
48
|
+
/** Mode name, if selecting a mode (default: `"."`) */
|
|
49
|
+
mode?: string | string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TransformHookOptions {
|
|
53
|
+
/** Map of tokens */
|
|
54
|
+
tokens: Record<string, TokenNormalized>;
|
|
55
|
+
/** Query transformed values */
|
|
56
|
+
getTransforms(params: TransformParams): TokenTransformed;
|
|
57
|
+
/** Update transformed values */
|
|
58
|
+
setTransform(
|
|
59
|
+
id: string,
|
|
60
|
+
params: {
|
|
61
|
+
format: string;
|
|
62
|
+
localID?: string;
|
|
63
|
+
value: string | Record<string, string>;
|
|
64
|
+
mode?: string;
|
|
65
|
+
},
|
|
66
|
+
): void;
|
|
67
|
+
/** Momoa document */
|
|
68
|
+
ast: DocumentNode;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface BuildHookOptions {
|
|
72
|
+
/** Map of tokens */
|
|
73
|
+
tokens: Record<string, TokenNormalized>;
|
|
74
|
+
/** Query transformed values */
|
|
75
|
+
getTransforms(params: TransformParams): TokenTransformed[];
|
|
76
|
+
/** Momoa document */
|
|
77
|
+
ast: DocumentNode;
|
|
78
|
+
outputFile: (
|
|
79
|
+
/** Filename to output (relative to outDir) */
|
|
80
|
+
filename: string,
|
|
81
|
+
/** Contents to write to file */
|
|
82
|
+
contents: string | Buffer,
|
|
83
|
+
) => void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface BuildRunnerResult {
|
|
87
|
+
outputFiles: OutputFile[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface BuildEndHookOptions {
|
|
91
|
+
/** Map of tokens */
|
|
92
|
+
tokens: Record<string, TokenNormalized>;
|
|
93
|
+
/** Query transformed values */
|
|
94
|
+
getTransforms(params: TransformParams): TokenTransformed[];
|
|
95
|
+
/** Momoa document */
|
|
96
|
+
ast: DocumentNode;
|
|
97
|
+
/** Final files to be written */
|
|
98
|
+
outputFiles: OutputFile[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default function build(
|
|
102
|
+
tokens: Record<string, TokenNormalized>,
|
|
103
|
+
options: BuildRunnerOptions,
|
|
104
|
+
): Promise<BuildRunnerResult>;
|
package/build/index.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { isTokenMatch } from '@terrazzo/token-tools';
|
|
2
|
+
import wcmatch from 'wildcard-match';
|
|
3
|
+
import Logger from '../logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} BuildRunnerOptions
|
|
7
|
+
* @typedef {Record<string, TokenNormalized>} BuildRunnerOptions.tokens
|
|
8
|
+
* @typedef {DocumentNode} BuildRunnerOptions.ast
|
|
9
|
+
* @typedef {ConfigInit} BuildRunnerOptions.config
|
|
10
|
+
* @typedef {Logger} BuildRunnerOptions.logger
|
|
11
|
+
* @typedef {import("@humanwhocodes/momoa").DocumentNode} DocumentNode
|
|
12
|
+
* @typedef {import("../config.js").ConfigInit} ConfigInit
|
|
13
|
+
* @typedef {import("../logger.js")} Logger
|
|
14
|
+
* @typedef {import("../types.js").TokenNormalized} TokenNormalized
|
|
15
|
+
*
|
|
16
|
+
* @typedef {object} BuildRunnerResult
|
|
17
|
+
* @typedef {OutputFile[]} BuildRunnerResult.outputFiles
|
|
18
|
+
* @typedef {object} OutputFile
|
|
19
|
+
* @typedef {string} OutputFile.filename
|
|
20
|
+
* @typedef {string | Buffer} OutputFile.contents
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const SINGLE_VALUE = 'SINGLE_VALUE';
|
|
24
|
+
export const MULTI_VALUE = 'MULTI_VALUE';
|
|
25
|
+
|
|
26
|
+
/** Validate plugin setTransform() calls for immediate feedback */
|
|
27
|
+
function validateTransformParams({ params, token, logger, pluginName }) {
|
|
28
|
+
const baseEntry = { group: 'plugin', task: pluginName };
|
|
29
|
+
|
|
30
|
+
// validate ID
|
|
31
|
+
if (!token) {
|
|
32
|
+
logger.error({
|
|
33
|
+
...baseEntry,
|
|
34
|
+
message: `setTransform() tried to transform token "${id}" but it doesn’t exist.`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// validate value is valid for SINGLE_VALUE or MULTI_VALUE
|
|
38
|
+
if (
|
|
39
|
+
!params.value ||
|
|
40
|
+
(typeof params.value !== 'string' && typeof params.value !== 'object') ||
|
|
41
|
+
Array.isArray(params.value)
|
|
42
|
+
) {
|
|
43
|
+
logger.error({
|
|
44
|
+
...baseEntry,
|
|
45
|
+
message: `setTransform() value expected string or object of strings, received ${
|
|
46
|
+
Array.isArray(params.value) ? 'Array' : typeof params.value
|
|
47
|
+
}`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (typeof params.value === 'object' && Object.values(params.value).some((v) => !v || typeof v !== 'string')) {
|
|
51
|
+
logger.error({
|
|
52
|
+
...baseEntry,
|
|
53
|
+
message: 'setTransform() value expected object of strings, received some non-string values',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run build stage
|
|
60
|
+
* @param {BuildOptions} options
|
|
61
|
+
* @return {Promise<BuildResult>}
|
|
62
|
+
*/
|
|
63
|
+
export default async function build(tokens, { ast, logger = new Logger(), config }) {
|
|
64
|
+
const formats = {};
|
|
65
|
+
const result = { outputFiles: [] };
|
|
66
|
+
|
|
67
|
+
function getTransforms(params) {
|
|
68
|
+
return (formats[params.format] ?? []).filter((token) => {
|
|
69
|
+
if (
|
|
70
|
+
params.select &&
|
|
71
|
+
params.select !== '*' &&
|
|
72
|
+
!isTokenMatch(token.token.id, Array.isArray(params.select) ? params.select : [params.select])
|
|
73
|
+
) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return !params.mode || wcmatch(params.mode)(token.mode);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// transform()
|
|
81
|
+
let transformsLocked = false; // prevent plugins from transforming after stage has ended
|
|
82
|
+
const startTransform = performance.now();
|
|
83
|
+
logger.debug({ group: 'parser', task: 'transform', message: 'Start transform' });
|
|
84
|
+
for (const plugin of config.plugins) {
|
|
85
|
+
if (typeof plugin.transform === 'function') {
|
|
86
|
+
await plugin.transform({
|
|
87
|
+
tokens,
|
|
88
|
+
ast,
|
|
89
|
+
getTransforms,
|
|
90
|
+
setTransform(id, params) {
|
|
91
|
+
if (transformsLocked) {
|
|
92
|
+
logger.warn({
|
|
93
|
+
message: 'Attempted to call setTransform() after transform step has completed.',
|
|
94
|
+
group: 'plugin',
|
|
95
|
+
task: plugin.name,
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const token = tokens[id];
|
|
100
|
+
validateTransformParams({ token, logger, params, pluginName: plugin.name });
|
|
101
|
+
|
|
102
|
+
// upsert
|
|
103
|
+
if (!formats[params.format]) {
|
|
104
|
+
formats[params.format] = [];
|
|
105
|
+
}
|
|
106
|
+
const foundTokenI = formats[params.format].findIndex(
|
|
107
|
+
(t) =>
|
|
108
|
+
params.localID === t.localID &&
|
|
109
|
+
(!params.mode || params.mode === t.mode) &&
|
|
110
|
+
(!params.variant || params.variant === t.variant),
|
|
111
|
+
);
|
|
112
|
+
if (foundTokenI === -1) {
|
|
113
|
+
formats[params.format].push({
|
|
114
|
+
...params,
|
|
115
|
+
type: typeof params.value === 'string' ? SINGLE_VALUE : MULTI_VALUE,
|
|
116
|
+
mode: params.mode || '.',
|
|
117
|
+
token: structuredClone(token),
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
formats[params.format][foundTokenI].value = params.value;
|
|
121
|
+
formats[params.format][foundTokenI].type = typeof params.value === 'string' ? SINGLE_VALUE : MULTI_VALUE;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
transformsLocked = true;
|
|
128
|
+
logger.debug({
|
|
129
|
+
group: 'parser',
|
|
130
|
+
task: 'transform',
|
|
131
|
+
message: 'Finish transform',
|
|
132
|
+
timing: performance.now() - startTransform,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// build()
|
|
136
|
+
const startBuild = performance.now();
|
|
137
|
+
logger.debug({ group: 'parser', task: 'build', message: 'Start build' });
|
|
138
|
+
for (const plugin of config.plugins) {
|
|
139
|
+
if (typeof plugin.build === 'function') {
|
|
140
|
+
await plugin.build({
|
|
141
|
+
tokens,
|
|
142
|
+
ast,
|
|
143
|
+
getTransforms,
|
|
144
|
+
outputFile(filename, contents) {
|
|
145
|
+
const resolved = new URL(filename, config.outDir);
|
|
146
|
+
if (result.outputFiles.some((f) => new URL(f.filename, config.outDir).href === resolved.href)) {
|
|
147
|
+
logger.error({ message: `Can’t overwrite file "${filename}"`, label: plugin.name });
|
|
148
|
+
}
|
|
149
|
+
result.outputFiles.push({ filename, contents });
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
logger.debug({
|
|
155
|
+
group: 'parser',
|
|
156
|
+
task: 'build',
|
|
157
|
+
message: 'Finish build',
|
|
158
|
+
timing: performance.now() - startBuild,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// buildEnd()
|
|
162
|
+
const startBuildEnd = performance.now();
|
|
163
|
+
logger.debug({ group: 'parser', task: 'build', message: 'Start buildEnd' });
|
|
164
|
+
for (const plugin of config.plugins) {
|
|
165
|
+
if (typeof plugin.buildEnd === 'function') {
|
|
166
|
+
await plugin.buildEnd({
|
|
167
|
+
tokens,
|
|
168
|
+
ast,
|
|
169
|
+
format: (formatID) => createFormatter(formatID),
|
|
170
|
+
outputFiles: structruedClone(result.outputFiles),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
logger.debug({
|
|
175
|
+
group: 'parser',
|
|
176
|
+
task: 'build',
|
|
177
|
+
message: 'Finish buildEnd',
|
|
178
|
+
timing: performance.now() - startBuildEnd,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return result;
|
|
182
|
+
}
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { BuildHookOptions, BuildRunnerResult, TransformHookOptions } from './build/index.js';
|
|
2
|
+
import type { LintRuleShorthand, LintRuleLonghand, Linter } from './lint/index.js';
|
|
3
|
+
import type Logger from './logger.js';
|
|
4
|
+
|
|
5
|
+
export interface Config {
|
|
6
|
+
/** Path to tokens.json (default: "./tokens.json") */
|
|
7
|
+
tokens?: string | string[];
|
|
8
|
+
/** Output directory (default: "./tokens/") */
|
|
9
|
+
outDir?: string;
|
|
10
|
+
/** Specify plugins */
|
|
11
|
+
plugins?: Plugin[];
|
|
12
|
+
/** Specify linting settings */
|
|
13
|
+
lint?: {
|
|
14
|
+
/** Configure build behavior */
|
|
15
|
+
build?: {
|
|
16
|
+
/** Should linters run with `co build`? (default: true) */
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** Configure lint rules */
|
|
20
|
+
rules?: Record<string, LintRuleShorthand | LintRuleLonghand>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ConfigInit {
|
|
25
|
+
tokens: URL[];
|
|
26
|
+
outDir: URL;
|
|
27
|
+
plugins: Plugin[];
|
|
28
|
+
lint: {
|
|
29
|
+
build: NonNullable<NonNullable<Config['lint']>['build']>;
|
|
30
|
+
rules: Record<string, LintRuleLonghand>;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Plugin {
|
|
35
|
+
name: string;
|
|
36
|
+
/** Read config, and optionally modify */
|
|
37
|
+
// biome-ignore lint/suspicious/noConfusingVoidType format: this helps plugins be a little looser on their typing
|
|
38
|
+
config?(config: ConfigInit): void | ConfigInit | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Declare:
|
|
41
|
+
* - `"pre"`: run this plugin BEFORE all others
|
|
42
|
+
* - `"post"`: run this plugin AFTER all others
|
|
43
|
+
* - (default) run this plugin in default order (array order)
|
|
44
|
+
*/
|
|
45
|
+
enforce?: 'pre' | 'post';
|
|
46
|
+
/** Throw lint errors/warnings */
|
|
47
|
+
lint?(): Record<string, Linter>;
|
|
48
|
+
transform?(options: TransformHookOptions): Promise<void>;
|
|
49
|
+
build?(options: BuildHookOptions): Promise<void>;
|
|
50
|
+
buildEnd?(result: BuildRunnerResult): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ConfigOptions {
|
|
54
|
+
logger?: Logger;
|
|
55
|
+
/** @terrazzo/parser needs cwd so this can be run without Node.js. Importing defineConfig from @terrazzo/cli doesn’t need this. */
|
|
56
|
+
cwd: URL;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate and normalize a config
|
|
61
|
+
*/
|
|
62
|
+
export default function defineConfig(rawConfig: Config, options: ConfigOptions): ConfigInit;
|
|
63
|
+
|
|
64
|
+
export function mergeConfigs(a: Config, b: Config): Config;
|
package/config.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { merge } from 'merge-anything';
|
|
2
|
+
import coreLintPlugin from './lint/plugin-core/index.js';
|
|
3
|
+
import Logger from './logger.js';
|
|
4
|
+
|
|
5
|
+
const TRAILING_SLASH_RE = /\/*$/;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate and normalize a config
|
|
9
|
+
* @param {Config} rawConfig
|
|
10
|
+
* @param {object} options
|
|
11
|
+
* @param {Logger} options.logger
|
|
12
|
+
* @param {URL} options.cwd
|
|
13
|
+
*/
|
|
14
|
+
export default function defineConfig(rawConfig, { logger = new Logger(), cwd = import.meta.url } = {}) {
|
|
15
|
+
const configStart = performance.now();
|
|
16
|
+
|
|
17
|
+
logger.debug({ group: 'parser', task: 'config', message: 'Start config validation' });
|
|
18
|
+
|
|
19
|
+
const config = { ...rawConfig };
|
|
20
|
+
|
|
21
|
+
// config.tokens
|
|
22
|
+
if (rawConfig.tokens === undefined) {
|
|
23
|
+
config.tokens = ['./tokens.json']; // will be normalized in next step
|
|
24
|
+
} else if (typeof rawConfig.tokens === 'string') {
|
|
25
|
+
config.tokens = [rawConfig.tokens]; // will be normalized in next step
|
|
26
|
+
} else if (Array.isArray(rawConfig.tokens)) {
|
|
27
|
+
config.tokens = [];
|
|
28
|
+
for (const file of rawConfig.tokens) {
|
|
29
|
+
if (typeof file === 'string') {
|
|
30
|
+
config.tokens.push(file); // will be normalized in next step
|
|
31
|
+
} else {
|
|
32
|
+
logger.error({
|
|
33
|
+
label: 'config.tokens',
|
|
34
|
+
message: `Expected array of strings, encountered ${JSON.stringify(file)}`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
logger.error({
|
|
40
|
+
label: 'config.tokens',
|
|
41
|
+
message: `Expected string or array of strings, received ${typeof rawConfig.tokens}`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
for (let i = 0; i < config.tokens.length; i++) {
|
|
45
|
+
const filepath = config.tokens[i];
|
|
46
|
+
try {
|
|
47
|
+
config.tokens[i] = new URL(filepath, cwd);
|
|
48
|
+
} catch {
|
|
49
|
+
logger.error({ label: 'config.tokens', message: `Invalid URL ${filepath}` });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// config.outDir
|
|
54
|
+
if (typeof config.outDir === 'undefined') {
|
|
55
|
+
config.outDir = new URL('./tokens/', cwd);
|
|
56
|
+
} else if (typeof config.outDir !== 'string') {
|
|
57
|
+
logger.error({ label: 'config.outDir', message: `Expected string, received ${JSON.stringify(config.outDir)}` });
|
|
58
|
+
} else {
|
|
59
|
+
// note: always add trailing slash so URL treats it as a directory
|
|
60
|
+
config.outDir = new URL(config.outDir.replace(TRAILING_SLASH_RE, '/'), cwd);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// config.plugins
|
|
64
|
+
if (typeof config.plugins === 'undefined') {
|
|
65
|
+
config.plugins = [];
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(config.plugins)) {
|
|
68
|
+
logger.error({
|
|
69
|
+
label: 'config.plugins',
|
|
70
|
+
message: `Expected array of plugins, received ${JSON.stringify(config.plugins)}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
config.plugins.push(coreLintPlugin());
|
|
74
|
+
for (let n = 0; n < config.plugins.length; n++) {
|
|
75
|
+
const plugin = config.plugins[n];
|
|
76
|
+
if (typeof plugin !== 'object') {
|
|
77
|
+
logger.error({ label: `plugin[${n}]`, message: `Expected output plugin, received ${JSON.stringify(plugin)}` });
|
|
78
|
+
} else if (!plugin.name) {
|
|
79
|
+
logger.error({ label: `plugin[${n}]`, message: `Missing "name"` });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// order plugins with "enforce"
|
|
83
|
+
config.plugins.sort((a, b) => {
|
|
84
|
+
if (a.enforce === 'pre' && b.enforce !== 'pre') {
|
|
85
|
+
return -1;
|
|
86
|
+
} else if (a.enforce === 'post' && b.enforce !== 'post') {
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// config.lint
|
|
93
|
+
if (config.lint !== undefined) {
|
|
94
|
+
if (config.lint === null || typeof config.lint !== 'object' || Array.isArray(config.lint)) {
|
|
95
|
+
logger.error({ label: 'config.lint', message: 'Must be an object' });
|
|
96
|
+
return config;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!config.lint.build) {
|
|
100
|
+
config.lint.build = { enabled: true };
|
|
101
|
+
}
|
|
102
|
+
if (config.lint.build.enabled !== undefined) {
|
|
103
|
+
if (typeof config.lint.build.enabled !== 'boolean') {
|
|
104
|
+
logger.error({
|
|
105
|
+
label: 'config.lint.build.enabled',
|
|
106
|
+
message: `Expected boolean, received ${JSON.stringify(config.lint.build)}`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
config.lint.build.enabled = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (config.lint.rules !== undefined) {
|
|
114
|
+
if (config.lint.rules === null || typeof config.lint.rules !== 'object' || Array.isArray(config.lint.rules)) {
|
|
115
|
+
logger.error({
|
|
116
|
+
label: 'config.lint.rules',
|
|
117
|
+
message: `Expected object, received ${JSON.stringify(config.lint.rules)}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const id in config.lint.rules) {
|
|
122
|
+
if (!Object.hasOwn(config.lint.rules, id)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (typeof id !== 'string') {
|
|
126
|
+
logger.error({ label: 'config.lint.rules', message: `Expects string keys, received ${JSON.stringify(id)}` });
|
|
127
|
+
}
|
|
128
|
+
const value = config.lint.rules[id];
|
|
129
|
+
let severity = 'off';
|
|
130
|
+
let options;
|
|
131
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
132
|
+
severity = value;
|
|
133
|
+
} else if (Array.isArray(value)) {
|
|
134
|
+
severity = value[0];
|
|
135
|
+
options = value[1];
|
|
136
|
+
} else if (value !== undefined) {
|
|
137
|
+
logger.error({
|
|
138
|
+
label: `config.lint.rule:${id}`,
|
|
139
|
+
message: `Invalid eyntax. Expected \`string | number | Array\`, received ${JSON.stringify(value)}}`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
config.lint.rules[id] = { id, severity, options };
|
|
143
|
+
if (typeof severity === 'number') {
|
|
144
|
+
if (severity !== 0 && severity !== 1 && severity !== 2) {
|
|
145
|
+
logger.error({
|
|
146
|
+
label: `config.lint.rule:${id}`,
|
|
147
|
+
message: `Invalid number ${severity}. Specify 0 (off), 1 (warn), or 2 (error).`,
|
|
148
|
+
});
|
|
149
|
+
return config;
|
|
150
|
+
}
|
|
151
|
+
config.lint.rules[id].severity = ['off', 'warn', 'error'][severity];
|
|
152
|
+
} else if (typeof severity === 'string') {
|
|
153
|
+
if (severity !== 'off' && severity !== 'warn' && severity !== 'error') {
|
|
154
|
+
logger.error({
|
|
155
|
+
label: `config.lint.rule:${id}`,
|
|
156
|
+
message: `Invalid string ${JSON.stringify(severity)}. Specify "off", "warn", or "error".`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} else if (value !== null) {
|
|
160
|
+
logger.error({
|
|
161
|
+
label: `config.lint.rule:${id}`,
|
|
162
|
+
message: `Expected string or number, received ${JSON.stringify(value)}`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
config.lint = {
|
|
169
|
+
build: { enabled: true },
|
|
170
|
+
rules: {},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// call plugin.config()
|
|
175
|
+
for (const plugin of config.plugins) {
|
|
176
|
+
plugin.config?.({ ...config });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
logger.debug({
|
|
180
|
+
group: 'parser',
|
|
181
|
+
task: 'config',
|
|
182
|
+
message: 'Finish config validation',
|
|
183
|
+
timing: performance.now() - configStart,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return config;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {object} a
|
|
191
|
+
* @param {object} b
|
|
192
|
+
* @return {object}
|
|
193
|
+
*/
|
|
194
|
+
export function mergeConfigs(a, b) {
|
|
195
|
+
return merge(a, b);
|
|
196
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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';
|
package/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// MIT License
|
|
2
|
+
//
|
|
3
|
+
// Copyright (c) 2021 Drew Powers
|
|
4
|
+
//
|
|
5
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
// in the Software without restriction, including without limitation the rights
|
|
8
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
// furnished to do so, subject to the following conditions:
|
|
11
|
+
//
|
|
12
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
// copies or substantial portions of the Software.
|
|
14
|
+
//
|
|
15
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
// SOFTWARE.
|
|
22
|
+
export { default as build } from './build/index.js';
|
|
23
|
+
export * from './build/index.js';
|
|
24
|
+
|
|
25
|
+
export { default as defineConfig } from './config.js';
|
|
26
|
+
export * from './config.js';
|
|
27
|
+
|
|
28
|
+
export { default as lintRunner } from './lint/index.js';
|
|
29
|
+
export * from './lint/index.js';
|
|
30
|
+
|
|
31
|
+
export { default as Logger } from './logger.js';
|
|
32
|
+
export * from './logger.js';
|
|
33
|
+
|
|
34
|
+
export { default as parse } from './parse/index.js';
|
|
35
|
+
export * from './parse/index.js';
|
|
36
|
+
|
|
37
|
+
export * from './types.js';
|
package/lint/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AnyNode, DocumentNode } from '@humanwhocodes/momoa';
|
|
2
|
+
import type { ConfigInit } from '../config.js';
|
|
3
|
+
import type Logger from '../logger.js';
|
|
4
|
+
import type { Group } from '../types.js';
|
|
5
|
+
|
|
6
|
+
export interface LintNotice {
|
|
7
|
+
/** Lint message shown to the user */
|
|
8
|
+
message: string;
|
|
9
|
+
/** Erring node (used to point to a specific line) */
|
|
10
|
+
node?: AnyNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type LintRuleSeverity = 'error' | 'warn' | 'off';
|
|
14
|
+
export type LintRuleShorthand = LintRuleSeverity | 0 | 1 | 2;
|
|
15
|
+
export type LintRuleLonghand = [LintRuleSeverity | 0 | 1 | 2, any];
|
|
16
|
+
|
|
17
|
+
export interface LintRuleNormalized<O = any> {
|
|
18
|
+
id: string;
|
|
19
|
+
severity: LintRuleSeverity;
|
|
20
|
+
options?: O;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LinterOptions<O = any> {
|
|
24
|
+
tokens: Group;
|
|
25
|
+
rule: {
|
|
26
|
+
id: string;
|
|
27
|
+
severity: LintRuleSeverity;
|
|
28
|
+
};
|
|
29
|
+
ast: DocumentNode;
|
|
30
|
+
/** Any options the user has declared for this plugin */
|
|
31
|
+
options?: O;
|
|
32
|
+
}
|
|
33
|
+
export type Linter = (options: LinterOptions) => Promise<LintNotice[] | undefined>;
|
|
34
|
+
|
|
35
|
+
export interface LintRunnerOptions {
|
|
36
|
+
ast: DocumentNode;
|
|
37
|
+
config: ConfigInit;
|
|
38
|
+
logger: Logger;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function lintRunner(options: LintRunnerOptions): Promise<void>;
|