tokvista 1.7.0 → 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) |
@@ -821,17 +821,35 @@ function isTokenLike2(obj) {
821
821
  return isRecord4(obj) && "value" in obj;
822
822
  }
823
823
  function isValidColor(value) {
824
- return /^#([0-9A-F]{3}|[0-9A-F]{6}|[0-9A-F]{8})$/i.test(value) || /^rgb\(/.test(value) || /^rgba\(/.test(value) || /^hsl\(/.test(value) || /^hsla\(/.test(value);
824
+ return /^#([0-9A-F]{3}|[0-9A-F]{6}|[0-9A-F]{8})$/i.test(value) || /^rgb\(/.test(value) || /^rgba\(/.test(value) || /^hsl\(/.test(value) || /^hsla\(/.test(value) || value === "transparent" || value === "currentColor";
825
825
  }
826
826
  function isValidDimension(value) {
827
- return /^\d+(\.\d+)?(px|rem|em|%|vh|vw)$/.test(value);
827
+ return /^\d+(\.\d+)?(px|rem|em|%|vh|vw)$/.test(value) || value === "0";
828
+ }
829
+ function buildTokenMap(tokens) {
830
+ const tokenPaths = /* @__PURE__ */ new Set();
831
+ function walk(node, path2 = []) {
832
+ if (!isRecord4(node)) return;
833
+ if (path2.length === 0 && Object.keys(node).some((k) => k.includes("/"))) {
834
+ Object.values(node).forEach((val) => walk(val, []));
835
+ return;
836
+ }
837
+ if (isTokenLike2(node)) {
838
+ tokenPaths.add(path2.join("."));
839
+ return;
840
+ }
841
+ Object.entries(node).forEach(([key, val]) => {
842
+ walk(val, [...path2, key]);
843
+ });
844
+ }
845
+ walk(tokens);
846
+ return tokenPaths;
828
847
  }
829
848
  function validateTokens(tokens) {
830
849
  const errors = [];
831
850
  const warnings = [];
832
851
  let totalTokens = 0;
833
- const allAliases = /* @__PURE__ */ new Set();
834
- const definedTokens = /* @__PURE__ */ new Set();
852
+ const allAliases = [];
835
853
  if (!isRecord4(tokens)) {
836
854
  return {
837
855
  valid: false,
@@ -840,12 +858,20 @@ function validateTokens(tokens) {
840
858
  totalTokens: 0
841
859
  };
842
860
  }
861
+ const detection = detectTokenFormat(tokens);
862
+ let normalizedTokens = tokens;
863
+ if (detection.format !== "token-studio" && detection.format !== "unknown") {
864
+ normalizedTokens = normalizeTokenFormat(tokens, detection.format);
865
+ }
843
866
  function walk(node, path2 = []) {
844
867
  if (!isRecord4(node)) return;
868
+ if (path2.length === 0 && Object.keys(node).some((k) => k.includes("/"))) {
869
+ Object.values(node).forEach((val) => walk(val, []));
870
+ return;
871
+ }
845
872
  if (isTokenLike2(node)) {
846
873
  totalTokens++;
847
874
  const tokenPath = path2.join(".");
848
- definedTokens.add(tokenPath);
849
875
  if (!node.type) {
850
876
  warnings.push({
851
877
  type: "warning",
@@ -863,7 +889,7 @@ function validateTokens(tokens) {
863
889
  }
864
890
  const value = String(node.value);
865
891
  if (value.match(/^\{(.+)\}$/)) {
866
- allAliases.add(tokenPath + "\u2192" + value);
892
+ allAliases.push({ tokenPath, reference: value });
867
893
  }
868
894
  if (node.type === "color" && !value.startsWith("{")) {
869
895
  if (!isValidColor(value)) {
@@ -889,10 +915,10 @@ function validateTokens(tokens) {
889
915
  walk(val, [...path2, key]);
890
916
  });
891
917
  }
892
- walk(tokens);
893
- allAliases.forEach((alias) => {
894
- const [tokenPath, reference] = alias.split("\u2192");
895
- const refPath = reference.slice(1, -1).replace(/\./g, ".");
918
+ walk(normalizedTokens);
919
+ const definedTokens = buildTokenMap(normalizedTokens);
920
+ allAliases.forEach(({ tokenPath, reference }) => {
921
+ const refPath = reference.slice(1, -1);
896
922
  if (!definedTokens.has(refPath)) {
897
923
  errors.push({
898
924
  type: "error",
@@ -909,6 +935,66 @@ function validateTokens(tokens) {
909
935
  };
910
936
  }
911
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
+
912
998
  // src/bin/watcher.ts
913
999
  import { watch } from "fs";
914
1000
  function watchFile(filePath, onChange) {
@@ -941,6 +1027,7 @@ Usage:
941
1027
  tokvista init [--force] [--port 3000] [--no-open] [--no-preview]
942
1028
  tokvista export <tokens.json> --format <css|scss|json|tailwind> [--output <file>]
943
1029
  tokvista validate <tokens.json>
1030
+ tokvista diff <old.json> <new.json>
944
1031
 
945
1032
  Arguments:
946
1033
  tokens.json Path to your tokens file (overrides config.tokens)
@@ -1070,6 +1157,9 @@ function parseArgs(args) {
1070
1157
  if (args[0] === "validate") {
1071
1158
  return parseValidateArgs(args.slice(1));
1072
1159
  }
1160
+ if (args[0] === "diff") {
1161
+ return parseDiffArgs(args.slice(1));
1162
+ }
1073
1163
  return parseServeArgs(args);
1074
1164
  }
1075
1165
  function parseValidateArgs(args) {
@@ -1091,6 +1181,30 @@ function parseValidateArgs(args) {
1091
1181
  if (!tokenFileArg) throw new Error("Token file is required for validate");
1092
1182
  return { command: "validate", tokenFileArg };
1093
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
+ }
1094
1208
  function parseExportArgs(args) {
1095
1209
  let tokenFileArg;
1096
1210
  let format;
@@ -1799,6 +1913,48 @@ Validating ${resolvedTokenPath}...
1799
1913
  process.exit(1);
1800
1914
  }
1801
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
+ }
1802
1958
  async function main() {
1803
1959
  try {
1804
1960
  const options = parseArgs(process.argv.slice(2));
@@ -1818,6 +1974,10 @@ async function main() {
1818
1974
  await runValidateCommand(cwd, options);
1819
1975
  return;
1820
1976
  }
1977
+ if (options.command === "diff") {
1978
+ await runDiffCommand(cwd, options);
1979
+ return;
1980
+ }
1821
1981
  await runServeCommand(cwd, options);
1822
1982
  } catch (error) {
1823
1983
  console.error(error.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokvista",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Interactive visual documentation for design tokens.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",