@x12i/kit-cli 0.0.0-stage → 1.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/README.md +48 -2
- package/dist/cli.js +316 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +309 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 x12i
|
|
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/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @x12i/kit-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command line for the Unified UI Kit. The binary is called `kit`.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx @x12i/kit-cli doctor
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
| Command | Status |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `kit init` | Writes `kit.config.json` (`--theme`, `--density`, `--force`). |
|
|
12
|
+
| `kit doctor [paths...]` | Finds hard-coded hex colors in CSS, TS, JS, HTML, Vue, Svelte and MDX files. Exits with 1 when it finds any, so it can gate CI. |
|
|
13
|
+
| `kit theme [name]` | Lists the built-in themes and densities. Generating a theme is planned. |
|
|
14
|
+
| `kit add <component...>` | Stub. The component registry is planned, so it exits with 1 and says so. |
|
|
15
|
+
|
|
16
|
+
Exit codes: `0` success, `1` the command ran and found a problem, `2` bad usage
|
|
17
|
+
(unknown command, option, theme or path).
|
|
18
|
+
|
|
19
|
+
## doctor
|
|
20
|
+
|
|
21
|
+
`kit doctor` is a minimal scan. It finds `#rgb`, `#rgba`, `#rrggbb` and
|
|
22
|
+
`#rrggbbaa` values. It skips `node_modules`, `dist`, `build`, `coverage` and
|
|
23
|
+
other build folders, minified files and `.d.ts` files. It does not flag URL
|
|
24
|
+
fragments, `href="#anchor"` links or HTML entities.
|
|
25
|
+
|
|
26
|
+
- Add `kit-ignore` to a line (in a comment) to keep a color on purpose, such as a
|
|
27
|
+
brand logo.
|
|
28
|
+
- List paths to skip in `kit.config.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"version": 1,
|
|
33
|
+
"theme": "light",
|
|
34
|
+
"density": "default",
|
|
35
|
+
"doctor": { "ignore": ["legacy", "src/vendor"] }
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- `--json` prints the findings as JSON.
|
|
40
|
+
|
|
41
|
+
Spacing, font sizes and `rgb()` or `hsl()` colors are not scanned yet.
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { scanForHardcodedColors } from "@x12i/kit-cli";
|
|
47
|
+
|
|
48
|
+
const findings = await scanForHardcodedColors(process.cwd(), ["src"]);
|
|
49
|
+
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { writeFile, readFile, stat, access, readdir } from 'fs/promises';
|
|
4
|
+
import path2 from 'path';
|
|
5
|
+
import { parseArgs } from 'util';
|
|
6
|
+
import { themeNames, densityNames } from '@x12i/tokens';
|
|
7
|
+
|
|
8
|
+
var CONFIG_FILE = "kit.config.json";
|
|
9
|
+
var CONFIG_VERSION = 1;
|
|
10
|
+
function isThemeName(value) {
|
|
11
|
+
return themeNames.includes(value);
|
|
12
|
+
}
|
|
13
|
+
function isDensityName(value) {
|
|
14
|
+
return densityNames.includes(value);
|
|
15
|
+
}
|
|
16
|
+
function createConfig(options = {}) {
|
|
17
|
+
return {
|
|
18
|
+
version: CONFIG_VERSION,
|
|
19
|
+
theme: options.theme ?? "light",
|
|
20
|
+
density: options.density ?? "default",
|
|
21
|
+
doctor: { ignore: [] }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function loadConfig(cwd) {
|
|
25
|
+
let text;
|
|
26
|
+
try {
|
|
27
|
+
text = await readFile(path2.join(cwd, CONFIG_FILE), "utf8");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code === "ENOENT") return null;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
let raw;
|
|
33
|
+
try {
|
|
34
|
+
raw = JSON.parse(text);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error(`${CONFIG_FILE} is not valid JSON.`);
|
|
37
|
+
}
|
|
38
|
+
const input = typeof raw === "object" && raw !== null ? raw : {};
|
|
39
|
+
const defaults = createConfig();
|
|
40
|
+
const ignore = input.doctor?.ignore;
|
|
41
|
+
return {
|
|
42
|
+
version: CONFIG_VERSION,
|
|
43
|
+
theme: typeof input.theme === "string" && isThemeName(input.theme) ? input.theme : defaults.theme,
|
|
44
|
+
density: typeof input.density === "string" && isDensityName(input.density) ? input.density : defaults.density,
|
|
45
|
+
doctor: {
|
|
46
|
+
ignore: Array.isArray(ignore) ? ignore.filter((item) => typeof item === "string") : []
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
var SCANNED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
51
|
+
".css",
|
|
52
|
+
".scss",
|
|
53
|
+
".sass",
|
|
54
|
+
".less",
|
|
55
|
+
".ts",
|
|
56
|
+
".tsx",
|
|
57
|
+
".js",
|
|
58
|
+
".jsx",
|
|
59
|
+
".mjs",
|
|
60
|
+
".cjs",
|
|
61
|
+
".html",
|
|
62
|
+
".vue",
|
|
63
|
+
".svelte",
|
|
64
|
+
".mdx"
|
|
65
|
+
]);
|
|
66
|
+
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
67
|
+
"node_modules",
|
|
68
|
+
"dist",
|
|
69
|
+
"build",
|
|
70
|
+
"out",
|
|
71
|
+
"coverage",
|
|
72
|
+
".git",
|
|
73
|
+
".next",
|
|
74
|
+
".turbo",
|
|
75
|
+
".svelte-kit"
|
|
76
|
+
]);
|
|
77
|
+
var MAX_FILE_BYTES = 1e6;
|
|
78
|
+
var HEX_COLOR = /(?<![\w&#/-])(?<!\bhref=["'])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![\w-])/g;
|
|
79
|
+
var IGNORE_MARKER = "kit-ignore";
|
|
80
|
+
var toPosix = (value) => value.split(path2.sep).join("/");
|
|
81
|
+
function isIgnored(relativePath, ignore) {
|
|
82
|
+
return ignore.some((entry) => {
|
|
83
|
+
const prefix = entry.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
84
|
+
return prefix !== "" && (relativePath === prefix || relativePath.startsWith(`${prefix}/`));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function findHexColors(text) {
|
|
88
|
+
const findings = [];
|
|
89
|
+
const lines = text.split(/\r?\n/);
|
|
90
|
+
lines.forEach((line, index) => {
|
|
91
|
+
if (line.includes(IGNORE_MARKER)) return;
|
|
92
|
+
for (const found of line.matchAll(HEX_COLOR)) {
|
|
93
|
+
findings.push({ line: index + 1, column: (found.index ?? 0) + 1, match: found[0] });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return findings;
|
|
97
|
+
}
|
|
98
|
+
async function* walk(directory) {
|
|
99
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
100
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (entry.isSymbolicLink()) continue;
|
|
103
|
+
const full = path2.join(directory, entry.name);
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) yield* walk(full);
|
|
106
|
+
} else if (entry.isFile()) {
|
|
107
|
+
yield full;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function isScannable(file) {
|
|
112
|
+
const name = path2.basename(file);
|
|
113
|
+
if (name.includes(".min.") || name.endsWith(".d.ts")) return false;
|
|
114
|
+
return SCANNED_EXTENSIONS.has(path2.extname(name));
|
|
115
|
+
}
|
|
116
|
+
async function scanForHardcodedColors(cwd, targets = ["."], { ignore = [] } = {}) {
|
|
117
|
+
const findings = [];
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const target of targets) {
|
|
120
|
+
const root = path2.resolve(cwd, target);
|
|
121
|
+
const info = await stat(root);
|
|
122
|
+
const files = info.isDirectory() ? walk(root) : (async function* () {
|
|
123
|
+
yield root;
|
|
124
|
+
})();
|
|
125
|
+
for await (const file of files) {
|
|
126
|
+
if (seen.has(file)) continue;
|
|
127
|
+
seen.add(file);
|
|
128
|
+
const relative = toPosix(path2.relative(cwd, file));
|
|
129
|
+
if (isIgnored(relative, ignore) || !isScannable(file)) continue;
|
|
130
|
+
const { size } = await stat(file);
|
|
131
|
+
if (size > MAX_FILE_BYTES) continue;
|
|
132
|
+
const text = await readFile(file, "utf8");
|
|
133
|
+
for (const finding of findHexColors(text)) {
|
|
134
|
+
findings.push({ file: relative, ...finding });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return findings;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/main.ts
|
|
142
|
+
var HELP = `kit: command line for the Unified UI Kit
|
|
143
|
+
|
|
144
|
+
Usage
|
|
145
|
+
kit <command> [options]
|
|
146
|
+
|
|
147
|
+
Commands
|
|
148
|
+
init [--theme <name>] [--density <name>] [--force]
|
|
149
|
+
Write ${CONFIG_FILE} in the current folder.
|
|
150
|
+
add <component...>
|
|
151
|
+
Add a component to your project. Planned.
|
|
152
|
+
theme [name]
|
|
153
|
+
List the themes and densities. Generating a theme is planned.
|
|
154
|
+
doctor [paths...] [--json]
|
|
155
|
+
Find hard-coded hex colors that should be --kit- tokens.
|
|
156
|
+
Exits with 1 when it finds any.
|
|
157
|
+
|
|
158
|
+
Options
|
|
159
|
+
-h, --help Show this help.
|
|
160
|
+
-v, --version Show the version.
|
|
161
|
+
`;
|
|
162
|
+
function readVersion() {
|
|
163
|
+
const file = new URL("../package.json", import.meta.url);
|
|
164
|
+
return JSON.parse(readFileSync(file, "utf8")).version;
|
|
165
|
+
}
|
|
166
|
+
async function exists(file) {
|
|
167
|
+
try {
|
|
168
|
+
await access(file);
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function init(values, io) {
|
|
175
|
+
if (values.theme !== void 0 && !isThemeName(values.theme)) {
|
|
176
|
+
io.err(`Unknown theme "${values.theme}". Choose one of: ${themeNames.join(", ")}.
|
|
177
|
+
`);
|
|
178
|
+
return 2;
|
|
179
|
+
}
|
|
180
|
+
if (values.density !== void 0 && !isDensityName(values.density)) {
|
|
181
|
+
io.err(`Unknown density "${values.density}". Choose one of: ${densityNames.join(", ")}.
|
|
182
|
+
`);
|
|
183
|
+
return 2;
|
|
184
|
+
}
|
|
185
|
+
const target = path2.join(io.cwd, CONFIG_FILE);
|
|
186
|
+
if (!values.force && await exists(target)) {
|
|
187
|
+
io.err(`${CONFIG_FILE} already exists. Use --force to replace it.
|
|
188
|
+
`);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
const config = createConfig({ theme: values.theme, density: values.density });
|
|
192
|
+
await writeFile(target, `${JSON.stringify(config, null, 2)}
|
|
193
|
+
`, "utf8");
|
|
194
|
+
io.out(`Created ${CONFIG_FILE}
|
|
195
|
+
`);
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
function add(components, io) {
|
|
199
|
+
const wanted = components.length > 0 ? ` (${components.join(", ")})` : "";
|
|
200
|
+
io.err(
|
|
201
|
+
`kit add${wanted} is not available yet. The component registry is planned. Until then, import components from @x12i/ui.
|
|
202
|
+
`
|
|
203
|
+
);
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
function theme(name, io) {
|
|
207
|
+
if (name === void 0) {
|
|
208
|
+
io.out(`Themes: ${themeNames.join(", ")}
|
|
209
|
+
`);
|
|
210
|
+
io.out(`Densities: ${densityNames.join(", ")}
|
|
211
|
+
`);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
if (isThemeName(name)) {
|
|
215
|
+
io.out(`"${name}" is built in. Import @x12i/themes/${name}.css, or set data-theme="${name}" on <html>.
|
|
216
|
+
`);
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
io.err(
|
|
220
|
+
`Generating a custom theme is planned and is not available yet. Built-in themes: ${themeNames.join(", ")}.
|
|
221
|
+
`
|
|
222
|
+
);
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
async function doctor(paths, json, io) {
|
|
226
|
+
const config = await loadConfig(io.cwd);
|
|
227
|
+
const targets = paths.length > 0 ? paths : ["."];
|
|
228
|
+
let findings;
|
|
229
|
+
try {
|
|
230
|
+
findings = await scanForHardcodedColors(io.cwd, targets, { ignore: config?.doctor.ignore });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error.code === "ENOENT") {
|
|
233
|
+
io.err(`Path not found: ${error.path ?? targets.join(", ")}
|
|
234
|
+
`);
|
|
235
|
+
return 2;
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
if (json) {
|
|
240
|
+
io.out(`${JSON.stringify(findings, null, 2)}
|
|
241
|
+
`);
|
|
242
|
+
return findings.length > 0 ? 1 : 0;
|
|
243
|
+
}
|
|
244
|
+
if (findings.length === 0) {
|
|
245
|
+
io.out("No hard-coded colors found.\n");
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
for (const finding of findings) {
|
|
249
|
+
io.out(`${finding.file}:${finding.line}:${finding.column} ${finding.match} Use a --kit- color token.
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
const files = new Set(findings.map((finding) => finding.file)).size;
|
|
253
|
+
io.out(
|
|
254
|
+
`
|
|
255
|
+
${findings.length} hard-coded ${findings.length === 1 ? "color" : "colors"} found in ${files} ${files === 1 ? "file" : "files"}.
|
|
256
|
+
`
|
|
257
|
+
);
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
async function run(argv, io) {
|
|
261
|
+
let parsed;
|
|
262
|
+
try {
|
|
263
|
+
parsed = parseArgs({
|
|
264
|
+
args: argv,
|
|
265
|
+
allowPositionals: true,
|
|
266
|
+
options: {
|
|
267
|
+
help: { type: "boolean", short: "h" },
|
|
268
|
+
version: { type: "boolean", short: "v" },
|
|
269
|
+
force: { type: "boolean" },
|
|
270
|
+
json: { type: "boolean" },
|
|
271
|
+
theme: { type: "string" },
|
|
272
|
+
density: { type: "string" }
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
io.err(`${error.message}
|
|
277
|
+
|
|
278
|
+
${HELP}`);
|
|
279
|
+
return 2;
|
|
280
|
+
}
|
|
281
|
+
const { values, positionals } = parsed;
|
|
282
|
+
const [command, ...rest] = positionals;
|
|
283
|
+
if (values.version) {
|
|
284
|
+
io.out(`${readVersion()}
|
|
285
|
+
`);
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
if (values.help || command === void 0 || command === "help") {
|
|
289
|
+
io.out(HELP);
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
switch (command) {
|
|
293
|
+
case "init":
|
|
294
|
+
return init(values, io);
|
|
295
|
+
case "add":
|
|
296
|
+
return add(rest, io);
|
|
297
|
+
case "theme":
|
|
298
|
+
return theme(rest[0], io);
|
|
299
|
+
case "doctor":
|
|
300
|
+
return doctor(rest, values.json ?? false, io);
|
|
301
|
+
default:
|
|
302
|
+
io.err(`Unknown command "${command}".
|
|
303
|
+
|
|
304
|
+
${HELP}`);
|
|
305
|
+
return 2;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/cli.ts
|
|
310
|
+
process.exitCode = await run(process.argv.slice(2), {
|
|
311
|
+
cwd: process.cwd(),
|
|
312
|
+
out: (text) => process.stdout.write(text),
|
|
313
|
+
err: (text) => process.stderr.write(text)
|
|
314
|
+
});
|
|
315
|
+
//# sourceMappingURL=cli.js.map
|
|
316
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/doctor.ts","../src/main.ts","../src/cli.ts"],"names":["path","readFile","themeNames","densityNames"],"mappings":"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","file":"cli.js","sourcesContent":["import { readFile } from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { densityNames, themeNames, type DensityName, type ThemeName } from \"@x12i/tokens\";\r\n\r\nexport const CONFIG_FILE = \"kit.config.json\";\r\nexport const CONFIG_VERSION = 1;\r\n\r\nexport interface KitConfig {\r\n version: typeof CONFIG_VERSION;\r\n /** The theme the project starts with. */\r\n theme: ThemeName;\r\n density: DensityName;\r\n doctor: {\r\n /** Paths, relative to the config file, that `kit doctor` skips. */\r\n ignore: string[];\r\n };\r\n}\r\n\r\nexport function isThemeName(value: string): value is ThemeName {\r\n return (themeNames as readonly string[]).includes(value);\r\n}\r\n\r\nexport function isDensityName(value: string): value is DensityName {\r\n return (densityNames as readonly string[]).includes(value);\r\n}\r\n\r\nexport function createConfig(options: { theme?: ThemeName; density?: DensityName } = {}): KitConfig {\r\n return {\r\n version: CONFIG_VERSION,\r\n theme: options.theme ?? \"light\",\r\n density: options.density ?? \"default\",\r\n doctor: { ignore: [] },\r\n };\r\n}\r\n\r\n/** Reads `kit.config.json` from `cwd`. Returns `null` when there is none. */\r\nexport async function loadConfig(cwd: string): Promise<KitConfig | null> {\r\n let text: string;\r\n try {\r\n text = await readFile(path.join(cwd, CONFIG_FILE), \"utf8\");\r\n } catch (error) {\r\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\r\n throw error;\r\n }\r\n\r\n let raw: unknown;\r\n try {\r\n raw = JSON.parse(text);\r\n } catch {\r\n throw new Error(`${CONFIG_FILE} is not valid JSON.`);\r\n }\r\n\r\n const input = (typeof raw === \"object\" && raw !== null ? raw : {}) as Partial<KitConfig>;\r\n const defaults = createConfig();\r\n const ignore = input.doctor?.ignore;\r\n\r\n return {\r\n version: CONFIG_VERSION,\r\n theme: typeof input.theme === \"string\" && isThemeName(input.theme) ? input.theme : defaults.theme,\r\n density:\r\n typeof input.density === \"string\" && isDensityName(input.density) ? input.density : defaults.density,\r\n doctor: {\r\n ignore: Array.isArray(ignore) ? ignore.filter((item): item is string => typeof item === \"string\") : [],\r\n },\r\n };\r\n}\r\n","import { readdir, readFile, stat } from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\n\r\nexport interface Finding {\r\n /** Path relative to the scan root, with forward slashes. */\r\n file: string;\r\n line: number;\r\n column: number;\r\n /** The text that matched, such as `#ff0000`. */\r\n match: string;\r\n}\r\n\r\nexport interface ScanOptions {\r\n /** Paths, relative to `cwd`, to skip. A prefix skips everything under it. */\r\n ignore?: string[];\r\n}\r\n\r\nconst SCANNED_EXTENSIONS = new Set([\r\n \".css\",\r\n \".scss\",\r\n \".sass\",\r\n \".less\",\r\n \".ts\",\r\n \".tsx\",\r\n \".js\",\r\n \".jsx\",\r\n \".mjs\",\r\n \".cjs\",\r\n \".html\",\r\n \".vue\",\r\n \".svelte\",\r\n \".mdx\",\r\n]);\r\n\r\nconst SKIPPED_DIRECTORIES = new Set([\r\n \"node_modules\",\r\n \"dist\",\r\n \"build\",\r\n \"out\",\r\n \"coverage\",\r\n \".git\",\r\n \".next\",\r\n \".turbo\",\r\n \".svelte-kit\",\r\n]);\r\n\r\nconst MAX_FILE_BYTES = 1_000_000;\r\n\r\n/**\r\n * 3, 4, 6 or 8 hex digits after a `#`. The lookbehind skips URL fragments such\r\n * as `page#abc`, HTML entities such as `𞉀` and `href=\"#abc\"` anchors.\r\n */\r\nconst HEX_COLOR = /(?<![\\w&#/-])(?<!\\bhref=[\"'])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![\\w-])/g;\r\n\r\n/** A line that contains this text is skipped, for a color that is meant to stay. */\r\nexport const IGNORE_MARKER = \"kit-ignore\";\r\n\r\nconst toPosix = (value: string) => value.split(path.sep).join(\"/\");\r\n\r\nfunction isIgnored(relativePath: string, ignore: string[]): boolean {\r\n return ignore.some((entry) => {\r\n const prefix = entry.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/+$/, \"\");\r\n return prefix !== \"\" && (relativePath === prefix || relativePath.startsWith(`${prefix}/`));\r\n });\r\n}\r\n\r\n/** Finds hard-coded hex colors in one piece of text. */\r\nexport function findHexColors(text: string): Array<Omit<Finding, \"file\">> {\r\n const findings: Array<Omit<Finding, \"file\">> = [];\r\n const lines = text.split(/\\r?\\n/);\r\n\r\n lines.forEach((line, index) => {\r\n if (line.includes(IGNORE_MARKER)) return;\r\n for (const found of line.matchAll(HEX_COLOR)) {\r\n findings.push({ line: index + 1, column: (found.index ?? 0) + 1, match: found[0] });\r\n }\r\n });\r\n\r\n return findings;\r\n}\r\n\r\nasync function* walk(directory: string): AsyncGenerator<string> {\r\n const entries = await readdir(directory, { withFileTypes: true });\r\n entries.sort((a, b) => a.name.localeCompare(b.name));\r\n\r\n for (const entry of entries) {\r\n // Symlinks are skipped so a link back up the tree cannot loop forever.\r\n if (entry.isSymbolicLink()) continue;\r\n const full = path.join(directory, entry.name);\r\n\r\n if (entry.isDirectory()) {\r\n if (!SKIPPED_DIRECTORIES.has(entry.name)) yield* walk(full);\r\n } else if (entry.isFile()) {\r\n yield full;\r\n }\r\n }\r\n}\r\n\r\nfunction isScannable(file: string): boolean {\r\n const name = path.basename(file);\r\n if (name.includes(\".min.\") || name.endsWith(\".d.ts\")) return false;\r\n return SCANNED_EXTENSIONS.has(path.extname(name));\r\n}\r\n\r\n/**\r\n * Scans files and folders for hard-coded hex colors. Paths that do not exist\r\n * throw, so a typo in a path is not reported as a clean result.\r\n */\r\nexport async function scanForHardcodedColors(\r\n cwd: string,\r\n targets: string[] = [\".\"],\r\n { ignore = [] }: ScanOptions = {},\r\n): Promise<Finding[]> {\r\n const findings: Finding[] = [];\r\n const seen = new Set<string>();\r\n\r\n for (const target of targets) {\r\n const root = path.resolve(cwd, target);\r\n const info = await stat(root);\r\n const files = info.isDirectory() ? walk(root) : (async function* () { yield root; })();\r\n\r\n for await (const file of files) {\r\n if (seen.has(file)) continue;\r\n seen.add(file);\r\n\r\n const relative = toPosix(path.relative(cwd, file));\r\n if (isIgnored(relative, ignore) || !isScannable(file)) continue;\r\n\r\n const { size } = await stat(file);\r\n if (size > MAX_FILE_BYTES) continue;\r\n\r\n const text = await readFile(file, \"utf8\");\r\n for (const finding of findHexColors(text)) {\r\n findings.push({ file: relative, ...finding });\r\n }\r\n }\r\n }\r\n\r\n return findings;\r\n}\r\n","import { readFileSync } from \"node:fs\";\r\nimport { access, writeFile } from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { parseArgs } from \"node:util\";\r\nimport { densityNames, themeNames } from \"@x12i/tokens\";\r\nimport { CONFIG_FILE, createConfig, isDensityName, isThemeName, loadConfig } from \"./config\";\r\nimport { scanForHardcodedColors } from \"./doctor\";\r\n\r\nexport interface Io {\r\n cwd: string;\r\n out: (text: string) => void;\r\n err: (text: string) => void;\r\n}\r\n\r\n/** Exit codes: 0 success, 1 the command ran and found a problem, 2 bad usage. */\r\ntype ExitCode = 0 | 1 | 2;\r\n\r\nconst HELP = `kit: command line for the Unified UI Kit\r\n\r\nUsage\r\n kit <command> [options]\r\n\r\nCommands\r\n init [--theme <name>] [--density <name>] [--force]\r\n Write ${CONFIG_FILE} in the current folder.\r\n add <component...>\r\n Add a component to your project. Planned.\r\n theme [name]\r\n List the themes and densities. Generating a theme is planned.\r\n doctor [paths...] [--json]\r\n Find hard-coded hex colors that should be --kit- tokens.\r\n Exits with 1 when it finds any.\r\n\r\nOptions\r\n -h, --help Show this help.\r\n -v, --version Show the version.\r\n`;\r\n\r\nfunction readVersion(): string {\r\n const file = new URL(\"../package.json\", import.meta.url);\r\n return (JSON.parse(readFileSync(file, \"utf8\")) as { version: string }).version;\r\n}\r\n\r\nasync function exists(file: string): Promise<boolean> {\r\n try {\r\n await access(file);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nasync function init(values: { theme?: string; density?: string; force?: boolean }, io: Io): Promise<ExitCode> {\r\n if (values.theme !== undefined && !isThemeName(values.theme)) {\r\n io.err(`Unknown theme \"${values.theme}\". Choose one of: ${themeNames.join(\", \")}.\\n`);\r\n return 2;\r\n }\r\n if (values.density !== undefined && !isDensityName(values.density)) {\r\n io.err(`Unknown density \"${values.density}\". Choose one of: ${densityNames.join(\", \")}.\\n`);\r\n return 2;\r\n }\r\n\r\n const target = path.join(io.cwd, CONFIG_FILE);\r\n if (!values.force && (await exists(target))) {\r\n io.err(`${CONFIG_FILE} already exists. Use --force to replace it.\\n`);\r\n return 1;\r\n }\r\n\r\n const config = createConfig({ theme: values.theme, density: values.density });\r\n await writeFile(target, `${JSON.stringify(config, null, 2)}\\n`, \"utf8\");\r\n io.out(`Created ${CONFIG_FILE}\\n`);\r\n return 0;\r\n}\r\n\r\nfunction add(components: string[], io: Io): ExitCode {\r\n const wanted = components.length > 0 ? ` (${components.join(\", \")})` : \"\";\r\n io.err(\r\n `kit add${wanted} is not available yet. The component registry is planned. ` +\r\n `Until then, import components from @x12i/ui.\\n`,\r\n );\r\n return 1;\r\n}\r\n\r\nfunction theme(name: string | undefined, io: Io): ExitCode {\r\n if (name === undefined) {\r\n io.out(`Themes: ${themeNames.join(\", \")}\\n`);\r\n io.out(`Densities: ${densityNames.join(\", \")}\\n`);\r\n return 0;\r\n }\r\n\r\n if (isThemeName(name)) {\r\n io.out(`\"${name}\" is built in. Import @x12i/themes/${name}.css, or set data-theme=\"${name}\" on <html>.\\n`);\r\n return 0;\r\n }\r\n\r\n io.err(\r\n `Generating a custom theme is planned and is not available yet. ` +\r\n `Built-in themes: ${themeNames.join(\", \")}.\\n`,\r\n );\r\n return 1;\r\n}\r\n\r\nasync function doctor(paths: string[], json: boolean, io: Io): Promise<ExitCode> {\r\n const config = await loadConfig(io.cwd);\r\n const targets = paths.length > 0 ? paths : [\".\"];\r\n\r\n let findings;\r\n try {\r\n findings = await scanForHardcodedColors(io.cwd, targets, { ignore: config?.doctor.ignore });\r\n } catch (error) {\r\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\r\n io.err(`Path not found: ${(error as NodeJS.ErrnoException).path ?? targets.join(\", \")}\\n`);\r\n return 2;\r\n }\r\n throw error;\r\n }\r\n\r\n if (json) {\r\n io.out(`${JSON.stringify(findings, null, 2)}\\n`);\r\n return findings.length > 0 ? 1 : 0;\r\n }\r\n\r\n if (findings.length === 0) {\r\n io.out(\"No hard-coded colors found.\\n\");\r\n return 0;\r\n }\r\n\r\n for (const finding of findings) {\r\n io.out(`${finding.file}:${finding.line}:${finding.column} ${finding.match} Use a --kit- color token.\\n`);\r\n }\r\n const files = new Set(findings.map((finding) => finding.file)).size;\r\n io.out(\r\n `\\n${findings.length} hard-coded ${findings.length === 1 ? \"color\" : \"colors\"} ` +\r\n `found in ${files} ${files === 1 ? \"file\" : \"files\"}.\\n`,\r\n );\r\n return 1;\r\n}\r\n\r\n/** Runs the command line and returns the exit code. */\r\nexport async function run(argv: string[], io: Io): Promise<ExitCode> {\r\n let parsed;\r\n try {\r\n parsed = parseArgs({\r\n args: argv,\r\n allowPositionals: true,\r\n options: {\r\n help: { type: \"boolean\", short: \"h\" },\r\n version: { type: \"boolean\", short: \"v\" },\r\n force: { type: \"boolean\" },\r\n json: { type: \"boolean\" },\r\n theme: { type: \"string\" },\r\n density: { type: \"string\" },\r\n },\r\n });\r\n } catch (error) {\r\n io.err(`${(error as Error).message}\\n\\n${HELP}`);\r\n return 2;\r\n }\r\n\r\n const { values, positionals } = parsed;\r\n const [command, ...rest] = positionals;\r\n\r\n if (values.version) {\r\n io.out(`${readVersion()}\\n`);\r\n return 0;\r\n }\r\n if (values.help || command === undefined || command === \"help\") {\r\n io.out(HELP);\r\n return 0;\r\n }\r\n\r\n switch (command) {\r\n case \"init\":\r\n return init(values, io);\r\n case \"add\":\r\n return add(rest, io);\r\n case \"theme\":\r\n return theme(rest[0], io);\r\n case \"doctor\":\r\n return doctor(rest, values.json ?? false, io);\r\n default:\r\n io.err(`Unknown command \"${command}\".\\n\\n${HELP}`);\r\n return 2;\r\n }\r\n}\r\n","#!/usr/bin/env node\r\nimport { run } from \"./main\";\r\n\r\nprocess.exitCode = await run(process.argv.slice(2), {\r\n cwd: process.cwd(),\r\n out: (text) => process.stdout.write(text),\r\n err: (text) => process.stderr.write(text),\r\n});\r\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ThemeName, DensityName } from '@x12i/tokens';
|
|
2
|
+
|
|
3
|
+
declare const CONFIG_FILE = "kit.config.json";
|
|
4
|
+
declare const CONFIG_VERSION = 1;
|
|
5
|
+
interface KitConfig {
|
|
6
|
+
version: typeof CONFIG_VERSION;
|
|
7
|
+
/** The theme the project starts with. */
|
|
8
|
+
theme: ThemeName;
|
|
9
|
+
density: DensityName;
|
|
10
|
+
doctor: {
|
|
11
|
+
/** Paths, relative to the config file, that `kit doctor` skips. */
|
|
12
|
+
ignore: string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
declare function createConfig(options?: {
|
|
16
|
+
theme?: ThemeName;
|
|
17
|
+
density?: DensityName;
|
|
18
|
+
}): KitConfig;
|
|
19
|
+
/** Reads `kit.config.json` from `cwd`. Returns `null` when there is none. */
|
|
20
|
+
declare function loadConfig(cwd: string): Promise<KitConfig | null>;
|
|
21
|
+
|
|
22
|
+
interface Finding {
|
|
23
|
+
/** Path relative to the scan root, with forward slashes. */
|
|
24
|
+
file: string;
|
|
25
|
+
line: number;
|
|
26
|
+
column: number;
|
|
27
|
+
/** The text that matched, such as `#ff0000`. */
|
|
28
|
+
match: string;
|
|
29
|
+
}
|
|
30
|
+
interface ScanOptions {
|
|
31
|
+
/** Paths, relative to `cwd`, to skip. A prefix skips everything under it. */
|
|
32
|
+
ignore?: string[];
|
|
33
|
+
}
|
|
34
|
+
/** A line that contains this text is skipped, for a color that is meant to stay. */
|
|
35
|
+
declare const IGNORE_MARKER = "kit-ignore";
|
|
36
|
+
/** Finds hard-coded hex colors in one piece of text. */
|
|
37
|
+
declare function findHexColors(text: string): Array<Omit<Finding, "file">>;
|
|
38
|
+
/**
|
|
39
|
+
* Scans files and folders for hard-coded hex colors. Paths that do not exist
|
|
40
|
+
* throw, so a typo in a path is not reported as a clean result.
|
|
41
|
+
*/
|
|
42
|
+
declare function scanForHardcodedColors(cwd: string, targets?: string[], { ignore }?: ScanOptions): Promise<Finding[]>;
|
|
43
|
+
|
|
44
|
+
interface Io {
|
|
45
|
+
cwd: string;
|
|
46
|
+
out: (text: string) => void;
|
|
47
|
+
err: (text: string) => void;
|
|
48
|
+
}
|
|
49
|
+
/** Exit codes: 0 success, 1 the command ran and found a problem, 2 bad usage. */
|
|
50
|
+
type ExitCode = 0 | 1 | 2;
|
|
51
|
+
/** Runs the command line and returns the exit code. */
|
|
52
|
+
declare function run(argv: string[], io: Io): Promise<ExitCode>;
|
|
53
|
+
|
|
54
|
+
export { CONFIG_FILE, CONFIG_VERSION, IGNORE_MARKER, createConfig, findHexColors, loadConfig, run, scanForHardcodedColors };
|
|
55
|
+
export type { Finding, Io, KitConfig, ScanOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { readFile, stat, readdir, writeFile, access } from 'fs/promises';
|
|
2
|
+
import path2 from 'path';
|
|
3
|
+
import { densityNames, themeNames } from '@x12i/tokens';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { parseArgs } from 'util';
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
var CONFIG_FILE = "kit.config.json";
|
|
9
|
+
var CONFIG_VERSION = 1;
|
|
10
|
+
function isThemeName(value) {
|
|
11
|
+
return themeNames.includes(value);
|
|
12
|
+
}
|
|
13
|
+
function isDensityName(value) {
|
|
14
|
+
return densityNames.includes(value);
|
|
15
|
+
}
|
|
16
|
+
function createConfig(options = {}) {
|
|
17
|
+
return {
|
|
18
|
+
version: CONFIG_VERSION,
|
|
19
|
+
theme: options.theme ?? "light",
|
|
20
|
+
density: options.density ?? "default",
|
|
21
|
+
doctor: { ignore: [] }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function loadConfig(cwd) {
|
|
25
|
+
let text;
|
|
26
|
+
try {
|
|
27
|
+
text = await readFile(path2.join(cwd, CONFIG_FILE), "utf8");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code === "ENOENT") return null;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
let raw;
|
|
33
|
+
try {
|
|
34
|
+
raw = JSON.parse(text);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error(`${CONFIG_FILE} is not valid JSON.`);
|
|
37
|
+
}
|
|
38
|
+
const input = typeof raw === "object" && raw !== null ? raw : {};
|
|
39
|
+
const defaults = createConfig();
|
|
40
|
+
const ignore = input.doctor?.ignore;
|
|
41
|
+
return {
|
|
42
|
+
version: CONFIG_VERSION,
|
|
43
|
+
theme: typeof input.theme === "string" && isThemeName(input.theme) ? input.theme : defaults.theme,
|
|
44
|
+
density: typeof input.density === "string" && isDensityName(input.density) ? input.density : defaults.density,
|
|
45
|
+
doctor: {
|
|
46
|
+
ignore: Array.isArray(ignore) ? ignore.filter((item) => typeof item === "string") : []
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
var SCANNED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
51
|
+
".css",
|
|
52
|
+
".scss",
|
|
53
|
+
".sass",
|
|
54
|
+
".less",
|
|
55
|
+
".ts",
|
|
56
|
+
".tsx",
|
|
57
|
+
".js",
|
|
58
|
+
".jsx",
|
|
59
|
+
".mjs",
|
|
60
|
+
".cjs",
|
|
61
|
+
".html",
|
|
62
|
+
".vue",
|
|
63
|
+
".svelte",
|
|
64
|
+
".mdx"
|
|
65
|
+
]);
|
|
66
|
+
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
67
|
+
"node_modules",
|
|
68
|
+
"dist",
|
|
69
|
+
"build",
|
|
70
|
+
"out",
|
|
71
|
+
"coverage",
|
|
72
|
+
".git",
|
|
73
|
+
".next",
|
|
74
|
+
".turbo",
|
|
75
|
+
".svelte-kit"
|
|
76
|
+
]);
|
|
77
|
+
var MAX_FILE_BYTES = 1e6;
|
|
78
|
+
var HEX_COLOR = /(?<![\w&#/-])(?<!\bhref=["'])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![\w-])/g;
|
|
79
|
+
var IGNORE_MARKER = "kit-ignore";
|
|
80
|
+
var toPosix = (value) => value.split(path2.sep).join("/");
|
|
81
|
+
function isIgnored(relativePath, ignore) {
|
|
82
|
+
return ignore.some((entry) => {
|
|
83
|
+
const prefix = entry.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
84
|
+
return prefix !== "" && (relativePath === prefix || relativePath.startsWith(`${prefix}/`));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function findHexColors(text) {
|
|
88
|
+
const findings = [];
|
|
89
|
+
const lines = text.split(/\r?\n/);
|
|
90
|
+
lines.forEach((line, index) => {
|
|
91
|
+
if (line.includes(IGNORE_MARKER)) return;
|
|
92
|
+
for (const found of line.matchAll(HEX_COLOR)) {
|
|
93
|
+
findings.push({ line: index + 1, column: (found.index ?? 0) + 1, match: found[0] });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return findings;
|
|
97
|
+
}
|
|
98
|
+
async function* walk(directory) {
|
|
99
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
100
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (entry.isSymbolicLink()) continue;
|
|
103
|
+
const full = path2.join(directory, entry.name);
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) yield* walk(full);
|
|
106
|
+
} else if (entry.isFile()) {
|
|
107
|
+
yield full;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function isScannable(file) {
|
|
112
|
+
const name = path2.basename(file);
|
|
113
|
+
if (name.includes(".min.") || name.endsWith(".d.ts")) return false;
|
|
114
|
+
return SCANNED_EXTENSIONS.has(path2.extname(name));
|
|
115
|
+
}
|
|
116
|
+
async function scanForHardcodedColors(cwd, targets = ["."], { ignore = [] } = {}) {
|
|
117
|
+
const findings = [];
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const target of targets) {
|
|
120
|
+
const root = path2.resolve(cwd, target);
|
|
121
|
+
const info = await stat(root);
|
|
122
|
+
const files = info.isDirectory() ? walk(root) : (async function* () {
|
|
123
|
+
yield root;
|
|
124
|
+
})();
|
|
125
|
+
for await (const file of files) {
|
|
126
|
+
if (seen.has(file)) continue;
|
|
127
|
+
seen.add(file);
|
|
128
|
+
const relative = toPosix(path2.relative(cwd, file));
|
|
129
|
+
if (isIgnored(relative, ignore) || !isScannable(file)) continue;
|
|
130
|
+
const { size } = await stat(file);
|
|
131
|
+
if (size > MAX_FILE_BYTES) continue;
|
|
132
|
+
const text = await readFile(file, "utf8");
|
|
133
|
+
for (const finding of findHexColors(text)) {
|
|
134
|
+
findings.push({ file: relative, ...finding });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return findings;
|
|
139
|
+
}
|
|
140
|
+
var HELP = `kit: command line for the Unified UI Kit
|
|
141
|
+
|
|
142
|
+
Usage
|
|
143
|
+
kit <command> [options]
|
|
144
|
+
|
|
145
|
+
Commands
|
|
146
|
+
init [--theme <name>] [--density <name>] [--force]
|
|
147
|
+
Write ${CONFIG_FILE} in the current folder.
|
|
148
|
+
add <component...>
|
|
149
|
+
Add a component to your project. Planned.
|
|
150
|
+
theme [name]
|
|
151
|
+
List the themes and densities. Generating a theme is planned.
|
|
152
|
+
doctor [paths...] [--json]
|
|
153
|
+
Find hard-coded hex colors that should be --kit- tokens.
|
|
154
|
+
Exits with 1 when it finds any.
|
|
155
|
+
|
|
156
|
+
Options
|
|
157
|
+
-h, --help Show this help.
|
|
158
|
+
-v, --version Show the version.
|
|
159
|
+
`;
|
|
160
|
+
function readVersion() {
|
|
161
|
+
const file = new URL("../package.json", import.meta.url);
|
|
162
|
+
return JSON.parse(readFileSync(file, "utf8")).version;
|
|
163
|
+
}
|
|
164
|
+
async function exists(file) {
|
|
165
|
+
try {
|
|
166
|
+
await access(file);
|
|
167
|
+
return true;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function init(values, io) {
|
|
173
|
+
if (values.theme !== void 0 && !isThemeName(values.theme)) {
|
|
174
|
+
io.err(`Unknown theme "${values.theme}". Choose one of: ${themeNames.join(", ")}.
|
|
175
|
+
`);
|
|
176
|
+
return 2;
|
|
177
|
+
}
|
|
178
|
+
if (values.density !== void 0 && !isDensityName(values.density)) {
|
|
179
|
+
io.err(`Unknown density "${values.density}". Choose one of: ${densityNames.join(", ")}.
|
|
180
|
+
`);
|
|
181
|
+
return 2;
|
|
182
|
+
}
|
|
183
|
+
const target = path2.join(io.cwd, CONFIG_FILE);
|
|
184
|
+
if (!values.force && await exists(target)) {
|
|
185
|
+
io.err(`${CONFIG_FILE} already exists. Use --force to replace it.
|
|
186
|
+
`);
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
const config = createConfig({ theme: values.theme, density: values.density });
|
|
190
|
+
await writeFile(target, `${JSON.stringify(config, null, 2)}
|
|
191
|
+
`, "utf8");
|
|
192
|
+
io.out(`Created ${CONFIG_FILE}
|
|
193
|
+
`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
function add(components, io) {
|
|
197
|
+
const wanted = components.length > 0 ? ` (${components.join(", ")})` : "";
|
|
198
|
+
io.err(
|
|
199
|
+
`kit add${wanted} is not available yet. The component registry is planned. Until then, import components from @x12i/ui.
|
|
200
|
+
`
|
|
201
|
+
);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
function theme(name, io) {
|
|
205
|
+
if (name === void 0) {
|
|
206
|
+
io.out(`Themes: ${themeNames.join(", ")}
|
|
207
|
+
`);
|
|
208
|
+
io.out(`Densities: ${densityNames.join(", ")}
|
|
209
|
+
`);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
if (isThemeName(name)) {
|
|
213
|
+
io.out(`"${name}" is built in. Import @x12i/themes/${name}.css, or set data-theme="${name}" on <html>.
|
|
214
|
+
`);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
io.err(
|
|
218
|
+
`Generating a custom theme is planned and is not available yet. Built-in themes: ${themeNames.join(", ")}.
|
|
219
|
+
`
|
|
220
|
+
);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
async function doctor(paths, json, io) {
|
|
224
|
+
const config = await loadConfig(io.cwd);
|
|
225
|
+
const targets = paths.length > 0 ? paths : ["."];
|
|
226
|
+
let findings;
|
|
227
|
+
try {
|
|
228
|
+
findings = await scanForHardcodedColors(io.cwd, targets, { ignore: config?.doctor.ignore });
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error.code === "ENOENT") {
|
|
231
|
+
io.err(`Path not found: ${error.path ?? targets.join(", ")}
|
|
232
|
+
`);
|
|
233
|
+
return 2;
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
if (json) {
|
|
238
|
+
io.out(`${JSON.stringify(findings, null, 2)}
|
|
239
|
+
`);
|
|
240
|
+
return findings.length > 0 ? 1 : 0;
|
|
241
|
+
}
|
|
242
|
+
if (findings.length === 0) {
|
|
243
|
+
io.out("No hard-coded colors found.\n");
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
for (const finding of findings) {
|
|
247
|
+
io.out(`${finding.file}:${finding.line}:${finding.column} ${finding.match} Use a --kit- color token.
|
|
248
|
+
`);
|
|
249
|
+
}
|
|
250
|
+
const files = new Set(findings.map((finding) => finding.file)).size;
|
|
251
|
+
io.out(
|
|
252
|
+
`
|
|
253
|
+
${findings.length} hard-coded ${findings.length === 1 ? "color" : "colors"} found in ${files} ${files === 1 ? "file" : "files"}.
|
|
254
|
+
`
|
|
255
|
+
);
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
async function run(argv, io) {
|
|
259
|
+
let parsed;
|
|
260
|
+
try {
|
|
261
|
+
parsed = parseArgs({
|
|
262
|
+
args: argv,
|
|
263
|
+
allowPositionals: true,
|
|
264
|
+
options: {
|
|
265
|
+
help: { type: "boolean", short: "h" },
|
|
266
|
+
version: { type: "boolean", short: "v" },
|
|
267
|
+
force: { type: "boolean" },
|
|
268
|
+
json: { type: "boolean" },
|
|
269
|
+
theme: { type: "string" },
|
|
270
|
+
density: { type: "string" }
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
} catch (error) {
|
|
274
|
+
io.err(`${error.message}
|
|
275
|
+
|
|
276
|
+
${HELP}`);
|
|
277
|
+
return 2;
|
|
278
|
+
}
|
|
279
|
+
const { values, positionals } = parsed;
|
|
280
|
+
const [command, ...rest] = positionals;
|
|
281
|
+
if (values.version) {
|
|
282
|
+
io.out(`${readVersion()}
|
|
283
|
+
`);
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
if (values.help || command === void 0 || command === "help") {
|
|
287
|
+
io.out(HELP);
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
switch (command) {
|
|
291
|
+
case "init":
|
|
292
|
+
return init(values, io);
|
|
293
|
+
case "add":
|
|
294
|
+
return add(rest, io);
|
|
295
|
+
case "theme":
|
|
296
|
+
return theme(rest[0], io);
|
|
297
|
+
case "doctor":
|
|
298
|
+
return doctor(rest, values.json ?? false, io);
|
|
299
|
+
default:
|
|
300
|
+
io.err(`Unknown command "${command}".
|
|
301
|
+
|
|
302
|
+
${HELP}`);
|
|
303
|
+
return 2;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export { CONFIG_FILE, CONFIG_VERSION, IGNORE_MARKER, createConfig, findHexColors, loadConfig, run, scanForHardcodedColors };
|
|
308
|
+
//# sourceMappingURL=index.js.map
|
|
309
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/doctor.ts","../src/main.ts"],"names":["path","readFile","themeNames","densityNames"],"mappings":"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","file":"index.js","sourcesContent":["import { readFile } from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { densityNames, themeNames, type DensityName, type ThemeName } from \"@x12i/tokens\";\r\n\r\nexport const CONFIG_FILE = \"kit.config.json\";\r\nexport const CONFIG_VERSION = 1;\r\n\r\nexport interface KitConfig {\r\n version: typeof CONFIG_VERSION;\r\n /** The theme the project starts with. */\r\n theme: ThemeName;\r\n density: DensityName;\r\n doctor: {\r\n /** Paths, relative to the config file, that `kit doctor` skips. */\r\n ignore: string[];\r\n };\r\n}\r\n\r\nexport function isThemeName(value: string): value is ThemeName {\r\n return (themeNames as readonly string[]).includes(value);\r\n}\r\n\r\nexport function isDensityName(value: string): value is DensityName {\r\n return (densityNames as readonly string[]).includes(value);\r\n}\r\n\r\nexport function createConfig(options: { theme?: ThemeName; density?: DensityName } = {}): KitConfig {\r\n return {\r\n version: CONFIG_VERSION,\r\n theme: options.theme ?? \"light\",\r\n density: options.density ?? \"default\",\r\n doctor: { ignore: [] },\r\n };\r\n}\r\n\r\n/** Reads `kit.config.json` from `cwd`. Returns `null` when there is none. */\r\nexport async function loadConfig(cwd: string): Promise<KitConfig | null> {\r\n let text: string;\r\n try {\r\n text = await readFile(path.join(cwd, CONFIG_FILE), \"utf8\");\r\n } catch (error) {\r\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\r\n throw error;\r\n }\r\n\r\n let raw: unknown;\r\n try {\r\n raw = JSON.parse(text);\r\n } catch {\r\n throw new Error(`${CONFIG_FILE} is not valid JSON.`);\r\n }\r\n\r\n const input = (typeof raw === \"object\" && raw !== null ? raw : {}) as Partial<KitConfig>;\r\n const defaults = createConfig();\r\n const ignore = input.doctor?.ignore;\r\n\r\n return {\r\n version: CONFIG_VERSION,\r\n theme: typeof input.theme === \"string\" && isThemeName(input.theme) ? input.theme : defaults.theme,\r\n density:\r\n typeof input.density === \"string\" && isDensityName(input.density) ? input.density : defaults.density,\r\n doctor: {\r\n ignore: Array.isArray(ignore) ? ignore.filter((item): item is string => typeof item === \"string\") : [],\r\n },\r\n };\r\n}\r\n","import { readdir, readFile, stat } from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\n\r\nexport interface Finding {\r\n /** Path relative to the scan root, with forward slashes. */\r\n file: string;\r\n line: number;\r\n column: number;\r\n /** The text that matched, such as `#ff0000`. */\r\n match: string;\r\n}\r\n\r\nexport interface ScanOptions {\r\n /** Paths, relative to `cwd`, to skip. A prefix skips everything under it. */\r\n ignore?: string[];\r\n}\r\n\r\nconst SCANNED_EXTENSIONS = new Set([\r\n \".css\",\r\n \".scss\",\r\n \".sass\",\r\n \".less\",\r\n \".ts\",\r\n \".tsx\",\r\n \".js\",\r\n \".jsx\",\r\n \".mjs\",\r\n \".cjs\",\r\n \".html\",\r\n \".vue\",\r\n \".svelte\",\r\n \".mdx\",\r\n]);\r\n\r\nconst SKIPPED_DIRECTORIES = new Set([\r\n \"node_modules\",\r\n \"dist\",\r\n \"build\",\r\n \"out\",\r\n \"coverage\",\r\n \".git\",\r\n \".next\",\r\n \".turbo\",\r\n \".svelte-kit\",\r\n]);\r\n\r\nconst MAX_FILE_BYTES = 1_000_000;\r\n\r\n/**\r\n * 3, 4, 6 or 8 hex digits after a `#`. The lookbehind skips URL fragments such\r\n * as `page#abc`, HTML entities such as `𞉀` and `href=\"#abc\"` anchors.\r\n */\r\nconst HEX_COLOR = /(?<![\\w&#/-])(?<!\\bhref=[\"'])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![\\w-])/g;\r\n\r\n/** A line that contains this text is skipped, for a color that is meant to stay. */\r\nexport const IGNORE_MARKER = \"kit-ignore\";\r\n\r\nconst toPosix = (value: string) => value.split(path.sep).join(\"/\");\r\n\r\nfunction isIgnored(relativePath: string, ignore: string[]): boolean {\r\n return ignore.some((entry) => {\r\n const prefix = entry.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/+$/, \"\");\r\n return prefix !== \"\" && (relativePath === prefix || relativePath.startsWith(`${prefix}/`));\r\n });\r\n}\r\n\r\n/** Finds hard-coded hex colors in one piece of text. */\r\nexport function findHexColors(text: string): Array<Omit<Finding, \"file\">> {\r\n const findings: Array<Omit<Finding, \"file\">> = [];\r\n const lines = text.split(/\\r?\\n/);\r\n\r\n lines.forEach((line, index) => {\r\n if (line.includes(IGNORE_MARKER)) return;\r\n for (const found of line.matchAll(HEX_COLOR)) {\r\n findings.push({ line: index + 1, column: (found.index ?? 0) + 1, match: found[0] });\r\n }\r\n });\r\n\r\n return findings;\r\n}\r\n\r\nasync function* walk(directory: string): AsyncGenerator<string> {\r\n const entries = await readdir(directory, { withFileTypes: true });\r\n entries.sort((a, b) => a.name.localeCompare(b.name));\r\n\r\n for (const entry of entries) {\r\n // Symlinks are skipped so a link back up the tree cannot loop forever.\r\n if (entry.isSymbolicLink()) continue;\r\n const full = path.join(directory, entry.name);\r\n\r\n if (entry.isDirectory()) {\r\n if (!SKIPPED_DIRECTORIES.has(entry.name)) yield* walk(full);\r\n } else if (entry.isFile()) {\r\n yield full;\r\n }\r\n }\r\n}\r\n\r\nfunction isScannable(file: string): boolean {\r\n const name = path.basename(file);\r\n if (name.includes(\".min.\") || name.endsWith(\".d.ts\")) return false;\r\n return SCANNED_EXTENSIONS.has(path.extname(name));\r\n}\r\n\r\n/**\r\n * Scans files and folders for hard-coded hex colors. Paths that do not exist\r\n * throw, so a typo in a path is not reported as a clean result.\r\n */\r\nexport async function scanForHardcodedColors(\r\n cwd: string,\r\n targets: string[] = [\".\"],\r\n { ignore = [] }: ScanOptions = {},\r\n): Promise<Finding[]> {\r\n const findings: Finding[] = [];\r\n const seen = new Set<string>();\r\n\r\n for (const target of targets) {\r\n const root = path.resolve(cwd, target);\r\n const info = await stat(root);\r\n const files = info.isDirectory() ? walk(root) : (async function* () { yield root; })();\r\n\r\n for await (const file of files) {\r\n if (seen.has(file)) continue;\r\n seen.add(file);\r\n\r\n const relative = toPosix(path.relative(cwd, file));\r\n if (isIgnored(relative, ignore) || !isScannable(file)) continue;\r\n\r\n const { size } = await stat(file);\r\n if (size > MAX_FILE_BYTES) continue;\r\n\r\n const text = await readFile(file, \"utf8\");\r\n for (const finding of findHexColors(text)) {\r\n findings.push({ file: relative, ...finding });\r\n }\r\n }\r\n }\r\n\r\n return findings;\r\n}\r\n","import { readFileSync } from \"node:fs\";\r\nimport { access, writeFile } from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { parseArgs } from \"node:util\";\r\nimport { densityNames, themeNames } from \"@x12i/tokens\";\r\nimport { CONFIG_FILE, createConfig, isDensityName, isThemeName, loadConfig } from \"./config\";\r\nimport { scanForHardcodedColors } from \"./doctor\";\r\n\r\nexport interface Io {\r\n cwd: string;\r\n out: (text: string) => void;\r\n err: (text: string) => void;\r\n}\r\n\r\n/** Exit codes: 0 success, 1 the command ran and found a problem, 2 bad usage. */\r\ntype ExitCode = 0 | 1 | 2;\r\n\r\nconst HELP = `kit: command line for the Unified UI Kit\r\n\r\nUsage\r\n kit <command> [options]\r\n\r\nCommands\r\n init [--theme <name>] [--density <name>] [--force]\r\n Write ${CONFIG_FILE} in the current folder.\r\n add <component...>\r\n Add a component to your project. Planned.\r\n theme [name]\r\n List the themes and densities. Generating a theme is planned.\r\n doctor [paths...] [--json]\r\n Find hard-coded hex colors that should be --kit- tokens.\r\n Exits with 1 when it finds any.\r\n\r\nOptions\r\n -h, --help Show this help.\r\n -v, --version Show the version.\r\n`;\r\n\r\nfunction readVersion(): string {\r\n const file = new URL(\"../package.json\", import.meta.url);\r\n return (JSON.parse(readFileSync(file, \"utf8\")) as { version: string }).version;\r\n}\r\n\r\nasync function exists(file: string): Promise<boolean> {\r\n try {\r\n await access(file);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nasync function init(values: { theme?: string; density?: string; force?: boolean }, io: Io): Promise<ExitCode> {\r\n if (values.theme !== undefined && !isThemeName(values.theme)) {\r\n io.err(`Unknown theme \"${values.theme}\". Choose one of: ${themeNames.join(\", \")}.\\n`);\r\n return 2;\r\n }\r\n if (values.density !== undefined && !isDensityName(values.density)) {\r\n io.err(`Unknown density \"${values.density}\". Choose one of: ${densityNames.join(\", \")}.\\n`);\r\n return 2;\r\n }\r\n\r\n const target = path.join(io.cwd, CONFIG_FILE);\r\n if (!values.force && (await exists(target))) {\r\n io.err(`${CONFIG_FILE} already exists. Use --force to replace it.\\n`);\r\n return 1;\r\n }\r\n\r\n const config = createConfig({ theme: values.theme, density: values.density });\r\n await writeFile(target, `${JSON.stringify(config, null, 2)}\\n`, \"utf8\");\r\n io.out(`Created ${CONFIG_FILE}\\n`);\r\n return 0;\r\n}\r\n\r\nfunction add(components: string[], io: Io): ExitCode {\r\n const wanted = components.length > 0 ? ` (${components.join(\", \")})` : \"\";\r\n io.err(\r\n `kit add${wanted} is not available yet. The component registry is planned. ` +\r\n `Until then, import components from @x12i/ui.\\n`,\r\n );\r\n return 1;\r\n}\r\n\r\nfunction theme(name: string | undefined, io: Io): ExitCode {\r\n if (name === undefined) {\r\n io.out(`Themes: ${themeNames.join(\", \")}\\n`);\r\n io.out(`Densities: ${densityNames.join(\", \")}\\n`);\r\n return 0;\r\n }\r\n\r\n if (isThemeName(name)) {\r\n io.out(`\"${name}\" is built in. Import @x12i/themes/${name}.css, or set data-theme=\"${name}\" on <html>.\\n`);\r\n return 0;\r\n }\r\n\r\n io.err(\r\n `Generating a custom theme is planned and is not available yet. ` +\r\n `Built-in themes: ${themeNames.join(\", \")}.\\n`,\r\n );\r\n return 1;\r\n}\r\n\r\nasync function doctor(paths: string[], json: boolean, io: Io): Promise<ExitCode> {\r\n const config = await loadConfig(io.cwd);\r\n const targets = paths.length > 0 ? paths : [\".\"];\r\n\r\n let findings;\r\n try {\r\n findings = await scanForHardcodedColors(io.cwd, targets, { ignore: config?.doctor.ignore });\r\n } catch (error) {\r\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\r\n io.err(`Path not found: ${(error as NodeJS.ErrnoException).path ?? targets.join(\", \")}\\n`);\r\n return 2;\r\n }\r\n throw error;\r\n }\r\n\r\n if (json) {\r\n io.out(`${JSON.stringify(findings, null, 2)}\\n`);\r\n return findings.length > 0 ? 1 : 0;\r\n }\r\n\r\n if (findings.length === 0) {\r\n io.out(\"No hard-coded colors found.\\n\");\r\n return 0;\r\n }\r\n\r\n for (const finding of findings) {\r\n io.out(`${finding.file}:${finding.line}:${finding.column} ${finding.match} Use a --kit- color token.\\n`);\r\n }\r\n const files = new Set(findings.map((finding) => finding.file)).size;\r\n io.out(\r\n `\\n${findings.length} hard-coded ${findings.length === 1 ? \"color\" : \"colors\"} ` +\r\n `found in ${files} ${files === 1 ? \"file\" : \"files\"}.\\n`,\r\n );\r\n return 1;\r\n}\r\n\r\n/** Runs the command line and returns the exit code. */\r\nexport async function run(argv: string[], io: Io): Promise<ExitCode> {\r\n let parsed;\r\n try {\r\n parsed = parseArgs({\r\n args: argv,\r\n allowPositionals: true,\r\n options: {\r\n help: { type: \"boolean\", short: \"h\" },\r\n version: { type: \"boolean\", short: \"v\" },\r\n force: { type: \"boolean\" },\r\n json: { type: \"boolean\" },\r\n theme: { type: \"string\" },\r\n density: { type: \"string\" },\r\n },\r\n });\r\n } catch (error) {\r\n io.err(`${(error as Error).message}\\n\\n${HELP}`);\r\n return 2;\r\n }\r\n\r\n const { values, positionals } = parsed;\r\n const [command, ...rest] = positionals;\r\n\r\n if (values.version) {\r\n io.out(`${readVersion()}\\n`);\r\n return 0;\r\n }\r\n if (values.help || command === undefined || command === \"help\") {\r\n io.out(HELP);\r\n return 0;\r\n }\r\n\r\n switch (command) {\r\n case \"init\":\r\n return init(values, io);\r\n case \"add\":\r\n return add(rest, io);\r\n case \"theme\":\r\n return theme(rest[0], io);\r\n case \"doctor\":\r\n return doctor(rest, values.json ?? false, io);\r\n default:\r\n io.err(`Unknown command \"${command}\".\\n\\n${HELP}`);\r\n return 2;\r\n }\r\n}\r\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x12i/kit-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Command line for the Unified UI Kit: init, doctor, and stubs for add and theme.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/x12i/unified-ui-kit.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/x12i/unified-ui-kit/tree/main/packages/cli#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/x12i/unified-ui-kit/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"bin": {
|
|
18
|
+
"kit": "./dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@x12i/tokens": "^1.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^24.0.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"lint": "eslint .",
|
|
47
|
+
"test": "vitest run"
|
|
48
|
+
}
|
|
6
49
|
}
|