tokvista 1.7.1 → 1.9.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 +30 -0
- package/dist/bin/tokvista.js +308 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,6 +119,34 @@ npx tokvista validate tokens.json
|
|
|
119
119
|
npm run validate-tokens
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
### Compare Tokens
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Compare two token files
|
|
126
|
+
npx tokvista diff tokens-v1.json tokens-v2.json
|
|
127
|
+
|
|
128
|
+
# Perfect for:
|
|
129
|
+
# - Version control reviews
|
|
130
|
+
# - Release changelogs
|
|
131
|
+
# - Migration tracking
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Convert Formats
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Convert to W3C DTCG format
|
|
138
|
+
npx tokvista convert tokens.json --to w3c --output tokens-w3c.json
|
|
139
|
+
|
|
140
|
+
# Convert to Style Dictionary
|
|
141
|
+
npx tokvista convert tokens.json --to style-dictionary --output tokens-sd.json
|
|
142
|
+
|
|
143
|
+
# Convert to Supernova array format
|
|
144
|
+
npx tokvista convert tokens.json --to supernova --output tokens-sn.json
|
|
145
|
+
|
|
146
|
+
# Print to stdout
|
|
147
|
+
npx tokvista convert tokens.json --to w3c
|
|
148
|
+
```
|
|
149
|
+
|
|
122
150
|
### Interactive Setup
|
|
123
151
|
|
|
124
152
|
```bash
|
|
@@ -149,6 +177,8 @@ Then run `npx tokvista` to use your config.
|
|
|
149
177
|
| `tokvista init` | Interactive config setup |
|
|
150
178
|
| `tokvista export <file> --format <type>` | Export tokens (css, scss, json, tailwind) |
|
|
151
179
|
| `tokvista validate <file>` | Validate token structure and values |
|
|
180
|
+
| `tokvista diff <old> <new>` | Compare two token files |
|
|
181
|
+
| `tokvista convert <file> --to <format>` | Convert between token formats |
|
|
152
182
|
| `--config`, `-c` | Config file path |
|
|
153
183
|
| `--port`, `-p` | Server port (default: `3000`) |
|
|
154
184
|
| `--format` | Export format (export only) |
|
package/dist/bin/tokvista.js
CHANGED
|
@@ -935,6 +935,159 @@ function validateTokens(tokens) {
|
|
|
935
935
|
};
|
|
936
936
|
}
|
|
937
937
|
|
|
938
|
+
// src/bin/differ.ts
|
|
939
|
+
function isRecord5(value) {
|
|
940
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
941
|
+
}
|
|
942
|
+
function isTokenLike3(obj) {
|
|
943
|
+
return isRecord5(obj) && "value" in obj;
|
|
944
|
+
}
|
|
945
|
+
function flattenTokens(tokens) {
|
|
946
|
+
const flat = /* @__PURE__ */ new Map();
|
|
947
|
+
function walk(node, path2 = []) {
|
|
948
|
+
if (!isRecord5(node)) return;
|
|
949
|
+
if (path2.length === 0 && Object.keys(node).some((k) => k.includes("/"))) {
|
|
950
|
+
Object.values(node).forEach((val) => walk(val, []));
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (isTokenLike3(node)) {
|
|
954
|
+
flat.set(path2.join("."), String(node.value));
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
Object.entries(node).forEach(([key, val]) => {
|
|
958
|
+
walk(val, [...path2, key]);
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
walk(tokens);
|
|
962
|
+
return flat;
|
|
963
|
+
}
|
|
964
|
+
function diffTokens(oldTokens, newTokens) {
|
|
965
|
+
const oldDetection = detectTokenFormat(oldTokens);
|
|
966
|
+
const newDetection = detectTokenFormat(newTokens);
|
|
967
|
+
let normalizedOld = oldTokens;
|
|
968
|
+
let normalizedNew = newTokens;
|
|
969
|
+
if (oldDetection.format !== "token-studio" && oldDetection.format !== "unknown") {
|
|
970
|
+
normalizedOld = normalizeTokenFormat(oldTokens, oldDetection.format);
|
|
971
|
+
}
|
|
972
|
+
if (newDetection.format !== "token-studio" && newDetection.format !== "unknown") {
|
|
973
|
+
normalizedNew = normalizeTokenFormat(newTokens, newDetection.format);
|
|
974
|
+
}
|
|
975
|
+
const oldFlat = flattenTokens(normalizedOld);
|
|
976
|
+
const newFlat = flattenTokens(normalizedNew);
|
|
977
|
+
const added = [];
|
|
978
|
+
const removed = [];
|
|
979
|
+
const modified = [];
|
|
980
|
+
let unchanged = 0;
|
|
981
|
+
newFlat.forEach((newValue, path2) => {
|
|
982
|
+
if (!oldFlat.has(path2)) {
|
|
983
|
+
added.push(path2);
|
|
984
|
+
} else if (oldFlat.get(path2) !== newValue) {
|
|
985
|
+
modified.push({ path: path2, oldValue: oldFlat.get(path2), newValue });
|
|
986
|
+
} else {
|
|
987
|
+
unchanged++;
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
oldFlat.forEach((_, path2) => {
|
|
991
|
+
if (!newFlat.has(path2)) {
|
|
992
|
+
removed.push(path2);
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
return { added, removed, modified, unchanged };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/bin/converter.ts
|
|
999
|
+
function isRecord6(value) {
|
|
1000
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1001
|
+
}
|
|
1002
|
+
function isTokenLike4(obj) {
|
|
1003
|
+
return isRecord6(obj) && "value" in obj;
|
|
1004
|
+
}
|
|
1005
|
+
function toW3C(tokens) {
|
|
1006
|
+
function walk(node) {
|
|
1007
|
+
if (!isRecord6(node)) return node;
|
|
1008
|
+
if (Object.keys(node).some((k) => k.includes("/"))) {
|
|
1009
|
+
const result2 = {};
|
|
1010
|
+
Object.entries(node).forEach(([key, val]) => {
|
|
1011
|
+
Object.assign(result2, walk(val));
|
|
1012
|
+
});
|
|
1013
|
+
return result2;
|
|
1014
|
+
}
|
|
1015
|
+
if (isTokenLike4(node)) {
|
|
1016
|
+
return {
|
|
1017
|
+
$value: node.value,
|
|
1018
|
+
...node.type ? { $type: node.type } : {}
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
const result = {};
|
|
1022
|
+
Object.entries(node).forEach(([key, val]) => {
|
|
1023
|
+
result[key] = walk(val);
|
|
1024
|
+
});
|
|
1025
|
+
return result;
|
|
1026
|
+
}
|
|
1027
|
+
return walk(tokens);
|
|
1028
|
+
}
|
|
1029
|
+
function toStyleDictionary(tokens) {
|
|
1030
|
+
function walk(node) {
|
|
1031
|
+
if (!isRecord6(node)) return node;
|
|
1032
|
+
if (Object.keys(node).some((k) => k.includes("/"))) {
|
|
1033
|
+
const result2 = {};
|
|
1034
|
+
Object.entries(node).forEach(([key, val]) => {
|
|
1035
|
+
Object.assign(result2, walk(val));
|
|
1036
|
+
});
|
|
1037
|
+
return result2;
|
|
1038
|
+
}
|
|
1039
|
+
if (isTokenLike4(node)) {
|
|
1040
|
+
return node;
|
|
1041
|
+
}
|
|
1042
|
+
const result = {};
|
|
1043
|
+
Object.entries(node).forEach(([key, val]) => {
|
|
1044
|
+
result[key] = walk(val);
|
|
1045
|
+
});
|
|
1046
|
+
return result;
|
|
1047
|
+
}
|
|
1048
|
+
return walk(tokens);
|
|
1049
|
+
}
|
|
1050
|
+
function toSupernova(tokens) {
|
|
1051
|
+
const result = [];
|
|
1052
|
+
let idCounter = 1;
|
|
1053
|
+
function walk(node, path2 = []) {
|
|
1054
|
+
if (!isRecord6(node)) return;
|
|
1055
|
+
if (path2.length === 0 && Object.keys(node).some((k) => k.includes("/"))) {
|
|
1056
|
+
Object.values(node).forEach((val) => walk(val, []));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (isTokenLike4(node)) {
|
|
1060
|
+
result.push({
|
|
1061
|
+
id: String(idCounter++),
|
|
1062
|
+
name: path2[path2.length - 1] || "token",
|
|
1063
|
+
tokenType: node.type || "unknown",
|
|
1064
|
+
value: node.value
|
|
1065
|
+
});
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
Object.entries(node).forEach(([key, val]) => {
|
|
1069
|
+
walk(val, [...path2, key]);
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
walk(tokens);
|
|
1073
|
+
return result;
|
|
1074
|
+
}
|
|
1075
|
+
function convertTokenFormat(tokens, targetFormat) {
|
|
1076
|
+
if (targetFormat === "token-studio") {
|
|
1077
|
+
return tokens;
|
|
1078
|
+
}
|
|
1079
|
+
switch (targetFormat) {
|
|
1080
|
+
case "w3c":
|
|
1081
|
+
return toW3C(tokens);
|
|
1082
|
+
case "style-dictionary":
|
|
1083
|
+
return toStyleDictionary(tokens);
|
|
1084
|
+
case "supernova":
|
|
1085
|
+
return toSupernova(tokens);
|
|
1086
|
+
default:
|
|
1087
|
+
throw new Error(`Unsupported target format: ${targetFormat}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
938
1091
|
// src/bin/watcher.ts
|
|
939
1092
|
import { watch } from "fs";
|
|
940
1093
|
function watchFile(filePath, onChange) {
|
|
@@ -967,6 +1120,8 @@ Usage:
|
|
|
967
1120
|
tokvista init [--force] [--port 3000] [--no-open] [--no-preview]
|
|
968
1121
|
tokvista export <tokens.json> --format <css|scss|json|tailwind> [--output <file>]
|
|
969
1122
|
tokvista validate <tokens.json>
|
|
1123
|
+
tokvista diff <old.json> <new.json>
|
|
1124
|
+
tokvista convert <tokens.json> --to <w3c|style-dictionary|supernova> [--output <file>]
|
|
970
1125
|
|
|
971
1126
|
Arguments:
|
|
972
1127
|
tokens.json Path to your tokens file (overrides config.tokens)
|
|
@@ -1096,6 +1251,12 @@ function parseArgs(args) {
|
|
|
1096
1251
|
if (args[0] === "validate") {
|
|
1097
1252
|
return parseValidateArgs(args.slice(1));
|
|
1098
1253
|
}
|
|
1254
|
+
if (args[0] === "diff") {
|
|
1255
|
+
return parseDiffArgs(args.slice(1));
|
|
1256
|
+
}
|
|
1257
|
+
if (args[0] === "convert") {
|
|
1258
|
+
return parseConvertArgs(args.slice(1));
|
|
1259
|
+
}
|
|
1099
1260
|
return parseServeArgs(args);
|
|
1100
1261
|
}
|
|
1101
1262
|
function parseValidateArgs(args) {
|
|
@@ -1117,6 +1278,81 @@ function parseValidateArgs(args) {
|
|
|
1117
1278
|
if (!tokenFileArg) throw new Error("Token file is required for validate");
|
|
1118
1279
|
return { command: "validate", tokenFileArg };
|
|
1119
1280
|
}
|
|
1281
|
+
function parseDiffArgs(args) {
|
|
1282
|
+
let oldFileArg;
|
|
1283
|
+
let newFileArg;
|
|
1284
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1285
|
+
const arg = args[index];
|
|
1286
|
+
if (arg === "-h" || arg === "--help") {
|
|
1287
|
+
printHelp();
|
|
1288
|
+
process.exit(0);
|
|
1289
|
+
}
|
|
1290
|
+
if (arg.startsWith("-")) {
|
|
1291
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1292
|
+
}
|
|
1293
|
+
if (!oldFileArg) {
|
|
1294
|
+
oldFileArg = arg;
|
|
1295
|
+
} else if (!newFileArg) {
|
|
1296
|
+
newFileArg = arg;
|
|
1297
|
+
} else {
|
|
1298
|
+
throw new Error(`Too many arguments. Expected: tokvista diff <old.json> <new.json>`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (!oldFileArg) throw new Error("Old token file is required");
|
|
1302
|
+
if (!newFileArg) throw new Error("New token file is required");
|
|
1303
|
+
return { command: "diff", oldFileArg, newFileArg };
|
|
1304
|
+
}
|
|
1305
|
+
function parseConvertArgs(args) {
|
|
1306
|
+
let tokenFileArg;
|
|
1307
|
+
let to;
|
|
1308
|
+
let output;
|
|
1309
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1310
|
+
const arg = args[index];
|
|
1311
|
+
if (arg === "-h" || arg === "--help") {
|
|
1312
|
+
printHelp();
|
|
1313
|
+
process.exit(0);
|
|
1314
|
+
}
|
|
1315
|
+
if (arg === "--to") {
|
|
1316
|
+
const next = args[index + 1];
|
|
1317
|
+
if (!next) throw new Error("Missing value for --to");
|
|
1318
|
+
if (!["w3c", "style-dictionary", "supernova", "token-studio"].includes(next)) {
|
|
1319
|
+
throw new Error("Format must be: w3c, style-dictionary, supernova, or token-studio");
|
|
1320
|
+
}
|
|
1321
|
+
to = next;
|
|
1322
|
+
index += 1;
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
if (arg === "--output" || arg === "-o") {
|
|
1326
|
+
const next = args[index + 1];
|
|
1327
|
+
if (!next) throw new Error("Missing value for --output");
|
|
1328
|
+
output = next;
|
|
1329
|
+
index += 1;
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
if (arg.startsWith("--to=")) {
|
|
1333
|
+
const val = arg.slice("--to=".length);
|
|
1334
|
+
if (!["w3c", "style-dictionary", "supernova", "token-studio"].includes(val)) {
|
|
1335
|
+
throw new Error("Format must be: w3c, style-dictionary, supernova, or token-studio");
|
|
1336
|
+
}
|
|
1337
|
+
to = val;
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (arg.startsWith("--output=")) {
|
|
1341
|
+
output = arg.slice("--output=".length);
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
if (arg.startsWith("-")) {
|
|
1345
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1346
|
+
}
|
|
1347
|
+
if (tokenFileArg) {
|
|
1348
|
+
throw new Error(`Only one token file is supported. Unexpected value: "${arg}"`);
|
|
1349
|
+
}
|
|
1350
|
+
tokenFileArg = arg;
|
|
1351
|
+
}
|
|
1352
|
+
if (!tokenFileArg) throw new Error("Token file is required for convert");
|
|
1353
|
+
if (!to) throw new Error("--to is required (w3c, style-dictionary, supernova, or token-studio)");
|
|
1354
|
+
return { command: "convert", tokenFileArg, to, output };
|
|
1355
|
+
}
|
|
1120
1356
|
function parseExportArgs(args) {
|
|
1121
1357
|
let tokenFileArg;
|
|
1122
1358
|
let format;
|
|
@@ -1825,6 +2061,70 @@ Validating ${resolvedTokenPath}...
|
|
|
1825
2061
|
process.exit(1);
|
|
1826
2062
|
}
|
|
1827
2063
|
}
|
|
2064
|
+
async function runDiffCommand(cwd, options) {
|
|
2065
|
+
const oldPath = path.resolve(cwd, options.oldFileArg);
|
|
2066
|
+
const newPath = path.resolve(cwd, options.newFileArg);
|
|
2067
|
+
if (!existsSync(oldPath)) {
|
|
2068
|
+
throw new Error(`Old token file not found: ${oldPath}`);
|
|
2069
|
+
}
|
|
2070
|
+
if (!existsSync(newPath)) {
|
|
2071
|
+
throw new Error(`New token file not found: ${newPath}`);
|
|
2072
|
+
}
|
|
2073
|
+
const [oldTokens, newTokens] = await Promise.all([
|
|
2074
|
+
readTokens(oldPath),
|
|
2075
|
+
readTokens(newPath)
|
|
2076
|
+
]);
|
|
2077
|
+
const diff = diffTokens(oldTokens, newTokens);
|
|
2078
|
+
console.log(`
|
|
2079
|
+
Comparing tokens:
|
|
2080
|
+
Old: ${oldPath}
|
|
2081
|
+
New: ${newPath}
|
|
2082
|
+
`);
|
|
2083
|
+
if (diff.added.length > 0) {
|
|
2084
|
+
console.log(`\u2705 Added (${diff.added.length}):`);
|
|
2085
|
+
diff.added.forEach((path2) => console.log(` + ${path2}`));
|
|
2086
|
+
console.log("");
|
|
2087
|
+
}
|
|
2088
|
+
if (diff.removed.length > 0) {
|
|
2089
|
+
console.log(`\u274C Removed (${diff.removed.length}):`);
|
|
2090
|
+
diff.removed.forEach((path2) => console.log(` - ${path2}`));
|
|
2091
|
+
console.log("");
|
|
2092
|
+
}
|
|
2093
|
+
if (diff.modified.length > 0) {
|
|
2094
|
+
console.log(`\u{1F504} Modified (${diff.modified.length}):`);
|
|
2095
|
+
diff.modified.forEach(({ path: path2, oldValue, newValue }) => {
|
|
2096
|
+
console.log(` ~ ${path2}`);
|
|
2097
|
+
console.log(` - ${oldValue}`);
|
|
2098
|
+
console.log(` + ${newValue}`);
|
|
2099
|
+
});
|
|
2100
|
+
console.log("");
|
|
2101
|
+
}
|
|
2102
|
+
console.log(`Unchanged: ${diff.unchanged}`);
|
|
2103
|
+
console.log(`Total changes: ${diff.added.length + diff.removed.length + diff.modified.length}
|
|
2104
|
+
`);
|
|
2105
|
+
}
|
|
2106
|
+
async function runConvertCommand(cwd, options) {
|
|
2107
|
+
const resolvedTokenPath = path.resolve(cwd, options.tokenFileArg);
|
|
2108
|
+
if (!existsSync(resolvedTokenPath)) {
|
|
2109
|
+
throw new Error(`Token file not found: ${resolvedTokenPath}`);
|
|
2110
|
+
}
|
|
2111
|
+
const tokens = await readTokens(resolvedTokenPath);
|
|
2112
|
+
const detection = detectTokenFormat(tokens);
|
|
2113
|
+
let normalizedTokens = tokens;
|
|
2114
|
+
if (detection.format !== "token-studio" && detection.format !== "unknown") {
|
|
2115
|
+
normalizedTokens = normalizeTokenFormat(tokens, detection.format);
|
|
2116
|
+
}
|
|
2117
|
+
const converted = convertTokenFormat(normalizedTokens, options.to);
|
|
2118
|
+
const output = JSON.stringify(converted, null, 2);
|
|
2119
|
+
if (options.output) {
|
|
2120
|
+
const outputPath = path.resolve(cwd, options.output);
|
|
2121
|
+
await writeFile(outputPath, output, "utf8");
|
|
2122
|
+
console.log(`Converted ${detection.format} \u2192 ${options.to}`);
|
|
2123
|
+
console.log(`Output: ${outputPath}`);
|
|
2124
|
+
} else {
|
|
2125
|
+
console.log(output);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
1828
2128
|
async function main() {
|
|
1829
2129
|
try {
|
|
1830
2130
|
const options = parseArgs(process.argv.slice(2));
|
|
@@ -1844,6 +2144,14 @@ async function main() {
|
|
|
1844
2144
|
await runValidateCommand(cwd, options);
|
|
1845
2145
|
return;
|
|
1846
2146
|
}
|
|
2147
|
+
if (options.command === "diff") {
|
|
2148
|
+
await runDiffCommand(cwd, options);
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
if (options.command === "convert") {
|
|
2152
|
+
await runConvertCommand(cwd, options);
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
1847
2155
|
await runServeCommand(cwd, options);
|
|
1848
2156
|
} catch (error) {
|
|
1849
2157
|
console.error(error.message);
|