tokvista 1.9.0 → 1.11.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/README.md +17 -0
- package/dist/bin/tokvista.js +320 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,22 @@ npx tokvista convert tokens.json --to supernova --output tokens-sn.json
|
|
|
147
147
|
npx tokvista convert tokens.json --to w3c
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
### Build All Formats
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Build everything in one command
|
|
154
|
+
npx tokvista build tokens.json --output-dir ./dist
|
|
155
|
+
|
|
156
|
+
# Creates:
|
|
157
|
+
# - tokens.css
|
|
158
|
+
# - tokens.scss
|
|
159
|
+
# - tokens.js
|
|
160
|
+
# - tailwind.config.js
|
|
161
|
+
|
|
162
|
+
# Skip validation for faster builds
|
|
163
|
+
npx tokvista build tokens.json --output-dir ./dist --skip-validation
|
|
164
|
+
```
|
|
165
|
+
|
|
150
166
|
### Interactive Setup
|
|
151
167
|
|
|
152
168
|
```bash
|
|
@@ -179,6 +195,7 @@ Then run `npx tokvista` to use your config.
|
|
|
179
195
|
| `tokvista validate <file>` | Validate token structure and values |
|
|
180
196
|
| `tokvista diff <old> <new>` | Compare two token files |
|
|
181
197
|
| `tokvista convert <file> --to <format>` | Convert between token formats |
|
|
198
|
+
| `tokvista build <file> --output-dir <dir>` | Build all formats (validate + export) |
|
|
182
199
|
| `--config`, `-c` | Config file path |
|
|
183
200
|
| `--port`, `-p` | Server port (default: `3000`) |
|
|
184
201
|
| `--format` | Export format (export only) |
|
package/dist/bin/tokvista.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/bin/tokvista.ts
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { existsSync, readdirSync } from "fs";
|
|
6
|
-
import { readFile, writeFile } from "fs/promises";
|
|
6
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
7
7
|
import { createServer } from "http";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { createInterface } from "readline";
|
|
@@ -1088,6 +1088,134 @@ function convertTokenFormat(tokens, targetFormat) {
|
|
|
1088
1088
|
}
|
|
1089
1089
|
}
|
|
1090
1090
|
|
|
1091
|
+
// src/bin/scanner.ts
|
|
1092
|
+
import { readdir, readFile } from "fs/promises";
|
|
1093
|
+
import { join, extname } from "path";
|
|
1094
|
+
function isRecord7(value) {
|
|
1095
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1096
|
+
}
|
|
1097
|
+
function isTokenLike5(obj) {
|
|
1098
|
+
return isRecord7(obj) && "value" in obj;
|
|
1099
|
+
}
|
|
1100
|
+
function extractTokenNames(tokens) {
|
|
1101
|
+
const tokenMap = /* @__PURE__ */ new Map();
|
|
1102
|
+
function walk(node, path2 = []) {
|
|
1103
|
+
if (!isRecord7(node)) return;
|
|
1104
|
+
if (path2.length === 0 && Object.keys(node).some((k) => k.includes("/"))) {
|
|
1105
|
+
Object.values(node).forEach((val) => walk(val, []));
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (isTokenLike5(node)) {
|
|
1109
|
+
const tokenPath = path2.join(".");
|
|
1110
|
+
const cssVar = `--${path2.join("-")}`;
|
|
1111
|
+
tokenMap.set(cssVar, tokenPath);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
Object.entries(node).forEach(([key, val]) => {
|
|
1115
|
+
walk(val, [...path2, key]);
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
walk(tokens);
|
|
1119
|
+
return tokenMap;
|
|
1120
|
+
}
|
|
1121
|
+
async function scanFile(filePath, tokenVars) {
|
|
1122
|
+
const content = await readFile(filePath, "utf8");
|
|
1123
|
+
const lines = content.split("\n");
|
|
1124
|
+
const usedVars = /* @__PURE__ */ new Set();
|
|
1125
|
+
const hardcodedColors = [];
|
|
1126
|
+
const hardcodedSpacing = [];
|
|
1127
|
+
const cssVarPattern = /var\((--[\w-]+)\)/g;
|
|
1128
|
+
const hexColorPattern = /#[0-9A-Fa-f]{3,8}\b/g;
|
|
1129
|
+
const rgbPattern = /rgba?\([^)]+\)/g;
|
|
1130
|
+
const spacingPattern = /\b(\d+(?:\.\d+)?(?:px|rem|em))\b/g;
|
|
1131
|
+
lines.forEach((line, index) => {
|
|
1132
|
+
let match;
|
|
1133
|
+
while ((match = cssVarPattern.exec(line)) !== null) {
|
|
1134
|
+
const varName = match[1];
|
|
1135
|
+
if (tokenVars.has(varName)) {
|
|
1136
|
+
usedVars.add(varName);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
while ((match = hexColorPattern.exec(line)) !== null) {
|
|
1140
|
+
const color = match[0];
|
|
1141
|
+
if (!line.includes("0x") && !line.includes("\\u")) {
|
|
1142
|
+
hardcodedColors.push({ line: index + 1, value: color });
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
while ((match = rgbPattern.exec(line)) !== null) {
|
|
1146
|
+
hardcodedColors.push({ line: index + 1, value: match[0] });
|
|
1147
|
+
}
|
|
1148
|
+
if (line.includes("padding") || line.includes("margin") || line.includes("gap") || line.includes("width") || line.includes("height")) {
|
|
1149
|
+
while ((match = spacingPattern.exec(line)) !== null) {
|
|
1150
|
+
hardcodedSpacing.push({ line: index + 1, value: match[1] });
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
return { usedVars, hardcodedColors, hardcodedSpacing };
|
|
1155
|
+
}
|
|
1156
|
+
async function scanDirectory(dir, tokenVars, extensions) {
|
|
1157
|
+
const usedVars = /* @__PURE__ */ new Set();
|
|
1158
|
+
const hardcodedColors = [];
|
|
1159
|
+
const hardcodedSpacing = [];
|
|
1160
|
+
let filesScanned = 0;
|
|
1161
|
+
async function scan(currentDir) {
|
|
1162
|
+
try {
|
|
1163
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
1164
|
+
for (const entry of entries) {
|
|
1165
|
+
const fullPath = join(currentDir, entry.name);
|
|
1166
|
+
if (entry.isDirectory()) {
|
|
1167
|
+
if (["node_modules", ".git", "dist", "build", ".next", "coverage"].includes(entry.name)) {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
await scan(fullPath);
|
|
1171
|
+
} else if (entry.isFile()) {
|
|
1172
|
+
const ext = extname(entry.name);
|
|
1173
|
+
if (extensions.has(ext)) {
|
|
1174
|
+
const result = await scanFile(fullPath, tokenVars);
|
|
1175
|
+
filesScanned++;
|
|
1176
|
+
result.usedVars.forEach((v) => usedVars.add(v));
|
|
1177
|
+
result.hardcodedColors.forEach((h) => hardcodedColors.push({ file: fullPath, ...h }));
|
|
1178
|
+
result.hardcodedSpacing.forEach((h) => hardcodedSpacing.push({ file: fullPath, ...h }));
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
await scan(dir);
|
|
1186
|
+
return { usedVars, hardcodedColors, hardcodedSpacing, filesScanned };
|
|
1187
|
+
}
|
|
1188
|
+
async function scanTokenUsage(tokensPath, scanDir, tokens) {
|
|
1189
|
+
const detection = detectTokenFormat(tokens);
|
|
1190
|
+
let normalizedTokens = tokens;
|
|
1191
|
+
if (detection.format !== "token-studio" && detection.format !== "unknown") {
|
|
1192
|
+
normalizedTokens = normalizeTokenFormat(tokens, detection.format);
|
|
1193
|
+
}
|
|
1194
|
+
const tokenMap = extractTokenNames(normalizedTokens);
|
|
1195
|
+
const tokenVars = new Set(tokenMap.keys());
|
|
1196
|
+
const extensions = /* @__PURE__ */ new Set([".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte"]);
|
|
1197
|
+
const scanResult = await scanDirectory(scanDir, tokenVars, extensions);
|
|
1198
|
+
const usedTokens = [];
|
|
1199
|
+
const unusedTokens = [];
|
|
1200
|
+
tokenMap.forEach((tokenPath, cssVar) => {
|
|
1201
|
+
if (scanResult.usedVars.has(cssVar)) {
|
|
1202
|
+
usedTokens.push(tokenPath);
|
|
1203
|
+
} else {
|
|
1204
|
+
unusedTokens.push(tokenPath);
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
return {
|
|
1208
|
+
totalTokens: tokenMap.size,
|
|
1209
|
+
usedTokens,
|
|
1210
|
+
unusedTokens,
|
|
1211
|
+
hardcodedColors: scanResult.hardcodedColors.slice(0, 50),
|
|
1212
|
+
// Limit to 50
|
|
1213
|
+
hardcodedSpacing: scanResult.hardcodedSpacing.slice(0, 50),
|
|
1214
|
+
// Limit to 50
|
|
1215
|
+
filesScanned: scanResult.filesScanned
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1091
1219
|
// src/bin/watcher.ts
|
|
1092
1220
|
import { watch } from "fs";
|
|
1093
1221
|
function watchFile(filePath, onChange) {
|
|
@@ -1122,6 +1250,8 @@ Usage:
|
|
|
1122
1250
|
tokvista validate <tokens.json>
|
|
1123
1251
|
tokvista diff <old.json> <new.json>
|
|
1124
1252
|
tokvista convert <tokens.json> --to <w3c|style-dictionary|supernova> [--output <file>]
|
|
1253
|
+
tokvista build <tokens.json> --output-dir <dir> [--skip-validation]
|
|
1254
|
+
tokvista scan <directory> [--tokens tokens.json]
|
|
1125
1255
|
|
|
1126
1256
|
Arguments:
|
|
1127
1257
|
tokens.json Path to your tokens file (overrides config.tokens)
|
|
@@ -1257,6 +1387,12 @@ function parseArgs(args) {
|
|
|
1257
1387
|
if (args[0] === "convert") {
|
|
1258
1388
|
return parseConvertArgs(args.slice(1));
|
|
1259
1389
|
}
|
|
1390
|
+
if (args[0] === "build") {
|
|
1391
|
+
return parseBuildArgs(args.slice(1));
|
|
1392
|
+
}
|
|
1393
|
+
if (args[0] === "scan") {
|
|
1394
|
+
return parseScanArgs(args.slice(1));
|
|
1395
|
+
}
|
|
1260
1396
|
return parseServeArgs(args);
|
|
1261
1397
|
}
|
|
1262
1398
|
function parseValidateArgs(args) {
|
|
@@ -1353,6 +1489,74 @@ function parseConvertArgs(args) {
|
|
|
1353
1489
|
if (!to) throw new Error("--to is required (w3c, style-dictionary, supernova, or token-studio)");
|
|
1354
1490
|
return { command: "convert", tokenFileArg, to, output };
|
|
1355
1491
|
}
|
|
1492
|
+
function parseBuildArgs(args) {
|
|
1493
|
+
let tokenFileArg;
|
|
1494
|
+
let outputDir;
|
|
1495
|
+
let skipValidation = false;
|
|
1496
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1497
|
+
const arg = args[index];
|
|
1498
|
+
if (arg === "-h" || arg === "--help") {
|
|
1499
|
+
printHelp();
|
|
1500
|
+
process.exit(0);
|
|
1501
|
+
}
|
|
1502
|
+
if (arg === "--output-dir" || arg === "-o") {
|
|
1503
|
+
const next = args[index + 1];
|
|
1504
|
+
if (!next) throw new Error("Missing value for --output-dir");
|
|
1505
|
+
outputDir = next;
|
|
1506
|
+
index += 1;
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
if (arg.startsWith("--output-dir=")) {
|
|
1510
|
+
outputDir = arg.slice("--output-dir=".length);
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
if (arg === "--skip-validation") {
|
|
1514
|
+
skipValidation = true;
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
if (arg.startsWith("-")) {
|
|
1518
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1519
|
+
}
|
|
1520
|
+
if (tokenFileArg) {
|
|
1521
|
+
throw new Error(`Only one token file is supported. Unexpected value: "${arg}"`);
|
|
1522
|
+
}
|
|
1523
|
+
tokenFileArg = arg;
|
|
1524
|
+
}
|
|
1525
|
+
if (!tokenFileArg) throw new Error("Token file is required for build");
|
|
1526
|
+
if (!outputDir) throw new Error("--output-dir is required");
|
|
1527
|
+
return { command: "build", tokenFileArg, outputDir, skipValidation };
|
|
1528
|
+
}
|
|
1529
|
+
function parseScanArgs(args) {
|
|
1530
|
+
let scanDir;
|
|
1531
|
+
let tokenFileArg;
|
|
1532
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1533
|
+
const arg = args[index];
|
|
1534
|
+
if (arg === "-h" || arg === "--help") {
|
|
1535
|
+
printHelp();
|
|
1536
|
+
process.exit(0);
|
|
1537
|
+
}
|
|
1538
|
+
if (arg === "--tokens") {
|
|
1539
|
+
const next = args[index + 1];
|
|
1540
|
+
if (!next) throw new Error("Missing value for --tokens");
|
|
1541
|
+
tokenFileArg = next;
|
|
1542
|
+
index += 1;
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (arg.startsWith("--tokens=")) {
|
|
1546
|
+
tokenFileArg = arg.slice("--tokens=".length);
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
if (arg.startsWith("-")) {
|
|
1550
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1551
|
+
}
|
|
1552
|
+
if (scanDir) {
|
|
1553
|
+
throw new Error(`Only one directory is supported. Unexpected value: "${arg}"`);
|
|
1554
|
+
}
|
|
1555
|
+
scanDir = arg;
|
|
1556
|
+
}
|
|
1557
|
+
if (!scanDir) throw new Error("Directory to scan is required");
|
|
1558
|
+
return { command: "scan", scanDir, tokenFileArg };
|
|
1559
|
+
}
|
|
1356
1560
|
function parseExportArgs(args) {
|
|
1357
1561
|
let tokenFileArg;
|
|
1358
1562
|
let format;
|
|
@@ -1419,7 +1623,7 @@ async function resolveDefaultInitTitle(cwd) {
|
|
|
1419
1623
|
return "My Design System";
|
|
1420
1624
|
}
|
|
1421
1625
|
try {
|
|
1422
|
-
const raw = await
|
|
1626
|
+
const raw = await readFile2(packageJsonPath, "utf8");
|
|
1423
1627
|
const parsed = JSON.parse(raw);
|
|
1424
1628
|
if (typeof parsed.name === "string") {
|
|
1425
1629
|
return formatTitleFromPackageName(parsed.name);
|
|
@@ -1659,7 +1863,7 @@ async function resolveLogoForRuntime(logoPathValue, configDir, cwd) {
|
|
|
1659
1863
|
if (!existsSync(resolvedLogoPath)) {
|
|
1660
1864
|
throw new Error(`Logo file not found: ${resolvedLogoPath}`);
|
|
1661
1865
|
}
|
|
1662
|
-
const content = await
|
|
1866
|
+
const content = await readFile2(resolvedLogoPath);
|
|
1663
1867
|
const mimeType = toDataUrlMimeType(resolvedLogoPath);
|
|
1664
1868
|
return `data:${mimeType};base64,${content.toString("base64")}`;
|
|
1665
1869
|
}
|
|
@@ -1747,7 +1951,7 @@ async function loadConfigFromFile(configPath) {
|
|
|
1747
1951
|
const sourceLabel = path.basename(configPath);
|
|
1748
1952
|
const extension = path.extname(configPath).toLowerCase();
|
|
1749
1953
|
if (extension === ".json") {
|
|
1750
|
-
const raw = await
|
|
1954
|
+
const raw = await readFile2(configPath, "utf8");
|
|
1751
1955
|
try {
|
|
1752
1956
|
return normalizeConfigObject(JSON.parse(raw), sourceLabel);
|
|
1753
1957
|
} catch (error) {
|
|
@@ -1755,7 +1959,7 @@ async function loadConfigFromFile(configPath) {
|
|
|
1755
1959
|
}
|
|
1756
1960
|
}
|
|
1757
1961
|
if (extension === ".ts") {
|
|
1758
|
-
const raw = await
|
|
1962
|
+
const raw = await readFile2(configPath, "utf8");
|
|
1759
1963
|
const parsed = parseTsConfigSource(raw, sourceLabel);
|
|
1760
1964
|
return normalizeConfigObject(parsed, sourceLabel);
|
|
1761
1965
|
}
|
|
@@ -1896,7 +2100,7 @@ function openBrowser(url) {
|
|
|
1896
2100
|
});
|
|
1897
2101
|
}
|
|
1898
2102
|
async function readTokens(tokenPath) {
|
|
1899
|
-
const raw = await
|
|
2103
|
+
const raw = await readFile2(tokenPath, "utf8");
|
|
1900
2104
|
try {
|
|
1901
2105
|
const parsed = JSON.parse(raw);
|
|
1902
2106
|
if (!parsed || typeof parsed !== "object") {
|
|
@@ -1917,8 +2121,8 @@ async function runServeCommand(cwd, options) {
|
|
|
1917
2121
|
}
|
|
1918
2122
|
const runtimeConfig = await buildRuntimeConfig(config, configPath, cwd);
|
|
1919
2123
|
const [css, appBundle] = await Promise.all([
|
|
1920
|
-
|
|
1921
|
-
|
|
2124
|
+
readFile2(resolveDistAsset("styles.css"), "utf8"),
|
|
2125
|
+
readFile2(resolveDistAsset("cli/browser.js"), "utf8")
|
|
1922
2126
|
]);
|
|
1923
2127
|
let cachedTokens = await readTokens(resolvedTokenPath);
|
|
1924
2128
|
const getHtml = () => buildHtml(cachedTokens, runtimeConfig, css, appBundle, options.watch);
|
|
@@ -2125,6 +2329,106 @@ async function runConvertCommand(cwd, options) {
|
|
|
2125
2329
|
console.log(output);
|
|
2126
2330
|
}
|
|
2127
2331
|
}
|
|
2332
|
+
async function runBuildCommand(cwd, options) {
|
|
2333
|
+
const resolvedTokenPath = path.resolve(cwd, options.tokenFileArg);
|
|
2334
|
+
const outputDir = path.resolve(cwd, options.outputDir);
|
|
2335
|
+
if (!existsSync(resolvedTokenPath)) {
|
|
2336
|
+
throw new Error(`Token file not found: ${resolvedTokenPath}`);
|
|
2337
|
+
}
|
|
2338
|
+
if (!existsSync(outputDir)) {
|
|
2339
|
+
await import("fs/promises").then((fs) => fs.mkdir(outputDir, { recursive: true }));
|
|
2340
|
+
}
|
|
2341
|
+
const tokens = await readTokens(resolvedTokenPath);
|
|
2342
|
+
if (!options.skipValidation) {
|
|
2343
|
+
console.log("\nValidating tokens...");
|
|
2344
|
+
const result = validateTokens(tokens);
|
|
2345
|
+
if (!result.valid) {
|
|
2346
|
+
console.log(`\u274C Found ${result.errors.length} errors`);
|
|
2347
|
+
result.errors.slice(0, 5).forEach((err) => {
|
|
2348
|
+
console.log(` ${err.path}: ${err.message}`);
|
|
2349
|
+
});
|
|
2350
|
+
if (result.errors.length > 5) {
|
|
2351
|
+
console.log(` ... and ${result.errors.length - 5} more errors`);
|
|
2352
|
+
}
|
|
2353
|
+
throw new Error("Validation failed. Fix errors or use --skip-validation");
|
|
2354
|
+
}
|
|
2355
|
+
console.log("\u2705 Validation passed\n");
|
|
2356
|
+
}
|
|
2357
|
+
console.log("Building tokens...");
|
|
2358
|
+
const formats = [
|
|
2359
|
+
{ name: "CSS", ext: "css", generator: generateCSS },
|
|
2360
|
+
{ name: "SCSS", ext: "scss", generator: generateSCSS },
|
|
2361
|
+
{ name: "JavaScript", ext: "js", generator: generateJS },
|
|
2362
|
+
{ name: "Tailwind", ext: "tailwind.config.js", generator: generateTailwind }
|
|
2363
|
+
];
|
|
2364
|
+
for (const format of formats) {
|
|
2365
|
+
const content = format.generator(tokens);
|
|
2366
|
+
const filename = format.ext.includes(".") ? format.ext : `tokens.${format.ext}`;
|
|
2367
|
+
const outputPath = path.join(outputDir, filename);
|
|
2368
|
+
await writeFile(outputPath, content, "utf8");
|
|
2369
|
+
console.log(` \u2713 ${format.name} \u2192 ${filename}`);
|
|
2370
|
+
}
|
|
2371
|
+
console.log(`
|
|
2372
|
+
\u2705 Build complete: ${outputDir}
|
|
2373
|
+
`);
|
|
2374
|
+
}
|
|
2375
|
+
async function runScanCommand(cwd, options) {
|
|
2376
|
+
const scanDir = path.resolve(cwd, options.scanDir);
|
|
2377
|
+
const tokenPath = options.tokenFileArg ? path.resolve(cwd, options.tokenFileArg) : path.resolve(cwd, "tokens.json");
|
|
2378
|
+
if (!existsSync(scanDir)) {
|
|
2379
|
+
throw new Error(`Directory not found: ${scanDir}`);
|
|
2380
|
+
}
|
|
2381
|
+
if (!existsSync(tokenPath)) {
|
|
2382
|
+
throw new Error(`Token file not found: ${tokenPath}`);
|
|
2383
|
+
}
|
|
2384
|
+
const tokens = await readTokens(tokenPath);
|
|
2385
|
+
console.log(`
|
|
2386
|
+
Scanning ${scanDir} for token usage...
|
|
2387
|
+
`);
|
|
2388
|
+
const result = await scanTokenUsage(tokenPath, scanDir, tokens);
|
|
2389
|
+
const usagePercent = (result.usedTokens.length / result.totalTokens * 100).toFixed(1);
|
|
2390
|
+
console.log(`\u{1F4CA} Token Usage Report
|
|
2391
|
+
`);
|
|
2392
|
+
console.log(`Files scanned: ${result.filesScanned}`);
|
|
2393
|
+
console.log(`Total tokens: ${result.totalTokens}`);
|
|
2394
|
+
console.log(`Used tokens: ${result.usedTokens.length} (${usagePercent}%)`);
|
|
2395
|
+
console.log(`Unused tokens: ${result.unusedTokens.length}
|
|
2396
|
+
`);
|
|
2397
|
+
if (result.unusedTokens.length > 0) {
|
|
2398
|
+
console.log(`\u26A0\uFE0F Unused Tokens (safe to remove):`);
|
|
2399
|
+
result.unusedTokens.slice(0, 20).forEach((token) => {
|
|
2400
|
+
console.log(` - ${token}`);
|
|
2401
|
+
});
|
|
2402
|
+
if (result.unusedTokens.length > 20) {
|
|
2403
|
+
console.log(` ... and ${result.unusedTokens.length - 20} more`);
|
|
2404
|
+
}
|
|
2405
|
+
console.log("");
|
|
2406
|
+
}
|
|
2407
|
+
if (result.hardcodedColors.length > 0) {
|
|
2408
|
+
console.log(`\u{1F3A8} Hardcoded Colors (should use tokens):`);
|
|
2409
|
+
result.hardcodedColors.slice(0, 10).forEach(({ file, line, value }) => {
|
|
2410
|
+
const relPath = path.relative(cwd, file);
|
|
2411
|
+
console.log(` ${relPath}:${line} - ${value}`);
|
|
2412
|
+
});
|
|
2413
|
+
if (result.hardcodedColors.length > 10) {
|
|
2414
|
+
console.log(` ... and ${result.hardcodedColors.length - 10} more`);
|
|
2415
|
+
}
|
|
2416
|
+
console.log("");
|
|
2417
|
+
}
|
|
2418
|
+
if (result.hardcodedSpacing.length > 0) {
|
|
2419
|
+
console.log(`\u{1F4CF} Hardcoded Spacing (should use tokens):`);
|
|
2420
|
+
result.hardcodedSpacing.slice(0, 10).forEach(({ file, line, value }) => {
|
|
2421
|
+
const relPath = path.relative(cwd, file);
|
|
2422
|
+
console.log(` ${relPath}:${line} - ${value}`);
|
|
2423
|
+
});
|
|
2424
|
+
if (result.hardcodedSpacing.length > 10) {
|
|
2425
|
+
console.log(` ... and ${result.hardcodedSpacing.length - 10} more`);
|
|
2426
|
+
}
|
|
2427
|
+
console.log("");
|
|
2428
|
+
}
|
|
2429
|
+
console.log(`\u2705 Scan complete
|
|
2430
|
+
`);
|
|
2431
|
+
}
|
|
2128
2432
|
async function main() {
|
|
2129
2433
|
try {
|
|
2130
2434
|
const options = parseArgs(process.argv.slice(2));
|
|
@@ -2152,6 +2456,14 @@ async function main() {
|
|
|
2152
2456
|
await runConvertCommand(cwd, options);
|
|
2153
2457
|
return;
|
|
2154
2458
|
}
|
|
2459
|
+
if (options.command === "build") {
|
|
2460
|
+
await runBuildCommand(cwd, options);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
if (options.command === "scan") {
|
|
2464
|
+
await runScanCommand(cwd, options);
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2155
2467
|
await runServeCommand(cwd, options);
|
|
2156
2468
|
} catch (error) {
|
|
2157
2469
|
console.error(error.message);
|