tokvista 1.10.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/dist/bin/tokvista.js +232 -8
- package/package.json +1 -1
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) {
|
|
@@ -1123,6 +1251,7 @@ Usage:
|
|
|
1123
1251
|
tokvista diff <old.json> <new.json>
|
|
1124
1252
|
tokvista convert <tokens.json> --to <w3c|style-dictionary|supernova> [--output <file>]
|
|
1125
1253
|
tokvista build <tokens.json> --output-dir <dir> [--skip-validation]
|
|
1254
|
+
tokvista scan <directory> [--tokens tokens.json]
|
|
1126
1255
|
|
|
1127
1256
|
Arguments:
|
|
1128
1257
|
tokens.json Path to your tokens file (overrides config.tokens)
|
|
@@ -1261,6 +1390,9 @@ function parseArgs(args) {
|
|
|
1261
1390
|
if (args[0] === "build") {
|
|
1262
1391
|
return parseBuildArgs(args.slice(1));
|
|
1263
1392
|
}
|
|
1393
|
+
if (args[0] === "scan") {
|
|
1394
|
+
return parseScanArgs(args.slice(1));
|
|
1395
|
+
}
|
|
1264
1396
|
return parseServeArgs(args);
|
|
1265
1397
|
}
|
|
1266
1398
|
function parseValidateArgs(args) {
|
|
@@ -1394,6 +1526,37 @@ function parseBuildArgs(args) {
|
|
|
1394
1526
|
if (!outputDir) throw new Error("--output-dir is required");
|
|
1395
1527
|
return { command: "build", tokenFileArg, outputDir, skipValidation };
|
|
1396
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
|
+
}
|
|
1397
1560
|
function parseExportArgs(args) {
|
|
1398
1561
|
let tokenFileArg;
|
|
1399
1562
|
let format;
|
|
@@ -1460,7 +1623,7 @@ async function resolveDefaultInitTitle(cwd) {
|
|
|
1460
1623
|
return "My Design System";
|
|
1461
1624
|
}
|
|
1462
1625
|
try {
|
|
1463
|
-
const raw = await
|
|
1626
|
+
const raw = await readFile2(packageJsonPath, "utf8");
|
|
1464
1627
|
const parsed = JSON.parse(raw);
|
|
1465
1628
|
if (typeof parsed.name === "string") {
|
|
1466
1629
|
return formatTitleFromPackageName(parsed.name);
|
|
@@ -1700,7 +1863,7 @@ async function resolveLogoForRuntime(logoPathValue, configDir, cwd) {
|
|
|
1700
1863
|
if (!existsSync(resolvedLogoPath)) {
|
|
1701
1864
|
throw new Error(`Logo file not found: ${resolvedLogoPath}`);
|
|
1702
1865
|
}
|
|
1703
|
-
const content = await
|
|
1866
|
+
const content = await readFile2(resolvedLogoPath);
|
|
1704
1867
|
const mimeType = toDataUrlMimeType(resolvedLogoPath);
|
|
1705
1868
|
return `data:${mimeType};base64,${content.toString("base64")}`;
|
|
1706
1869
|
}
|
|
@@ -1788,7 +1951,7 @@ async function loadConfigFromFile(configPath) {
|
|
|
1788
1951
|
const sourceLabel = path.basename(configPath);
|
|
1789
1952
|
const extension = path.extname(configPath).toLowerCase();
|
|
1790
1953
|
if (extension === ".json") {
|
|
1791
|
-
const raw = await
|
|
1954
|
+
const raw = await readFile2(configPath, "utf8");
|
|
1792
1955
|
try {
|
|
1793
1956
|
return normalizeConfigObject(JSON.parse(raw), sourceLabel);
|
|
1794
1957
|
} catch (error) {
|
|
@@ -1796,7 +1959,7 @@ async function loadConfigFromFile(configPath) {
|
|
|
1796
1959
|
}
|
|
1797
1960
|
}
|
|
1798
1961
|
if (extension === ".ts") {
|
|
1799
|
-
const raw = await
|
|
1962
|
+
const raw = await readFile2(configPath, "utf8");
|
|
1800
1963
|
const parsed = parseTsConfigSource(raw, sourceLabel);
|
|
1801
1964
|
return normalizeConfigObject(parsed, sourceLabel);
|
|
1802
1965
|
}
|
|
@@ -1937,7 +2100,7 @@ function openBrowser(url) {
|
|
|
1937
2100
|
});
|
|
1938
2101
|
}
|
|
1939
2102
|
async function readTokens(tokenPath) {
|
|
1940
|
-
const raw = await
|
|
2103
|
+
const raw = await readFile2(tokenPath, "utf8");
|
|
1941
2104
|
try {
|
|
1942
2105
|
const parsed = JSON.parse(raw);
|
|
1943
2106
|
if (!parsed || typeof parsed !== "object") {
|
|
@@ -1958,8 +2121,8 @@ async function runServeCommand(cwd, options) {
|
|
|
1958
2121
|
}
|
|
1959
2122
|
const runtimeConfig = await buildRuntimeConfig(config, configPath, cwd);
|
|
1960
2123
|
const [css, appBundle] = await Promise.all([
|
|
1961
|
-
|
|
1962
|
-
|
|
2124
|
+
readFile2(resolveDistAsset("styles.css"), "utf8"),
|
|
2125
|
+
readFile2(resolveDistAsset("cli/browser.js"), "utf8")
|
|
1963
2126
|
]);
|
|
1964
2127
|
let cachedTokens = await readTokens(resolvedTokenPath);
|
|
1965
2128
|
const getHtml = () => buildHtml(cachedTokens, runtimeConfig, css, appBundle, options.watch);
|
|
@@ -2209,6 +2372,63 @@ async function runBuildCommand(cwd, options) {
|
|
|
2209
2372
|
\u2705 Build complete: ${outputDir}
|
|
2210
2373
|
`);
|
|
2211
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
|
+
}
|
|
2212
2432
|
async function main() {
|
|
2213
2433
|
try {
|
|
2214
2434
|
const options = parseArgs(process.argv.slice(2));
|
|
@@ -2240,6 +2460,10 @@ async function main() {
|
|
|
2240
2460
|
await runBuildCommand(cwd, options);
|
|
2241
2461
|
return;
|
|
2242
2462
|
}
|
|
2463
|
+
if (options.command === "scan") {
|
|
2464
|
+
await runScanCommand(cwd, options);
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2243
2467
|
await runServeCommand(cwd, options);
|
|
2244
2468
|
} catch (error) {
|
|
2245
2469
|
console.error(error.message);
|