tokvista 1.14.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
@@ -112,6 +112,14 @@ npx tokvista init --no-preview
112
112
  ### Scan & Analyze
113
113
 
114
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
+
115
123
  # Scan for token usage and issues
116
124
  npx tokvista scan tokens.json
117
125
 
@@ -214,7 +222,7 @@ npx tokvista build tokens.json --output-dir ./dist --skip-validation
214
222
  | `tokvista diff <old> <new>` | Compare two token files |
215
223
  | `tokvista export <file>` | Export tokens to various formats |
216
224
  | `tokvista convert <file>` | Convert between token formats |
217
- | `tokvista build <file>` | Build all formats (validate + export) |
225
+ | `tokvista analytics <file>` | Check token health (CI/CD ready) |
218
226
 
219
227
  | Option | Description |
220
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokvista",
3
- "version": "1.14.0",
3
+ "version": "1.14.1",
4
4
  "description": "Interactive visual documentation for design tokens.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",