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 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) |
@@ -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 readFile(packageJsonPath, "utf8");
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 readFile(resolvedLogoPath);
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 readFile(configPath, "utf8");
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 readFile(configPath, "utf8");
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 readFile(tokenPath, "utf8");
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
- readFile(resolveDistAsset("styles.css"), "utf8"),
1921
- readFile(resolveDistAsset("cli/browser.js"), "utf8")
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokvista",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Interactive visual documentation for design tokens.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",