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 +44 -1
- package/dist/bin/tokvista.js +202 -5
- package/dist/cli/browser.js +13 -13
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +10 -10
- package/dist/styles.css +106 -0
- package/package.json +1 -1
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
|
|
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
|
package/dist/bin/tokvista.js
CHANGED
|
@@ -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
|
+
}
|