tokvista 1.13.0 → 1.14.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.
package/README.md CHANGED
@@ -19,6 +19,7 @@ Zero configuration. Multiple formats. One command.
19
19
  ## Features
20
20
 
21
21
  - 🎨 **Beautiful visuals** - Colors, spacing, typography, and components
22
+ - 📊 **Pre-ship checklist** - Analytics tab catches broken aliases, hardcoded values, and architecture issues before you publish
22
23
  - 🔄 **Multi-format support** - Token Studio, W3C, Style Dictionary, Supernova, Figma API
23
24
  - 📋 **Smart copy** - CSS Variables, SCSS, or Tailwind with one click
24
25
  - 🔍 **Instant search** - `Cmd+K` / `Ctrl+K` to find any token
@@ -111,6 +112,14 @@ npx tokvista init --no-preview
111
112
  ### Scan & Analyze
112
113
 
113
114
  ```bash
115
+ # Check token health (human-readable)
116
+ npx tokvista analytics tokens.json
117
+
118
+ # JSON output for CI/CD pipelines
119
+ npx tokvista analytics tokens.json --format json
120
+ # Exit code 1 if broken aliases found
121
+ # Perfect for GitHub Actions, GitLab CI, etc.
122
+
114
123
  # Scan for token usage and issues
115
124
  npx tokvista scan tokens.json
116
125
 
@@ -213,7 +222,7 @@ npx tokvista build tokens.json --output-dir ./dist --skip-validation
213
222
  | `tokvista diff <old> <new>` | Compare two token files |
214
223
  | `tokvista export <file>` | Export tokens to various formats |
215
224
  | `tokvista convert <file>` | Convert between token formats |
216
- | `tokvista build <file>` | Build all formats (validate + export) |
225
+ | `tokvista analytics <file>` | Check token health (CI/CD ready) |
217
226
 
218
227
  | Option | Description |
219
228
  |--------|-------------|
@@ -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) {
@@ -2584,6 +2663,10 @@ async function main() {
2584
2663
  await runScanCommand(cwd, options);
2585
2664
  return;
2586
2665
  }
2666
+ if (options.command === "analytics") {
2667
+ await runAnalyticsCommand(cwd, options);
2668
+ return;
2669
+ }
2587
2670
  await runServeCommand(cwd, options);
2588
2671
  } catch (error) {
2589
2672
  console.error(error.message);
@@ -2591,3 +2674,90 @@ async function main() {
2591
2674
  }
2592
2675
  }
2593
2676
  void main();
2677
+ function parseAnalyticsArgs(args) {
2678
+ let tokenFileArg;
2679
+ let format;
2680
+ for (let index = 0; index < args.length; index += 1) {
2681
+ const arg = args[index];
2682
+ if (arg === "-h" || arg === "--help") {
2683
+ printHelp();
2684
+ process.exit(0);
2685
+ }
2686
+ if (arg === "--format") {
2687
+ const next = args[index + 1];
2688
+ if (!next) throw new Error("Missing value for --format");
2689
+ if (!["json", "text"].includes(next)) {
2690
+ throw new Error("Format must be: json or text");
2691
+ }
2692
+ format = next;
2693
+ index += 1;
2694
+ continue;
2695
+ }
2696
+ if (arg.startsWith("--format=")) {
2697
+ const val = arg.slice("--format=".length);
2698
+ if (!["json", "text"].includes(val)) {
2699
+ throw new Error("Format must be: json or text");
2700
+ }
2701
+ format = val;
2702
+ continue;
2703
+ }
2704
+ if (arg.startsWith("-")) {
2705
+ throw new Error(`Unknown option: ${arg}`);
2706
+ }
2707
+ if (tokenFileArg) {
2708
+ throw new Error(`Only one token file is supported. Unexpected value: "${arg}"`);
2709
+ }
2710
+ tokenFileArg = arg;
2711
+ }
2712
+ if (!tokenFileArg) throw new Error("Token file is required for analytics");
2713
+ return { command: "analytics", tokenFileArg, format };
2714
+ }
2715
+ async function runAnalyticsCommand(cwd, options) {
2716
+ const resolvedTokenPath = path.resolve(cwd, options.tokenFileArg);
2717
+ if (!existsSync(resolvedTokenPath)) {
2718
+ throw new Error(`Token file not found: ${resolvedTokenPath}`);
2719
+ }
2720
+ const tokens = await readTokens(resolvedTokenPath);
2721
+ const result = analyzeTokens(tokens);
2722
+ if (options.format === "json") {
2723
+ console.log(JSON.stringify(result, null, 2));
2724
+ process.exit(result.brokenAliases.length > 0 ? 1 : 0);
2725
+ }
2726
+ console.log(`
2727
+ \u{1F4CA} Token Analytics
2728
+ `);
2729
+ console.log(`Total: ${result.total}`);
2730
+ console.log(`Foundation: ${result.foundation}`);
2731
+ console.log(`Semantic: ${result.semantic}`);
2732
+ console.log(`Components: ${result.components}
2733
+ `);
2734
+ console.log(`Aliases: ${result.aliases}`);
2735
+ console.log(`Hardcoded: ${result.hardcoded}
2736
+ `);
2737
+ if (result.brokenAliases.length > 0) {
2738
+ console.log(`\u274C Broken Aliases (${result.brokenAliases.length}):`);
2739
+ result.brokenAliases.slice(0, 10).forEach(({ path: path2, reference }) => {
2740
+ console.log(` ${path2} \u2192 ${reference}`);
2741
+ });
2742
+ if (result.brokenAliases.length > 10) {
2743
+ console.log(` ... and ${result.brokenAliases.length - 10} more`);
2744
+ }
2745
+ console.log("");
2746
+ }
2747
+ if (result.hardcodedInSemantic > 0) {
2748
+ console.log(`\u26A0\uFE0F ${result.hardcodedInSemantic} hardcoded values in Semantic layer`);
2749
+ }
2750
+ if (result.hardcodedInComponents > 0) {
2751
+ console.log(`\u26A0\uFE0F ${result.hardcodedInComponents} hardcoded values in Components layer`);
2752
+ }
2753
+ if (result.hardcodedInSemantic > 0 || result.hardcodedInComponents > 0) {
2754
+ console.log("");
2755
+ }
2756
+ if (result.brokenAliases.length > 0) {
2757
+ console.log("\u274C Quality check failed\n");
2758
+ process.exit(1);
2759
+ } else {
2760
+ console.log("\u2705 All checks passed\n");
2761
+ process.exit(0);
2762
+ }
2763
+ }