tokvista 1.10.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.json> [--tokens tokens.json]
1126
1255
 
1127
1256
  Arguments:
1128
1257
  tokens.json Path to your tokens file (overrides config.tokens)
@@ -1137,6 +1266,10 @@ Options:
1137
1266
  --no-watch Disable live reload (serve only)
1138
1267
  --no-preview Skip starting live preview after init
1139
1268
  -h, --help Show this help message
1269
+
1270
+ Scan command usage:
1271
+ tokvista scan ./src --tokens tokens.json # Scan directory with specific tokens
1272
+ tokvista scan tokens.json # Scan current directory using tokens file
1140
1273
  `);
1141
1274
  }
1142
1275
  function parsePort(value) {
@@ -1261,6 +1394,9 @@ function parseArgs(args) {
1261
1394
  if (args[0] === "build") {
1262
1395
  return parseBuildArgs(args.slice(1));
1263
1396
  }
1397
+ if (args[0] === "scan") {
1398
+ return parseScanArgs(args.slice(1));
1399
+ }
1264
1400
  return parseServeArgs(args);
1265
1401
  }
1266
1402
  function parseValidateArgs(args) {
@@ -1394,6 +1530,37 @@ function parseBuildArgs(args) {
1394
1530
  if (!outputDir) throw new Error("--output-dir is required");
1395
1531
  return { command: "build", tokenFileArg, outputDir, skipValidation };
1396
1532
  }
1533
+ function parseScanArgs(args) {
1534
+ let scanDir;
1535
+ let tokenFileArg;
1536
+ for (let index = 0; index < args.length; index += 1) {
1537
+ const arg = args[index];
1538
+ if (arg === "-h" || arg === "--help") {
1539
+ printHelp();
1540
+ process.exit(0);
1541
+ }
1542
+ if (arg === "--tokens") {
1543
+ const next = args[index + 1];
1544
+ if (!next) throw new Error("Missing value for --tokens");
1545
+ tokenFileArg = next;
1546
+ index += 1;
1547
+ continue;
1548
+ }
1549
+ if (arg.startsWith("--tokens=")) {
1550
+ tokenFileArg = arg.slice("--tokens=".length);
1551
+ continue;
1552
+ }
1553
+ if (arg.startsWith("-")) {
1554
+ throw new Error(`Unknown option: ${arg}`);
1555
+ }
1556
+ if (scanDir) {
1557
+ throw new Error(`Only one directory or token file is supported. Unexpected value: "${arg}"`);
1558
+ }
1559
+ scanDir = arg;
1560
+ }
1561
+ if (!scanDir) throw new Error("Directory to scan or token file is required");
1562
+ return { command: "scan", scanDir, tokenFileArg };
1563
+ }
1397
1564
  function parseExportArgs(args) {
1398
1565
  let tokenFileArg;
1399
1566
  let format;
@@ -1460,7 +1627,7 @@ async function resolveDefaultInitTitle(cwd) {
1460
1627
  return "My Design System";
1461
1628
  }
1462
1629
  try {
1463
- const raw = await readFile(packageJsonPath, "utf8");
1630
+ const raw = await readFile2(packageJsonPath, "utf8");
1464
1631
  const parsed = JSON.parse(raw);
1465
1632
  if (typeof parsed.name === "string") {
1466
1633
  return formatTitleFromPackageName(parsed.name);
@@ -1700,7 +1867,7 @@ async function resolveLogoForRuntime(logoPathValue, configDir, cwd) {
1700
1867
  if (!existsSync(resolvedLogoPath)) {
1701
1868
  throw new Error(`Logo file not found: ${resolvedLogoPath}`);
1702
1869
  }
1703
- const content = await readFile(resolvedLogoPath);
1870
+ const content = await readFile2(resolvedLogoPath);
1704
1871
  const mimeType = toDataUrlMimeType(resolvedLogoPath);
1705
1872
  return `data:${mimeType};base64,${content.toString("base64")}`;
1706
1873
  }
@@ -1788,7 +1955,7 @@ async function loadConfigFromFile(configPath) {
1788
1955
  const sourceLabel = path.basename(configPath);
1789
1956
  const extension = path.extname(configPath).toLowerCase();
1790
1957
  if (extension === ".json") {
1791
- const raw = await readFile(configPath, "utf8");
1958
+ const raw = await readFile2(configPath, "utf8");
1792
1959
  try {
1793
1960
  return normalizeConfigObject(JSON.parse(raw), sourceLabel);
1794
1961
  } catch (error) {
@@ -1796,7 +1963,7 @@ async function loadConfigFromFile(configPath) {
1796
1963
  }
1797
1964
  }
1798
1965
  if (extension === ".ts") {
1799
- const raw = await readFile(configPath, "utf8");
1966
+ const raw = await readFile2(configPath, "utf8");
1800
1967
  const parsed = parseTsConfigSource(raw, sourceLabel);
1801
1968
  return normalizeConfigObject(parsed, sourceLabel);
1802
1969
  }
@@ -1937,7 +2104,7 @@ function openBrowser(url) {
1937
2104
  });
1938
2105
  }
1939
2106
  async function readTokens(tokenPath) {
1940
- const raw = await readFile(tokenPath, "utf8");
2107
+ const raw = await readFile2(tokenPath, "utf8");
1941
2108
  try {
1942
2109
  const parsed = JSON.parse(raw);
1943
2110
  if (!parsed || typeof parsed !== "object") {
@@ -1958,8 +2125,8 @@ async function runServeCommand(cwd, options) {
1958
2125
  }
1959
2126
  const runtimeConfig = await buildRuntimeConfig(config, configPath, cwd);
1960
2127
  const [css, appBundle] = await Promise.all([
1961
- readFile(resolveDistAsset("styles.css"), "utf8"),
1962
- readFile(resolveDistAsset("cli/browser.js"), "utf8")
2128
+ readFile2(resolveDistAsset("styles.css"), "utf8"),
2129
+ readFile2(resolveDistAsset("cli/browser.js"), "utf8")
1963
2130
  ]);
1964
2131
  let cachedTokens = await readTokens(resolvedTokenPath);
1965
2132
  const getHtml = () => buildHtml(cachedTokens, runtimeConfig, css, appBundle, options.watch);
@@ -2209,6 +2376,119 @@ async function runBuildCommand(cwd, options) {
2209
2376
  \u2705 Build complete: ${outputDir}
2210
2377
  `);
2211
2378
  }
2379
+ async function runScanCommand(cwd, options) {
2380
+ const resolvedScanPath = path.resolve(cwd, options.scanDir);
2381
+ if (existsSync(resolvedScanPath)) {
2382
+ const fs = await import("fs");
2383
+ if (!fs.statSync(resolvedScanPath).isDirectory()) {
2384
+ const tokenPath2 = resolvedScanPath;
2385
+ const scanDir2 = cwd;
2386
+ const tokens2 = await readTokens(tokenPath2);
2387
+ console.log(`
2388
+ Scanning ${scanDir2} for token usage...
2389
+ `);
2390
+ const result2 = await scanTokenUsage(tokenPath2, scanDir2, tokens2);
2391
+ const usagePercent2 = (result2.usedTokens.length / result2.totalTokens * 100).toFixed(1);
2392
+ console.log(`\u{1F4CA} Token Usage Report
2393
+ `);
2394
+ console.log(`Files scanned: ${result2.filesScanned}`);
2395
+ console.log(`Total tokens: ${result2.totalTokens}`);
2396
+ console.log(`Used tokens: ${result2.usedTokens.length} (${usagePercent2}%)`);
2397
+ console.log(`Unused tokens: ${result2.unusedTokens.length}
2398
+ `);
2399
+ if (result2.unusedTokens.length > 0) {
2400
+ console.log(`\u26A0\uFE0F Unused Tokens (safe to remove):`);
2401
+ result2.unusedTokens.slice(0, 20).forEach((token) => {
2402
+ console.log(` - ${token}`);
2403
+ });
2404
+ if (result2.unusedTokens.length > 20) {
2405
+ console.log(` ... and ${result2.unusedTokens.length - 20} more`);
2406
+ }
2407
+ console.log("");
2408
+ }
2409
+ if (result2.hardcodedColors.length > 0) {
2410
+ console.log(`\u{1F3A8} Hardcoded Colors (should use tokens):`);
2411
+ result2.hardcodedColors.slice(0, 10).forEach(({ file, line, value }) => {
2412
+ const relPath = path.relative(cwd, file);
2413
+ console.log(` ${relPath}:${line} - ${value}`);
2414
+ });
2415
+ if (result2.hardcodedColors.length > 10) {
2416
+ console.log(` ... and ${result2.hardcodedColors.length - 10} more`);
2417
+ }
2418
+ console.log("");
2419
+ }
2420
+ if (result2.hardcodedSpacing.length > 0) {
2421
+ console.log(`\u{1F4CF} Hardcoded Spacing (should use tokens):`);
2422
+ result2.hardcodedSpacing.slice(0, 10).forEach(({ file, line, value }) => {
2423
+ const relPath = path.relative(cwd, file);
2424
+ console.log(` ${relPath}:${line} - ${value}`);
2425
+ });
2426
+ if (result2.hardcodedSpacing.length > 10) {
2427
+ console.log(` ... and ${result2.hardcodedSpacing.length - 10} more`);
2428
+ }
2429
+ console.log("");
2430
+ }
2431
+ console.log(`\u2705 Scan complete
2432
+ `);
2433
+ return;
2434
+ }
2435
+ }
2436
+ const scanDir = resolvedScanPath;
2437
+ const tokenPath = options.tokenFileArg ? path.resolve(cwd, options.tokenFileArg) : path.resolve(cwd, "tokens.json");
2438
+ if (!existsSync(scanDir)) {
2439
+ throw new Error(`Directory not found: ${scanDir}`);
2440
+ }
2441
+ if (!existsSync(tokenPath)) {
2442
+ throw new Error(`Token file not found: ${tokenPath}`);
2443
+ }
2444
+ const tokens = await readTokens(tokenPath);
2445
+ console.log(`
2446
+ Scanning ${scanDir} for token usage...
2447
+ `);
2448
+ const result = await scanTokenUsage(tokenPath, scanDir, tokens);
2449
+ const usagePercent = (result.usedTokens.length / result.totalTokens * 100).toFixed(1);
2450
+ console.log(`\u{1F4CA} Token Usage Report
2451
+ `);
2452
+ console.log(`Files scanned: ${result.filesScanned}`);
2453
+ console.log(`Total tokens: ${result.totalTokens}`);
2454
+ console.log(`Used tokens: ${result.usedTokens.length} (${usagePercent}%)`);
2455
+ console.log(`Unused tokens: ${result.unusedTokens.length}
2456
+ `);
2457
+ if (result.unusedTokens.length > 0) {
2458
+ console.log(`\u26A0\uFE0F Unused Tokens (safe to remove):`);
2459
+ result.unusedTokens.slice(0, 20).forEach((token) => {
2460
+ console.log(` - ${token}`);
2461
+ });
2462
+ if (result.unusedTokens.length > 20) {
2463
+ console.log(` ... and ${result.unusedTokens.length - 20} more`);
2464
+ }
2465
+ console.log("");
2466
+ }
2467
+ if (result.hardcodedColors.length > 0) {
2468
+ console.log(`\u{1F3A8} Hardcoded Colors (should use tokens):`);
2469
+ result.hardcodedColors.slice(0, 10).forEach(({ file, line, value }) => {
2470
+ const relPath = path.relative(cwd, file);
2471
+ console.log(` ${relPath}:${line} - ${value}`);
2472
+ });
2473
+ if (result.hardcodedColors.length > 10) {
2474
+ console.log(` ... and ${result.hardcodedColors.length - 10} more`);
2475
+ }
2476
+ console.log("");
2477
+ }
2478
+ if (result.hardcodedSpacing.length > 0) {
2479
+ console.log(`\u{1F4CF} Hardcoded Spacing (should use tokens):`);
2480
+ result.hardcodedSpacing.slice(0, 10).forEach(({ file, line, value }) => {
2481
+ const relPath = path.relative(cwd, file);
2482
+ console.log(` ${relPath}:${line} - ${value}`);
2483
+ });
2484
+ if (result.hardcodedSpacing.length > 10) {
2485
+ console.log(` ... and ${result.hardcodedSpacing.length - 10} more`);
2486
+ }
2487
+ console.log("");
2488
+ }
2489
+ console.log(`\u2705 Scan complete
2490
+ `);
2491
+ }
2212
2492
  async function main() {
2213
2493
  try {
2214
2494
  const options = parseArgs(process.argv.slice(2));
@@ -2240,6 +2520,10 @@ async function main() {
2240
2520
  await runBuildCommand(cwd, options);
2241
2521
  return;
2242
2522
  }
2523
+ if (options.command === "scan") {
2524
+ await runScanCommand(cwd, options);
2525
+ return;
2526
+ }
2243
2527
  await runServeCommand(cwd, options);
2244
2528
  } catch (error) {
2245
2529
  console.error(error.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokvista",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
4
4
  "description": "Interactive visual documentation for design tokens.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",