@tenphi/eslint-plugin-tasty 1.18.0 → 1.19.1
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/README.md +72 -45
- package/docs/rules/consistent-token-usage.md +74 -0
- package/docs/rules/known-property.md +55 -0
- package/docs/rules/no-classname-prop.md +62 -0
- package/docs/rules/no-ignored-pseudo-element-state.md +97 -0
- package/docs/rules/no-important.md +25 -0
- package/docs/rules/no-nested-selector.md +39 -0
- package/docs/rules/no-nested-state-map.md +33 -0
- package/docs/rules/no-own-at-root.md +57 -0
- package/docs/rules/no-protected-restyle.md +194 -0
- package/docs/rules/no-raw-color-values.md +51 -0
- package/docs/rules/no-raw-motion-duration.md +50 -0
- package/docs/rules/no-raw-transition-duration.md +56 -0
- package/docs/rules/no-runtime-styles-mutation.md +58 -0
- package/docs/rules/no-state-in-selector.md +142 -0
- package/docs/rules/no-style-prop.md +129 -0
- package/docs/rules/no-style-spread.md +55 -0
- package/docs/rules/no-styles-prop.md +59 -0
- package/docs/rules/no-unknown-state-alias.md +45 -0
- package/docs/rules/prefer-auto-calc.md +30 -0
- package/docs/rules/prefer-custom-property-syntax.md +56 -0
- package/docs/rules/prefer-directional-shorthand.md +46 -0
- package/docs/rules/prefer-element-selector.md +77 -0
- package/docs/rules/prefer-fill-mix.md +119 -0
- package/docs/rules/prefer-hide.md +31 -0
- package/docs/rules/prefer-longhand-property.md +25 -0
- package/docs/rules/prefer-shorthand-property.md +194 -0
- package/docs/rules/prefer-state-negation.md +44 -0
- package/docs/rules/require-default-state.md +50 -0
- package/docs/rules/static-no-dynamic-values.md +32 -0
- package/docs/rules/static-valid-selector.md +36 -0
- package/docs/rules/valid-boolean-property.md +39 -0
- package/docs/rules/valid-color-token.md +64 -0
- package/docs/rules/valid-custom-property.md +36 -0
- package/docs/rules/valid-custom-unit.md +34 -0
- package/docs/rules/valid-default-state-order.md +44 -0
- package/docs/rules/valid-directional-modifier.md +46 -0
- package/docs/rules/valid-preset.md +35 -0
- package/docs/rules/valid-radius-shape.md +31 -0
- package/docs/rules/valid-recipe.md +35 -0
- package/docs/rules/valid-state-definition.md +46 -0
- package/docs/rules/valid-state-key.md +62 -0
- package/docs/rules/valid-styles-structure.md +48 -0
- package/docs/rules/valid-sub-element.md +100 -0
- package/docs/rules/valid-transition.md +46 -0
- package/docs/rules/valid-value.md +65 -0
- package/harness/dev.harnesspm/hooks.json +12 -0
- package/harness/dev.harnesspm/rules/usage.md +11 -0
- package/harness/dev.harnesspm/scripts/check-edit.mjs +163 -0
- package/harness/dev.harnesspm/scripts/check.mjs +254 -0
- package/harness/dev.harnesspm/scripts/lint.mjs +318 -0
- package/harness/dev.harnesspm/scripts/project-config.mjs +316 -0
- package/harness/plugin.json +23 -0
- package/package.json +11 -7
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project's own Tasty lint config, so the tools report what the project's
|
|
3
|
+
* lint reports: the level and options of each tasty/* rule, `settings.tasty`,
|
|
4
|
+
* and the files the config ignores. It comes from the nearest Oxlint or ESLint
|
|
5
|
+
* config above a file that loads this plugin.
|
|
6
|
+
*
|
|
7
|
+
* Both are read through ESLint's `calculateConfigForFile()`: the project's ESLint
|
|
8
|
+
* config as it is, and an Oxlint config once it is turned into a flat config
|
|
9
|
+
* that holds only its Tasty part.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
12
|
+
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
13
|
+
|
|
14
|
+
const NAME = '@tenphi/eslint-plugin-tasty';
|
|
15
|
+
|
|
16
|
+
/** The names Oxlint gives the plugin when `jsPlugins` lists it without one. */
|
|
17
|
+
const OXLINT_NAMES = ['@tenphi/tasty', NAME];
|
|
18
|
+
|
|
19
|
+
/** The files Oxlint 1.x finds by itself, in each folder from the file up. */
|
|
20
|
+
export const OXLINT_CONFIGS = [
|
|
21
|
+
'.oxlintrc.json',
|
|
22
|
+
'.oxlintrc.jsonc',
|
|
23
|
+
'oxlint.config.ts',
|
|
24
|
+
'oxlint.config.mts',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const ESLINT_CONFIGS = [
|
|
28
|
+
'eslint.config.js',
|
|
29
|
+
'eslint.config.mjs',
|
|
30
|
+
'eslint.config.cjs',
|
|
31
|
+
'eslint.config.ts',
|
|
32
|
+
'eslint.config.mts',
|
|
33
|
+
'eslint.config.cts',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** The files the tools lint, in an ESLint `files` pattern. */
|
|
37
|
+
export const SOURCES = '**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}';
|
|
38
|
+
|
|
39
|
+
const LEVELS = {
|
|
40
|
+
off: 'off',
|
|
41
|
+
allow: 'off',
|
|
42
|
+
0: 'off',
|
|
43
|
+
warn: 'warn',
|
|
44
|
+
1: 'warn',
|
|
45
|
+
error: 'error',
|
|
46
|
+
deny: 'error',
|
|
47
|
+
2: 'error',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Whether a rule entry turns the rule on. */
|
|
51
|
+
export function isOn(entry) {
|
|
52
|
+
return LEVELS[Array.isArray(entry) ? entry[0] : entry] !== 'off';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* JSON with comments and trailing commas, as Oxlint reads `.oxlintrc.json`.
|
|
57
|
+
* Strings are copied as they are.
|
|
58
|
+
*/
|
|
59
|
+
export function parseJsonc(text) {
|
|
60
|
+
const withoutComments = replaceOutsideStrings(
|
|
61
|
+
text,
|
|
62
|
+
/\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$)/y,
|
|
63
|
+
' ',
|
|
64
|
+
);
|
|
65
|
+
return JSON.parse(
|
|
66
|
+
replaceOutsideStrings(withoutComments, /,(?=\s*[}\]])/y, ''),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Replaces each match of the sticky `pattern` that starts outside a string. */
|
|
71
|
+
function replaceOutsideStrings(text, pattern, replacement) {
|
|
72
|
+
let out = '';
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < text.length) {
|
|
75
|
+
if (text[i] === '"') {
|
|
76
|
+
let end = i + 1;
|
|
77
|
+
while (end < text.length && text[end] !== '"') {
|
|
78
|
+
end += text[end] === '\\' ? 2 : 1;
|
|
79
|
+
}
|
|
80
|
+
out += text.slice(i, end + 1);
|
|
81
|
+
i = end + 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
pattern.lastIndex = i;
|
|
85
|
+
const match = pattern.exec(text);
|
|
86
|
+
if (match) {
|
|
87
|
+
out += replacement;
|
|
88
|
+
i += match[0].length;
|
|
89
|
+
} else {
|
|
90
|
+
out += text[i++];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function importConfig(file) {
|
|
97
|
+
// Loaded only for a TypeScript config: the hook starts on every edit.
|
|
98
|
+
const { createJiti } = await import('jiti');
|
|
99
|
+
const jiti = createJiti(file, { moduleCache: false });
|
|
100
|
+
const mod = await jiti.import(file);
|
|
101
|
+
return mod?.default ?? mod;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function readOxlintConfig(file) {
|
|
105
|
+
return /\.jsonc?$/.test(file)
|
|
106
|
+
? parseJsonc(readFileSync(file, 'utf8'))
|
|
107
|
+
: importConfig(file);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A config and the configs it extends, in the order Oxlint merges them. */
|
|
111
|
+
async function oxlintLayers(config, dir, seen = new Set()) {
|
|
112
|
+
const layers = [];
|
|
113
|
+
for (const base of config?.extends ?? []) {
|
|
114
|
+
if (typeof base === 'string') {
|
|
115
|
+
const file = resolve(dir, base);
|
|
116
|
+
if (seen.has(file)) continue;
|
|
117
|
+
seen.add(file);
|
|
118
|
+
layers.push(
|
|
119
|
+
...(await oxlintLayers(
|
|
120
|
+
await readOxlintConfig(file),
|
|
121
|
+
dirname(file),
|
|
122
|
+
seen,
|
|
123
|
+
)),
|
|
124
|
+
);
|
|
125
|
+
} else if (base && typeof base === 'object') {
|
|
126
|
+
layers.push(...(await oxlintLayers(base, dir, seen)));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (config && typeof config === 'object') layers.push(config);
|
|
130
|
+
return layers;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Whether a `jsPlugins` specifier loads this package. */
|
|
134
|
+
function isThisPlugin(specifier, dir, packageRoot) {
|
|
135
|
+
if (specifier === NAME || specifier.startsWith(`${NAME}/`)) return true;
|
|
136
|
+
if (!specifier.startsWith('.') && !isAbsolute(specifier)) return false;
|
|
137
|
+
try {
|
|
138
|
+
return realpathSync(resolve(dir, specifier)).startsWith(packageRoot + sep);
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The names an Oxlint config's `jsPlugins` give this plugin. */
|
|
145
|
+
function oxlintNames(layers, dir, packageRoot) {
|
|
146
|
+
const names = new Set();
|
|
147
|
+
const entries = layers.flatMap((layer) => [
|
|
148
|
+
...(layer.jsPlugins ?? []),
|
|
149
|
+
...(layer.overrides ?? []).flatMap((o) => o?.jsPlugins ?? []),
|
|
150
|
+
]);
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
const { name, specifier } =
|
|
153
|
+
typeof entry === 'string' ? { specifier: entry } : (entry ?? {});
|
|
154
|
+
if (
|
|
155
|
+
typeof specifier !== 'string' ||
|
|
156
|
+
!isThisPlugin(specifier, dir, packageRoot)
|
|
157
|
+
) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const n of typeof name === 'string' ? [name] : OXLINT_NAMES) {
|
|
161
|
+
names.add(n);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return names;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The rules of this plugin in a rule map, renamed to `tasty/<rule>`, with
|
|
169
|
+
* Oxlint's levels (`allow`, `deny`) written as ESLint's.
|
|
170
|
+
*/
|
|
171
|
+
export function tastyRules(rules, names, known) {
|
|
172
|
+
const out = {};
|
|
173
|
+
for (const [key, entry] of Object.entries(rules ?? {})) {
|
|
174
|
+
const slash = key.lastIndexOf('/');
|
|
175
|
+
const rule = key.slice(slash + 1);
|
|
176
|
+
if (slash < 0 || !names.has(key.slice(0, slash)) || !known.has(rule)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const [level, ...options] = Array.isArray(entry) ? entry : [entry];
|
|
180
|
+
const severity = LEVELS[level];
|
|
181
|
+
if (severity) {
|
|
182
|
+
out[`tasty/${rule}`] = options.length ? [severity, ...options] : severity;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** An Oxlint `files` pattern for ESLint: one without a slash matches at any depth. */
|
|
189
|
+
export function fileGlob(pattern) {
|
|
190
|
+
const glob = pattern.replace(/^\.\//, '');
|
|
191
|
+
return glob.includes('/') ? glob : `**/${glob}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** An Oxlint `ignorePatterns` entry, which follows `.gitignore`, for ESLint. */
|
|
195
|
+
export function ignoreGlobs(pattern) {
|
|
196
|
+
const negated = pattern.startsWith('!');
|
|
197
|
+
let glob = negated ? pattern.slice(1) : pattern;
|
|
198
|
+
const folder = glob.endsWith('/');
|
|
199
|
+
if (folder) glob = glob.slice(0, -1);
|
|
200
|
+
if (glob.startsWith('/')) glob = glob.slice(1);
|
|
201
|
+
else if (!glob.includes('/')) glob = `**/${glob}`;
|
|
202
|
+
return (folder ? [`${glob}/**`] : [glob, `${glob}/**`]).map((g) =>
|
|
203
|
+
negated ? `!${g}` : g,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The flat config that holds the Tasty part of an Oxlint config: its rules,
|
|
209
|
+
* the overrides that change them, `settings.tasty` and `ignorePatterns`.
|
|
210
|
+
* Undefined when the config does not load the plugin.
|
|
211
|
+
*/
|
|
212
|
+
export async function oxlintFlatConfig(file, { plugin, packageRoot }) {
|
|
213
|
+
const dir = dirname(file);
|
|
214
|
+
const layers = await oxlintLayers(
|
|
215
|
+
await readOxlintConfig(file),
|
|
216
|
+
dir,
|
|
217
|
+
new Set([file]),
|
|
218
|
+
);
|
|
219
|
+
const names = oxlintNames(layers, dir, packageRoot);
|
|
220
|
+
if (names.size === 0) return undefined;
|
|
221
|
+
const known = new Set(Object.keys(plugin.rules));
|
|
222
|
+
const settings = Object.assign(
|
|
223
|
+
{},
|
|
224
|
+
...layers.map((layer) => layer.settings?.tasty),
|
|
225
|
+
);
|
|
226
|
+
const ignores = layers
|
|
227
|
+
.flatMap((layer) => layer.ignorePatterns ?? [])
|
|
228
|
+
.flatMap(ignoreGlobs);
|
|
229
|
+
const overrides = layers.flatMap((layer) => layer.overrides ?? []);
|
|
230
|
+
return [
|
|
231
|
+
{
|
|
232
|
+
files: [SOURCES],
|
|
233
|
+
plugins: { tasty: plugin },
|
|
234
|
+
settings: { tasty: settings },
|
|
235
|
+
},
|
|
236
|
+
...(ignores.length > 0 ? [{ ignores }] : []),
|
|
237
|
+
...layers.map((layer) => ({
|
|
238
|
+
rules: tastyRules(layer.rules, names, known),
|
|
239
|
+
})),
|
|
240
|
+
...overrides.map((override) => ({
|
|
241
|
+
files: (override.files ?? []).map(fileGlob),
|
|
242
|
+
...(override.excludeFiles
|
|
243
|
+
? { ignores: override.excludeFiles.map(fileGlob) }
|
|
244
|
+
: {}),
|
|
245
|
+
rules: tastyRules(override.rules, names, known),
|
|
246
|
+
})),
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Finds and reads the project's Tasty lint config for each file.
|
|
252
|
+
*
|
|
253
|
+
* `forFile(file)` gives `{ source, rules, settings }`: `source` is the config
|
|
254
|
+
* (`{ linter, file, dir }`), or null when no config above the file loads the
|
|
255
|
+
* plugin, and then `rules` is null too. `ignored` is true when the config does
|
|
256
|
+
* not lint the file.
|
|
257
|
+
*/
|
|
258
|
+
export function createConfigResolver({ ESLint, plugin, packageRoot }) {
|
|
259
|
+
const known = new Set(Object.keys(plugin.rules));
|
|
260
|
+
const byDir = new Map();
|
|
261
|
+
|
|
262
|
+
async function sourceIn(dir) {
|
|
263
|
+
const oxlint = OXLINT_CONFIGS.map((n) => join(dir, n)).find(existsSync);
|
|
264
|
+
if (oxlint) {
|
|
265
|
+
const config = await oxlintFlatConfig(oxlint, { plugin, packageRoot });
|
|
266
|
+
if (config) {
|
|
267
|
+
return {
|
|
268
|
+
linter: 'oxlint',
|
|
269
|
+
file: oxlint,
|
|
270
|
+
dir,
|
|
271
|
+
eslint: new ESLint({
|
|
272
|
+
cwd: dir,
|
|
273
|
+
overrideConfigFile: true,
|
|
274
|
+
overrideConfig: config,
|
|
275
|
+
}),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// A text test first: loading an ESLint config loads every plugin it uses.
|
|
280
|
+
const eslint = ESLINT_CONFIGS.map((n) => join(dir, n)).find(existsSync);
|
|
281
|
+
if (eslint && readFileSync(eslint, 'utf8').includes(NAME)) {
|
|
282
|
+
return {
|
|
283
|
+
linter: 'eslint',
|
|
284
|
+
file: eslint,
|
|
285
|
+
dir,
|
|
286
|
+
eslint: new ESLint({ cwd: dir, overrideConfigFile: eslint }),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const parent = dirname(dir);
|
|
290
|
+
return parent === dir ? null : sourceFor(parent);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function sourceFor(dir) {
|
|
294
|
+
if (!byDir.has(dir)) byDir.set(dir, sourceIn(dir));
|
|
295
|
+
return byDir.get(dir);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
async forFile(file) {
|
|
300
|
+
const source = await sourceFor(dirname(file));
|
|
301
|
+
if (!source) return { source: null, rules: null, settings: {} };
|
|
302
|
+
const config = await source.eslint.calculateConfigForFile(file);
|
|
303
|
+
if (!config) return { source, ignored: true, rules: {}, settings: {} };
|
|
304
|
+
const names = new Set(
|
|
305
|
+
Object.entries(config.plugins ?? {})
|
|
306
|
+
.filter(([, p]) => p === plugin || p?.meta?.name === NAME)
|
|
307
|
+
.map(([name]) => name),
|
|
308
|
+
);
|
|
309
|
+
return {
|
|
310
|
+
source,
|
|
311
|
+
rules: tastyRules(config.rules, names, known),
|
|
312
|
+
settings: { ...config.settings?.tasty },
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "tasty-lint",
|
|
4
|
+
"description": "Lint Tasty styles: a rule to lint what you change, a page for every tasty/* rule, a tool that runs only the Tasty rules, and a hook that lints each edited file",
|
|
5
|
+
"homepage": "https://github.com/tenphi/eslint-plugin-tasty#readme",
|
|
6
|
+
"repository": "https://github.com/tenphi/eslint-plugin-tasty",
|
|
7
|
+
"keywords": ["tasty", "eslint", "oxlint", "lint", "styles"],
|
|
8
|
+
"extensions": {
|
|
9
|
+
"dev.harnesspm": {
|
|
10
|
+
"docs": "../docs",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"check": {
|
|
13
|
+
"file": "./dev.harnesspm/scripts/check.mjs",
|
|
14
|
+
"description": "Lint files or folders with only the tasty/* rules, as the project's Oxlint or ESLint config sets them (else the recommended preset, or --strict), and print each problem with its rule's page",
|
|
15
|
+
"allow": true
|
|
16
|
+
},
|
|
17
|
+
"check-edit": {
|
|
18
|
+
"file": "./dev.harnesspm/scripts/check-edit.mjs"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenphi/eslint-plugin-tasty",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.1",
|
|
4
4
|
"description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"docs",
|
|
17
|
+
"harness"
|
|
16
18
|
],
|
|
17
19
|
"sideEffects": false,
|
|
18
20
|
"engines": {
|
|
@@ -53,7 +55,8 @@
|
|
|
53
55
|
"@changesets/changelog-github": "^0.5.2",
|
|
54
56
|
"@changesets/cli": "^2.31.1",
|
|
55
57
|
"@eslint/js": "^10.0.1",
|
|
56
|
-
"@tenphi/
|
|
58
|
+
"@tenphi/harnesspm": "^0.2.0",
|
|
59
|
+
"@tenphi/tasty": "^3.19.0",
|
|
57
60
|
"@types/node": "^22.0.0",
|
|
58
61
|
"@types/semver": "^7.8.0",
|
|
59
62
|
"@typescript-eslint/parser": "^8.56.1",
|
|
@@ -85,10 +88,11 @@
|
|
|
85
88
|
"test:watch": "vitest",
|
|
86
89
|
"test:coverage": "vitest run --coverage",
|
|
87
90
|
"typecheck": "tsc --noEmit",
|
|
88
|
-
"lint": "eslint src",
|
|
89
|
-
"lint:fix": "eslint src --fix",
|
|
90
|
-
"format": "prettier --write \"src/**/*.ts\"",
|
|
91
|
-
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
91
|
+
"lint": "eslint src harness",
|
|
92
|
+
"lint:fix": "eslint src harness --fix",
|
|
93
|
+
"format": "prettier --write \"src/**/*.ts\" \"harness/**/*.mjs\"",
|
|
94
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"harness/**/*.mjs\"",
|
|
95
|
+
"check:harness": "harnesspm check",
|
|
92
96
|
"generate:css-properties": "node scripts/generate-css-properties.mjs",
|
|
93
97
|
"changeset": "changeset",
|
|
94
98
|
"version": "changeset version",
|