tokvista 1.8.0 → 1.10.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
@@ -131,6 +131,38 @@ npx tokvista diff tokens-v1.json tokens-v2.json
131
131
  # - Migration tracking
132
132
  ```
133
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
+
150
+ ### Build All Formats
151
+
152
+ ```bash
153
+ # Build everything in one command
154
+ npx tokvista build tokens.json --output-dir ./dist
155
+
156
+ # Creates:
157
+ # - tokens.css
158
+ # - tokens.scss
159
+ # - tokens.js
160
+ # - tailwind.config.js
161
+
162
+ # Skip validation for faster builds
163
+ npx tokvista build tokens.json --output-dir ./dist --skip-validation
164
+ ```
165
+
134
166
  ### Interactive Setup
135
167
 
136
168
  ```bash
@@ -162,6 +194,8 @@ Then run `npx tokvista` to use your config.
162
194
  | `tokvista export <file> --format <type>` | Export tokens (css, scss, json, tailwind) |
163
195
  | `tokvista validate <file>` | Validate token structure and values |
164
196
  | `tokvista diff <old> <new>` | Compare two token files |
197
+ | `tokvista convert <file> --to <format>` | Convert between token formats |
198
+ | `tokvista build <file> --output-dir <dir>` | Build all formats (validate + export) |
165
199
  | `--config`, `-c` | Config file path |
166
200
  | `--port`, `-p` | Server port (default: `3000`) |
167
201
  | `--format` | Export format (export only) |
@@ -995,6 +995,99 @@ function diffTokens(oldTokens, newTokens) {
995
995
  return { added, removed, modified, unchanged };
996
996
  }
997
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
+
998
1091
  // src/bin/watcher.ts
999
1092
  import { watch } from "fs";
1000
1093
  function watchFile(filePath, onChange) {
@@ -1028,6 +1121,8 @@ Usage:
1028
1121
  tokvista export <tokens.json> --format <css|scss|json|tailwind> [--output <file>]
1029
1122
  tokvista validate <tokens.json>
1030
1123
  tokvista diff <old.json> <new.json>
1124
+ tokvista convert <tokens.json> --to <w3c|style-dictionary|supernova> [--output <file>]
1125
+ tokvista build <tokens.json> --output-dir <dir> [--skip-validation]
1031
1126
 
1032
1127
  Arguments:
1033
1128
  tokens.json Path to your tokens file (overrides config.tokens)
@@ -1160,6 +1255,12 @@ function parseArgs(args) {
1160
1255
  if (args[0] === "diff") {
1161
1256
  return parseDiffArgs(args.slice(1));
1162
1257
  }
1258
+ if (args[0] === "convert") {
1259
+ return parseConvertArgs(args.slice(1));
1260
+ }
1261
+ if (args[0] === "build") {
1262
+ return parseBuildArgs(args.slice(1));
1263
+ }
1163
1264
  return parseServeArgs(args);
1164
1265
  }
1165
1266
  function parseValidateArgs(args) {
@@ -1205,6 +1306,94 @@ function parseDiffArgs(args) {
1205
1306
  if (!newFileArg) throw new Error("New token file is required");
1206
1307
  return { command: "diff", oldFileArg, newFileArg };
1207
1308
  }
1309
+ function parseConvertArgs(args) {
1310
+ let tokenFileArg;
1311
+ let to;
1312
+ let output;
1313
+ for (let index = 0; index < args.length; index += 1) {
1314
+ const arg = args[index];
1315
+ if (arg === "-h" || arg === "--help") {
1316
+ printHelp();
1317
+ process.exit(0);
1318
+ }
1319
+ if (arg === "--to") {
1320
+ const next = args[index + 1];
1321
+ if (!next) throw new Error("Missing value for --to");
1322
+ if (!["w3c", "style-dictionary", "supernova", "token-studio"].includes(next)) {
1323
+ throw new Error("Format must be: w3c, style-dictionary, supernova, or token-studio");
1324
+ }
1325
+ to = next;
1326
+ index += 1;
1327
+ continue;
1328
+ }
1329
+ if (arg === "--output" || arg === "-o") {
1330
+ const next = args[index + 1];
1331
+ if (!next) throw new Error("Missing value for --output");
1332
+ output = next;
1333
+ index += 1;
1334
+ continue;
1335
+ }
1336
+ if (arg.startsWith("--to=")) {
1337
+ const val = arg.slice("--to=".length);
1338
+ if (!["w3c", "style-dictionary", "supernova", "token-studio"].includes(val)) {
1339
+ throw new Error("Format must be: w3c, style-dictionary, supernova, or token-studio");
1340
+ }
1341
+ to = val;
1342
+ continue;
1343
+ }
1344
+ if (arg.startsWith("--output=")) {
1345
+ output = arg.slice("--output=".length);
1346
+ continue;
1347
+ }
1348
+ if (arg.startsWith("-")) {
1349
+ throw new Error(`Unknown option: ${arg}`);
1350
+ }
1351
+ if (tokenFileArg) {
1352
+ throw new Error(`Only one token file is supported. Unexpected value: "${arg}"`);
1353
+ }
1354
+ tokenFileArg = arg;
1355
+ }
1356
+ if (!tokenFileArg) throw new Error("Token file is required for convert");
1357
+ if (!to) throw new Error("--to is required (w3c, style-dictionary, supernova, or token-studio)");
1358
+ return { command: "convert", tokenFileArg, to, output };
1359
+ }
1360
+ function parseBuildArgs(args) {
1361
+ let tokenFileArg;
1362
+ let outputDir;
1363
+ let skipValidation = false;
1364
+ for (let index = 0; index < args.length; index += 1) {
1365
+ const arg = args[index];
1366
+ if (arg === "-h" || arg === "--help") {
1367
+ printHelp();
1368
+ process.exit(0);
1369
+ }
1370
+ if (arg === "--output-dir" || arg === "-o") {
1371
+ const next = args[index + 1];
1372
+ if (!next) throw new Error("Missing value for --output-dir");
1373
+ outputDir = next;
1374
+ index += 1;
1375
+ continue;
1376
+ }
1377
+ if (arg.startsWith("--output-dir=")) {
1378
+ outputDir = arg.slice("--output-dir=".length);
1379
+ continue;
1380
+ }
1381
+ if (arg === "--skip-validation") {
1382
+ skipValidation = true;
1383
+ continue;
1384
+ }
1385
+ if (arg.startsWith("-")) {
1386
+ throw new Error(`Unknown option: ${arg}`);
1387
+ }
1388
+ if (tokenFileArg) {
1389
+ throw new Error(`Only one token file is supported. Unexpected value: "${arg}"`);
1390
+ }
1391
+ tokenFileArg = arg;
1392
+ }
1393
+ if (!tokenFileArg) throw new Error("Token file is required for build");
1394
+ if (!outputDir) throw new Error("--output-dir is required");
1395
+ return { command: "build", tokenFileArg, outputDir, skipValidation };
1396
+ }
1208
1397
  function parseExportArgs(args) {
1209
1398
  let tokenFileArg;
1210
1399
  let format;
@@ -1955,6 +2144,71 @@ Comparing tokens:
1955
2144
  console.log(`Total changes: ${diff.added.length + diff.removed.length + diff.modified.length}
1956
2145
  `);
1957
2146
  }
2147
+ async function runConvertCommand(cwd, options) {
2148
+ const resolvedTokenPath = path.resolve(cwd, options.tokenFileArg);
2149
+ if (!existsSync(resolvedTokenPath)) {
2150
+ throw new Error(`Token file not found: ${resolvedTokenPath}`);
2151
+ }
2152
+ const tokens = await readTokens(resolvedTokenPath);
2153
+ const detection = detectTokenFormat(tokens);
2154
+ let normalizedTokens = tokens;
2155
+ if (detection.format !== "token-studio" && detection.format !== "unknown") {
2156
+ normalizedTokens = normalizeTokenFormat(tokens, detection.format);
2157
+ }
2158
+ const converted = convertTokenFormat(normalizedTokens, options.to);
2159
+ const output = JSON.stringify(converted, null, 2);
2160
+ if (options.output) {
2161
+ const outputPath = path.resolve(cwd, options.output);
2162
+ await writeFile(outputPath, output, "utf8");
2163
+ console.log(`Converted ${detection.format} \u2192 ${options.to}`);
2164
+ console.log(`Output: ${outputPath}`);
2165
+ } else {
2166
+ console.log(output);
2167
+ }
2168
+ }
2169
+ async function runBuildCommand(cwd, options) {
2170
+ const resolvedTokenPath = path.resolve(cwd, options.tokenFileArg);
2171
+ const outputDir = path.resolve(cwd, options.outputDir);
2172
+ if (!existsSync(resolvedTokenPath)) {
2173
+ throw new Error(`Token file not found: ${resolvedTokenPath}`);
2174
+ }
2175
+ if (!existsSync(outputDir)) {
2176
+ await import("fs/promises").then((fs) => fs.mkdir(outputDir, { recursive: true }));
2177
+ }
2178
+ const tokens = await readTokens(resolvedTokenPath);
2179
+ if (!options.skipValidation) {
2180
+ console.log("\nValidating tokens...");
2181
+ const result = validateTokens(tokens);
2182
+ if (!result.valid) {
2183
+ console.log(`\u274C Found ${result.errors.length} errors`);
2184
+ result.errors.slice(0, 5).forEach((err) => {
2185
+ console.log(` ${err.path}: ${err.message}`);
2186
+ });
2187
+ if (result.errors.length > 5) {
2188
+ console.log(` ... and ${result.errors.length - 5} more errors`);
2189
+ }
2190
+ throw new Error("Validation failed. Fix errors or use --skip-validation");
2191
+ }
2192
+ console.log("\u2705 Validation passed\n");
2193
+ }
2194
+ console.log("Building tokens...");
2195
+ const formats = [
2196
+ { name: "CSS", ext: "css", generator: generateCSS },
2197
+ { name: "SCSS", ext: "scss", generator: generateSCSS },
2198
+ { name: "JavaScript", ext: "js", generator: generateJS },
2199
+ { name: "Tailwind", ext: "tailwind.config.js", generator: generateTailwind }
2200
+ ];
2201
+ for (const format of formats) {
2202
+ const content = format.generator(tokens);
2203
+ const filename = format.ext.includes(".") ? format.ext : `tokens.${format.ext}`;
2204
+ const outputPath = path.join(outputDir, filename);
2205
+ await writeFile(outputPath, content, "utf8");
2206
+ console.log(` \u2713 ${format.name} \u2192 ${filename}`);
2207
+ }
2208
+ console.log(`
2209
+ \u2705 Build complete: ${outputDir}
2210
+ `);
2211
+ }
1958
2212
  async function main() {
1959
2213
  try {
1960
2214
  const options = parseArgs(process.argv.slice(2));
@@ -1978,6 +2232,14 @@ async function main() {
1978
2232
  await runDiffCommand(cwd, options);
1979
2233
  return;
1980
2234
  }
2235
+ if (options.command === "convert") {
2236
+ await runConvertCommand(cwd, options);
2237
+ return;
2238
+ }
2239
+ if (options.command === "build") {
2240
+ await runBuildCommand(cwd, options);
2241
+ return;
2242
+ }
1981
2243
  await runServeCommand(cwd, options);
1982
2244
  } catch (error) {
1983
2245
  console.error(error.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokvista",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Interactive visual documentation for design tokens.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",