tokvista 1.7.1 → 1.8.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
@@ -119,6 +119,18 @@ 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
+
122
134
  ### Interactive Setup
123
135
 
124
136
  ```bash
@@ -149,6 +161,7 @@ Then run `npx tokvista` to use your config.
149
161
  | `tokvista init` | Interactive config setup |
150
162
  | `tokvista export <file> --format <type>` | Export tokens (css, scss, json, tailwind) |
151
163
  | `tokvista validate <file>` | Validate token structure and values |
164
+ | `tokvista diff <old> <new>` | Compare two token files |
152
165
  | `--config`, `-c` | Config file path |
153
166
  | `--port`, `-p` | Server port (default: `3000`) |
154
167
  | `--format` | Export format (export only) |
@@ -935,6 +935,66 @@ 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
+
938
998
  // src/bin/watcher.ts
939
999
  import { watch } from "fs";
940
1000
  function watchFile(filePath, onChange) {
@@ -967,6 +1027,7 @@ Usage:
967
1027
  tokvista init [--force] [--port 3000] [--no-open] [--no-preview]
968
1028
  tokvista export <tokens.json> --format <css|scss|json|tailwind> [--output <file>]
969
1029
  tokvista validate <tokens.json>
1030
+ tokvista diff <old.json> <new.json>
970
1031
 
971
1032
  Arguments:
972
1033
  tokens.json Path to your tokens file (overrides config.tokens)
@@ -1096,6 +1157,9 @@ function parseArgs(args) {
1096
1157
  if (args[0] === "validate") {
1097
1158
  return parseValidateArgs(args.slice(1));
1098
1159
  }
1160
+ if (args[0] === "diff") {
1161
+ return parseDiffArgs(args.slice(1));
1162
+ }
1099
1163
  return parseServeArgs(args);
1100
1164
  }
1101
1165
  function parseValidateArgs(args) {
@@ -1117,6 +1181,30 @@ function parseValidateArgs(args) {
1117
1181
  if (!tokenFileArg) throw new Error("Token file is required for validate");
1118
1182
  return { command: "validate", tokenFileArg };
1119
1183
  }
1184
+ function parseDiffArgs(args) {
1185
+ let oldFileArg;
1186
+ let newFileArg;
1187
+ for (let index = 0; index < args.length; index += 1) {
1188
+ const arg = args[index];
1189
+ if (arg === "-h" || arg === "--help") {
1190
+ printHelp();
1191
+ process.exit(0);
1192
+ }
1193
+ if (arg.startsWith("-")) {
1194
+ throw new Error(`Unknown option: ${arg}`);
1195
+ }
1196
+ if (!oldFileArg) {
1197
+ oldFileArg = arg;
1198
+ } else if (!newFileArg) {
1199
+ newFileArg = arg;
1200
+ } else {
1201
+ throw new Error(`Too many arguments. Expected: tokvista diff <old.json> <new.json>`);
1202
+ }
1203
+ }
1204
+ if (!oldFileArg) throw new Error("Old token file is required");
1205
+ if (!newFileArg) throw new Error("New token file is required");
1206
+ return { command: "diff", oldFileArg, newFileArg };
1207
+ }
1120
1208
  function parseExportArgs(args) {
1121
1209
  let tokenFileArg;
1122
1210
  let format;
@@ -1825,6 +1913,48 @@ Validating ${resolvedTokenPath}...
1825
1913
  process.exit(1);
1826
1914
  }
1827
1915
  }
1916
+ async function runDiffCommand(cwd, options) {
1917
+ const oldPath = path.resolve(cwd, options.oldFileArg);
1918
+ const newPath = path.resolve(cwd, options.newFileArg);
1919
+ if (!existsSync(oldPath)) {
1920
+ throw new Error(`Old token file not found: ${oldPath}`);
1921
+ }
1922
+ if (!existsSync(newPath)) {
1923
+ throw new Error(`New token file not found: ${newPath}`);
1924
+ }
1925
+ const [oldTokens, newTokens] = await Promise.all([
1926
+ readTokens(oldPath),
1927
+ readTokens(newPath)
1928
+ ]);
1929
+ const diff = diffTokens(oldTokens, newTokens);
1930
+ console.log(`
1931
+ Comparing tokens:
1932
+ Old: ${oldPath}
1933
+ New: ${newPath}
1934
+ `);
1935
+ if (diff.added.length > 0) {
1936
+ console.log(`\u2705 Added (${diff.added.length}):`);
1937
+ diff.added.forEach((path2) => console.log(` + ${path2}`));
1938
+ console.log("");
1939
+ }
1940
+ if (diff.removed.length > 0) {
1941
+ console.log(`\u274C Removed (${diff.removed.length}):`);
1942
+ diff.removed.forEach((path2) => console.log(` - ${path2}`));
1943
+ console.log("");
1944
+ }
1945
+ if (diff.modified.length > 0) {
1946
+ console.log(`\u{1F504} Modified (${diff.modified.length}):`);
1947
+ diff.modified.forEach(({ path: path2, oldValue, newValue }) => {
1948
+ console.log(` ~ ${path2}`);
1949
+ console.log(` - ${oldValue}`);
1950
+ console.log(` + ${newValue}`);
1951
+ });
1952
+ console.log("");
1953
+ }
1954
+ console.log(`Unchanged: ${diff.unchanged}`);
1955
+ console.log(`Total changes: ${diff.added.length + diff.removed.length + diff.modified.length}
1956
+ `);
1957
+ }
1828
1958
  async function main() {
1829
1959
  try {
1830
1960
  const options = parseArgs(process.argv.slice(2));
@@ -1844,6 +1974,10 @@ async function main() {
1844
1974
  await runValidateCommand(cwd, options);
1845
1975
  return;
1846
1976
  }
1977
+ if (options.command === "diff") {
1978
+ await runDiffCommand(cwd, options);
1979
+ return;
1980
+ }
1847
1981
  await runServeCommand(cwd, options);
1848
1982
  } catch (error) {
1849
1983
  console.error(error.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokvista",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "Interactive visual documentation for design tokens.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",