tokvista 1.14.0 → 1.15.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
@@ -20,6 +20,7 @@ Zero configuration. Multiple formats. One command.
20
20
 
21
21
  - 🎨 **Beautiful visuals** - Colors, spacing, typography, and components
22
22
  - 📊 **Pre-ship checklist** - Analytics tab catches broken aliases, hardcoded values, and architecture issues before you publish
23
+ - 🔥 **Token usage heatmap** - See which tokens are actually used in your codebase
23
24
  - 🔄 **Multi-format support** - Token Studio, W3C, Style Dictionary, Supernova, Figma API
24
25
  - 📋 **Smart copy** - CSS Variables, SCSS, or Tailwind with one click
25
26
  - 🔍 **Instant search** - `Cmd+K` / `Ctrl+K` to find any token
@@ -112,12 +113,23 @@ npx tokvista init --no-preview
112
113
  ### Scan & Analyze
113
114
 
114
115
  ```bash
116
+ # Check token health (human-readable)
117
+ npx tokvista analytics tokens.json
118
+
119
+ # JSON output for CI/CD pipelines
120
+ npx tokvista analytics tokens.json --format json
121
+ # Exit code 1 if broken aliases found
122
+ # Perfect for GitHub Actions, GitLab CI, etc.
123
+
115
124
  # Scan for token usage and issues
116
125
  npx tokvista scan tokens.json
117
126
 
118
127
  # Scan specific directory
119
128
  npx tokvista scan ./src --tokens tokens.json
120
129
 
130
+ # JSON output for usage heatmap
131
+ npx tokvista scan ./src --tokens tokens.json --format json > usage.json
132
+
121
133
  # Finds:
122
134
  # - Unused tokens (safe to remove)
123
135
  # - Hardcoded colors that should use tokens
@@ -214,7 +226,7 @@ npx tokvista build tokens.json --output-dir ./dist --skip-validation
214
226
  | `tokvista diff <old> <new>` | Compare two token files |
215
227
  | `tokvista export <file>` | Export tokens to various formats |
216
228
  | `tokvista convert <file>` | Convert between token formats |
217
- | `tokvista build <file>` | Build all formats (validate + export) |
229
+ | `tokvista analytics <file>` | Check token health (CI/CD ready) |
218
230
 
219
231
  | Option | Description |
220
232
  |--------|-------------|
@@ -248,9 +260,40 @@ npx tokvista build tokens.json --output-dir ./dist --skip-validation
248
260
  theme="dark" // Optional: 'light' | 'dark' | 'system'
249
261
  brandColor="#6366f1" // Optional: primary color
250
262
  onTokenClick={(token) => {}} // Optional: click handler
263
+ usageData={usageData} // Optional: token usage from scan command
251
264
  />
252
265
  ```
253
266
 
267
+ ### Token Usage Heatmap
268
+
269
+ Show which tokens are actually used in your codebase:
270
+
271
+ ```bash
272
+ # Generate usage data
273
+ npx tokvista scan ./src --tokens tokens.json --format json > usage.json
274
+ ```
275
+
276
+ ```tsx
277
+ import { TokenDocumentation } from 'tokvista';
278
+ import tokens from './tokens.json';
279
+ import usageData from './usage.json';
280
+
281
+ export default function DesignSystem() {
282
+ return (
283
+ <TokenDocumentation
284
+ tokens={tokens}
285
+ usageData={usageData}
286
+ />
287
+ );
288
+ }
289
+ ```
290
+
291
+ The Analytics tab will show:
292
+ - Used vs unused tokens
293
+ - Adoption percentage
294
+ - List of unused tokens (safe to delete)
295
+ - Files scanned count
296
+
254
297
  ### Custom Fonts
255
298
 
256
299
  ```tsx
@@ -1262,6 +1262,82 @@ async function scanTokenUsage(tokensPath, scanDir, tokens) {
1262
1262
  };
1263
1263
  }
1264
1264
 
1265
+ // src/utils/analytics.ts
1266
+ function isTokenLeaf(value) {
1267
+ return typeof value === "object" && value !== null && "type" in value && "value" in value;
1268
+ }
1269
+ function isAlias(value) {
1270
+ return typeof value === "string" && /^\{[^{}]+\}$/.test(value.trim());
1271
+ }
1272
+ function collectStats(obj, tokenMap, layer, path2 = [], stats) {
1273
+ if (!obj || typeof obj !== "object") return stats;
1274
+ if (isTokenLeaf(obj)) {
1275
+ stats.total++;
1276
+ stats[layer]++;
1277
+ const type = obj.type || "unknown";
1278
+ if (!stats.byType[type]) {
1279
+ stats.byType[type] = { total: 0, foundation: 0, semantic: 0, components: 0 };
1280
+ }
1281
+ stats.byType[type].total++;
1282
+ stats.byType[type][layer]++;
1283
+ if (type === "unknown") {
1284
+ const rawType = String(obj.type || "missing");
1285
+ stats.otherTypes[rawType] = (stats.otherTypes[rawType] || 0) + 1;
1286
+ }
1287
+ if (isAlias(obj.value)) {
1288
+ stats.aliases++;
1289
+ const resolved = resolveTokenValue(obj.value, tokenMap);
1290
+ if (resolved === obj.value) {
1291
+ stats.brokenAliases.push({ path: path2.join("."), reference: obj.value });
1292
+ }
1293
+ } else {
1294
+ stats.hardcoded++;
1295
+ if (layer === "semantic") {
1296
+ stats.hardcodedInSemantic++;
1297
+ stats.hardcodedSemanticPaths.push(path2.join("."));
1298
+ }
1299
+ if (layer === "components") {
1300
+ stats.hardcodedInComponents++;
1301
+ stats.hardcodedComponentPaths.push(path2.join("."));
1302
+ }
1303
+ }
1304
+ return stats;
1305
+ }
1306
+ if (Array.isArray(obj)) {
1307
+ obj.forEach((item, i) => collectStats(item, tokenMap, layer, [...path2, String(i)], stats));
1308
+ } else {
1309
+ Object.entries(obj).forEach(
1310
+ ([key, value]) => collectStats(value, tokenMap, layer, [...path2, key], stats)
1311
+ );
1312
+ }
1313
+ return stats;
1314
+ }
1315
+ function analyzeTokens(tokens) {
1316
+ const tokenMap = createTokenMap(tokens);
1317
+ const foundation = extractFoundationSet(tokens);
1318
+ const semantic = extractSemanticSet(tokens);
1319
+ const components = extractComponentSet(tokens);
1320
+ const result = {
1321
+ total: 0,
1322
+ foundation: 0,
1323
+ semantic: 0,
1324
+ components: 0,
1325
+ byType: {},
1326
+ aliases: 0,
1327
+ hardcoded: 0,
1328
+ brokenAliases: [],
1329
+ hardcodedInSemantic: 0,
1330
+ hardcodedInComponents: 0,
1331
+ hardcodedSemanticPaths: [],
1332
+ hardcodedComponentPaths: [],
1333
+ otherTypes: {}
1334
+ };
1335
+ collectStats(foundation, tokenMap, "foundation", [], result);
1336
+ collectStats(semantic, tokenMap, "semantic", [], result);
1337
+ collectStats(components, tokenMap, "components", [], result);
1338
+ return result;
1339
+ }
1340
+
1265
1341
  // src/bin/watcher.ts
1266
1342
  import { watch } from "fs";
1267
1343
  function watchFile(filePath, onChange) {
@@ -1443,6 +1519,9 @@ function parseArgs(args) {
1443
1519
  if (args[0] === "scan") {
1444
1520
  return parseScanArgs(args.slice(1));
1445
1521
  }
1522
+ if (args[0] === "analytics") {
1523
+ return parseAnalyticsArgs(args.slice(1));
1524
+ }
1446
1525
  return parseServeArgs(args);
1447
1526
  }
1448
1527
  function parseValidateArgs(args) {
@@ -1579,6 +1658,7 @@ function parseBuildArgs(args) {
1579
1658
  function parseScanArgs(args) {
1580
1659
  let scanDir;
1581
1660
  let tokenFileArg;
1661
+ let format;
1582
1662
  for (let index = 0; index < args.length; index += 1) {
1583
1663
  const arg = args[index];
1584
1664
  if (arg === "-h" || arg === "--help") {
@@ -1596,6 +1676,24 @@ function parseScanArgs(args) {
1596
1676
  tokenFileArg = arg.slice("--tokens=".length);
1597
1677
  continue;
1598
1678
  }
1679
+ if (arg === "--format") {
1680
+ const next = args[index + 1];
1681
+ if (!next) throw new Error("Missing value for --format");
1682
+ if (!["json", "text"].includes(next)) {
1683
+ throw new Error("Format must be: json or text");
1684
+ }
1685
+ format = next;
1686
+ index += 1;
1687
+ continue;
1688
+ }
1689
+ if (arg.startsWith("--format=")) {
1690
+ const val = arg.slice("--format=".length);
1691
+ if (!["json", "text"].includes(val)) {
1692
+ throw new Error("Format must be: json or text");
1693
+ }
1694
+ format = val;
1695
+ continue;
1696
+ }
1599
1697
  if (arg.startsWith("-")) {
1600
1698
  throw new Error(`Unknown option: ${arg}`);
1601
1699
  }
@@ -1605,7 +1703,7 @@ function parseScanArgs(args) {
1605
1703
  scanDir = arg;
1606
1704
  }
1607
1705
  if (!scanDir) throw new Error("Directory to scan or token file is required");
1608
- return { command: "scan", scanDir, tokenFileArg };
1706
+ return { command: "scan", scanDir, tokenFileArg, format };
1609
1707
  }
1610
1708
  function parseExportArgs(args) {
1611
1709
  let tokenFileArg;
@@ -2430,11 +2528,15 @@ async function runScanCommand(cwd, options) {
2430
2528
  const tokenPath2 = resolvedScanPath;
2431
2529
  const scanDir2 = cwd;
2432
2530
  const tokens2 = await readTokens(tokenPath2);
2531
+ const result2 = await scanTokenUsage(tokenPath2, scanDir2, tokens2);
2532
+ if (options.format === "json") {
2533
+ console.log(JSON.stringify(result2, null, 2));
2534
+ return;
2535
+ }
2536
+ const usagePercent2 = (result2.usedTokens.length / result2.totalTokens * 100).toFixed(1);
2433
2537
  console.log(`
2434
2538
  Scanning ${scanDir2} for token usage...
2435
2539
  `);
2436
- const result2 = await scanTokenUsage(tokenPath2, scanDir2, tokens2);
2437
- const usagePercent2 = (result2.usedTokens.length / result2.totalTokens * 100).toFixed(1);
2438
2540
  console.log(`\u{1F4CA} Token Usage Report
2439
2541
  `);
2440
2542
  console.log(`Files scanned: ${result2.filesScanned}`);
@@ -2495,11 +2597,15 @@ Scanning ${scanDir2} for token usage...
2495
2597
  throw new Error(`Token file not found: ${tokenPath}`);
2496
2598
  }
2497
2599
  const tokens = await readTokens(tokenPath);
2600
+ const result = await scanTokenUsage(tokenPath, scanDir, tokens);
2601
+ if (options.format === "json") {
2602
+ console.log(JSON.stringify(result, null, 2));
2603
+ return;
2604
+ }
2605
+ const usagePercent = (result.usedTokens.length / result.totalTokens * 100).toFixed(1);
2498
2606
  console.log(`
2499
2607
  Scanning ${scanDir} for token usage...
2500
2608
  `);
2501
- const result = await scanTokenUsage(tokenPath, scanDir, tokens);
2502
- const usagePercent = (result.usedTokens.length / result.totalTokens * 100).toFixed(1);
2503
2609
  console.log(`\u{1F4CA} Token Usage Report
2504
2610
  `);
2505
2611
  console.log(`Files scanned: ${result.filesScanned}`);
@@ -2584,6 +2690,10 @@ async function main() {
2584
2690
  await runScanCommand(cwd, options);
2585
2691
  return;
2586
2692
  }
2693
+ if (options.command === "analytics") {
2694
+ await runAnalyticsCommand(cwd, options);
2695
+ return;
2696
+ }
2587
2697
  await runServeCommand(cwd, options);
2588
2698
  } catch (error) {
2589
2699
  console.error(error.message);
@@ -2591,3 +2701,90 @@ async function main() {
2591
2701
  }
2592
2702
  }
2593
2703
  void main();
2704
+ function parseAnalyticsArgs(args) {
2705
+ let tokenFileArg;
2706
+ let format;
2707
+ for (let index = 0; index < args.length; index += 1) {
2708
+ const arg = args[index];
2709
+ if (arg === "-h" || arg === "--help") {
2710
+ printHelp();
2711
+ process.exit(0);
2712
+ }
2713
+ if (arg === "--format") {
2714
+ const next = args[index + 1];
2715
+ if (!next) throw new Error("Missing value for --format");
2716
+ if (!["json", "text"].includes(next)) {
2717
+ throw new Error("Format must be: json or text");
2718
+ }
2719
+ format = next;
2720
+ index += 1;
2721
+ continue;
2722
+ }
2723
+ if (arg.startsWith("--format=")) {
2724
+ const val = arg.slice("--format=".length);
2725
+ if (!["json", "text"].includes(val)) {
2726
+ throw new Error("Format must be: json or text");
2727
+ }
2728
+ format = val;
2729
+ continue;
2730
+ }
2731
+ if (arg.startsWith("-")) {
2732
+ throw new Error(`Unknown option: ${arg}`);
2733
+ }
2734
+ if (tokenFileArg) {
2735
+ throw new Error(`Only one token file is supported. Unexpected value: "${arg}"`);
2736
+ }
2737
+ tokenFileArg = arg;
2738
+ }
2739
+ if (!tokenFileArg) throw new Error("Token file is required for analytics");
2740
+ return { command: "analytics", tokenFileArg, format };
2741
+ }
2742
+ async function runAnalyticsCommand(cwd, options) {
2743
+ const resolvedTokenPath = path.resolve(cwd, options.tokenFileArg);
2744
+ if (!existsSync(resolvedTokenPath)) {
2745
+ throw new Error(`Token file not found: ${resolvedTokenPath}`);
2746
+ }
2747
+ const tokens = await readTokens(resolvedTokenPath);
2748
+ const result = analyzeTokens(tokens);
2749
+ if (options.format === "json") {
2750
+ console.log(JSON.stringify(result, null, 2));
2751
+ process.exit(result.brokenAliases.length > 0 ? 1 : 0);
2752
+ }
2753
+ console.log(`
2754
+ \u{1F4CA} Token Analytics
2755
+ `);
2756
+ console.log(`Total: ${result.total}`);
2757
+ console.log(`Foundation: ${result.foundation}`);
2758
+ console.log(`Semantic: ${result.semantic}`);
2759
+ console.log(`Components: ${result.components}
2760
+ `);
2761
+ console.log(`Aliases: ${result.aliases}`);
2762
+ console.log(`Hardcoded: ${result.hardcoded}
2763
+ `);
2764
+ if (result.brokenAliases.length > 0) {
2765
+ console.log(`\u274C Broken Aliases (${result.brokenAliases.length}):`);
2766
+ result.brokenAliases.slice(0, 10).forEach(({ path: path2, reference }) => {
2767
+ console.log(` ${path2} \u2192 ${reference}`);
2768
+ });
2769
+ if (result.brokenAliases.length > 10) {
2770
+ console.log(` ... and ${result.brokenAliases.length - 10} more`);
2771
+ }
2772
+ console.log("");
2773
+ }
2774
+ if (result.hardcodedInSemantic > 0) {
2775
+ console.log(`\u26A0\uFE0F ${result.hardcodedInSemantic} hardcoded values in Semantic layer`);
2776
+ }
2777
+ if (result.hardcodedInComponents > 0) {
2778
+ console.log(`\u26A0\uFE0F ${result.hardcodedInComponents} hardcoded values in Components layer`);
2779
+ }
2780
+ if (result.hardcodedInSemantic > 0 || result.hardcodedInComponents > 0) {
2781
+ console.log("");
2782
+ }
2783
+ if (result.brokenAliases.length > 0) {
2784
+ console.log("\u274C Quality check failed\n");
2785
+ process.exit(1);
2786
+ } else {
2787
+ console.log("\u2705 All checks passed\n");
2788
+ process.exit(0);
2789
+ }
2790
+ }