tokvista 1.5.0 → 1.5.2

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
@@ -24,6 +24,7 @@ Zero configuration. Multiple formats. One command.
24
24
  - 🔍 **Instant search** - `Cmd+K` / `Ctrl+K` to find any token
25
25
  - 🎯 **Zero config** - Works out of the box with any token format
26
26
  - ⚡ **Two modes** - CLI for quick preview or React component for apps
27
+ - 🔥 **Live reload** - Auto-refresh on file changes
27
28
 
28
29
  ---
29
30
 
@@ -120,6 +121,7 @@ Then run `npx tokvista` to use your config.
120
121
  | `--config`, `-c` | Config file path |
121
122
  | `--port`, `-p` | Server port (default: `3000`) |
122
123
  | `--no-open` | Don't open browser |
124
+ | `--no-watch` | Disable live reload |
123
125
  | `--no-preview` | Skip preview after init |
124
126
  | `--force`, `-f` | Overwrite existing config |
125
127
  | `--help`, `-h` | Show help |
@@ -2,12 +2,76 @@
2
2
 
3
3
  // src/bin/tokvista.ts
4
4
  import { spawn } from "child_process";
5
- import { existsSync } from "fs";
5
+ import { existsSync, readdirSync } from "fs";
6
6
  import { readFile, writeFile } from "fs/promises";
7
7
  import { createServer } from "http";
8
8
  import path from "path";
9
9
  import { createInterface } from "readline";
10
10
  import { fileURLToPath, pathToFileURL } from "url";
11
+
12
+ // src/bin/hotreload.ts
13
+ import { createHash } from "crypto";
14
+ var HotReloadServer = class {
15
+ constructor() {
16
+ this.clients = /* @__PURE__ */ new Set();
17
+ }
18
+ handleUpgrade(request, socket, head) {
19
+ if (request.url !== "/__tokvista_ws") {
20
+ socket.destroy();
21
+ return;
22
+ }
23
+ const key = request.headers["sec-websocket-key"];
24
+ if (!key) {
25
+ socket.destroy();
26
+ return;
27
+ }
28
+ const accept = createHash("sha1").update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
29
+ socket.write(
30
+ `HTTP/1.1 101 Switching Protocols\r
31
+ Upgrade: websocket\r
32
+ Connection: Upgrade\r
33
+ Sec-WebSocket-Accept: ${accept}\r
34
+ \r
35
+ `
36
+ );
37
+ const client = {
38
+ socket,
39
+ send: (data) => {
40
+ const buffer = Buffer.from(data);
41
+ const frame = Buffer.allocUnsafe(2 + buffer.length);
42
+ frame[0] = 129;
43
+ frame[1] = buffer.length;
44
+ buffer.copy(frame, 2);
45
+ socket.write(frame);
46
+ }
47
+ };
48
+ this.clients.add(client);
49
+ socket.on("close", () => this.clients.delete(client));
50
+ }
51
+ reload() {
52
+ for (const client of this.clients) {
53
+ client.send("reload");
54
+ }
55
+ }
56
+ close() {
57
+ for (const client of this.clients) {
58
+ client.socket.destroy();
59
+ }
60
+ this.clients.clear();
61
+ }
62
+ };
63
+
64
+ // src/bin/watcher.ts
65
+ import { watch } from "fs";
66
+ function watchFile(filePath, onChange) {
67
+ let debounce = null;
68
+ return watch(filePath, () => {
69
+ if (debounce) clearTimeout(debounce);
70
+ debounce = setTimeout(onChange, 100);
71
+ });
72
+ }
73
+
74
+ // src/bin/tokvista.ts
11
75
  var DEFAULT_PORT = 3e3;
12
76
  var MAX_PORT_ATTEMPTS = 25;
13
77
  var SHUTDOWN_TIMEOUT_MS = 1500;
@@ -52,6 +116,7 @@ function parseServeArgs(args) {
52
116
  let configPathArg;
53
117
  let port = DEFAULT_PORT;
54
118
  let openBrowser2 = true;
119
+ let watch2 = true;
55
120
  for (let index = 0; index < args.length; index += 1) {
56
121
  const arg = args[index];
57
122
  if (arg === "-h" || arg === "--help") {
@@ -62,6 +127,10 @@ function parseServeArgs(args) {
62
127
  openBrowser2 = false;
63
128
  continue;
64
129
  }
130
+ if (arg === "--no-watch") {
131
+ watch2 = false;
132
+ continue;
133
+ }
65
134
  if (arg === "--port" || arg === "-p") {
66
135
  const next = args[index + 1];
67
136
  if (!next) {
@@ -96,7 +165,7 @@ function parseServeArgs(args) {
96
165
  }
97
166
  tokenFileArg = arg;
98
167
  }
99
- return { command: "serve", tokenFileArg, configPathArg, port, openBrowser: openBrowser2 };
168
+ return { command: "serve", tokenFileArg, configPathArg, port, openBrowser: openBrowser2, watch: watch2 };
100
169
  }
101
170
  function parseInitArgs(args) {
102
171
  let force = false;
@@ -176,6 +245,17 @@ function askRawQuestion(rl, prompt) {
176
245
  rl.question(prompt, (answer) => resolve(answer.trim()));
177
246
  });
178
247
  }
248
+ function createCompleter(cwd) {
249
+ return (line) => {
250
+ try {
251
+ const files = readdirSync(cwd);
252
+ const hits = files.filter((f) => f.startsWith(line));
253
+ return [hits.length ? hits : files, line];
254
+ } catch {
255
+ return [[], line];
256
+ }
257
+ };
258
+ }
179
259
  async function askWithDefault(rl, label, defaultValue) {
180
260
  const suffix = defaultValue != null && defaultValue !== "" ? ` [${defaultValue}]` : "";
181
261
  const answer = await askRawQuestion(rl, `${label}${suffix}: `);
@@ -257,7 +337,12 @@ async function buildInitConfigFromPrompt(cwd, askPreviewQuestion) {
257
337
  }
258
338
  console.log("TokVista init");
259
339
  console.log("Press Enter to accept defaults.\n");
260
- const rl = createInterface({ input: process.stdin, output: process.stdout });
340
+ const rl = createInterface({
341
+ input: process.stdin,
342
+ output: process.stdout,
343
+ completer: createCompleter(cwd),
344
+ tabSize: 2
345
+ });
261
346
  try {
262
347
  const title = await askWithDefault(rl, "Title", defaults.title);
263
348
  const subtitle = await askWithDefault(rl, "Subtitle", defaults.subtitle);
@@ -351,7 +436,8 @@ async function runInitCommand(cwd, options) {
351
436
  tokenFileArg: void 0,
352
437
  configPathArg: configPath,
353
438
  port: options.port,
354
- openBrowser: options.openBrowser
439
+ openBrowser: options.openBrowser,
440
+ watch: true
355
441
  };
356
442
  }
357
443
  function serializeForInlineScript(value) {
@@ -528,12 +614,13 @@ async function buildRuntimeConfig(config, configPath, cwd) {
528
614
  ...typeof config.showSearch === "boolean" ? { showSearch: config.showSearch } : {}
529
615
  };
530
616
  }
531
- function buildHtml(tokens, runtimeConfig, css, appBundle) {
617
+ function buildHtml(tokens, runtimeConfig, css, appBundle, enableHotReload) {
532
618
  const serializedTokens = serializeForInlineScript(tokens);
533
619
  const serializedConfig = serializeForInlineScript(runtimeConfig);
534
620
  const safeCss = escapeInlineTag(css, "style");
535
621
  const safeAppBundle = escapeInlineTag(appBundle, "script");
536
622
  const pageTitle = runtimeConfig.title || "TokVista";
623
+ const hotReloadScript = enableHotReload ? `<script>(function(){const ws=new WebSocket('ws://'+location.host+'/__tokvista_ws');ws.onmessage=()=>location.reload();ws.onerror=()=>setTimeout(()=>location.reload(),1000);})();</script>` : "";
537
624
  return `<!doctype html>
538
625
  <html lang="en">
539
626
  <head>
@@ -547,6 +634,7 @@ function buildHtml(tokens, runtimeConfig, css, appBundle) {
547
634
  <script>window.__TOKVISTA_TOKENS__ = ${serializedTokens};</script>
548
635
  <script>window.__TOKVISTA_CONFIG__ = ${serializedConfig};</script>
549
636
  <script type="module">${safeAppBundle}</script>
637
+ ${hotReloadScript}
550
638
  </body>
551
639
  </html>`;
552
640
  }
@@ -555,7 +643,7 @@ function resolveDistAsset(relativePath) {
555
643
  const binDir = path.dirname(currentFilePath);
556
644
  return path.resolve(binDir, "..", relativePath);
557
645
  }
558
- function handleRequest(request, response, html) {
646
+ function handleRequest(request, response, getHtml) {
559
647
  const requestUrl = request.url ?? "/";
560
648
  const pathname = requestUrl.split("?")[0];
561
649
  if (pathname === "/" || pathname === "") {
@@ -563,7 +651,7 @@ function handleRequest(request, response, html) {
563
651
  "content-type": "text/html; charset=utf-8",
564
652
  "cache-control": "no-store"
565
653
  });
566
- response.end(html);
654
+ response.end(getHtml());
567
655
  return;
568
656
  }
569
657
  if (pathname === "/favicon.ico") {
@@ -577,11 +665,11 @@ function handleRequest(request, response, html) {
577
665
  function isPortInUse(error) {
578
666
  return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
579
667
  }
580
- async function startServer(html, preferredPort) {
668
+ async function startServer(getHtml, preferredPort) {
581
669
  for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt += 1) {
582
670
  const port = preferredPort + attempt;
583
671
  const server = createServer(
584
- (request, response) => handleRequest(request, response, html)
672
+ (request, response) => handleRequest(request, response, getHtml)
585
673
  );
586
674
  try {
587
675
  await new Promise((resolve, reject) => {
@@ -637,27 +725,48 @@ async function runServeCommand(cwd, options) {
637
725
  throw new Error(`Token file not found: ${resolvedTokenPath}`);
638
726
  }
639
727
  const runtimeConfig = await buildRuntimeConfig(config, configPath, cwd);
640
- const [tokens, css, appBundle] = await Promise.all([
641
- readTokens(resolvedTokenPath),
728
+ const [css, appBundle] = await Promise.all([
642
729
  readFile(resolveDistAsset("styles.css"), "utf8"),
643
730
  readFile(resolveDistAsset("cli/browser.js"), "utf8")
644
731
  ]);
645
- const html = buildHtml(tokens, runtimeConfig, css, appBundle);
646
- const { server, port } = await startServer(html, options.port);
732
+ let cachedTokens = await readTokens(resolvedTokenPath);
733
+ const getHtml = () => buildHtml(cachedTokens, runtimeConfig, css, appBundle, options.watch);
734
+ const { server, port } = await startServer(getHtml, options.port);
647
735
  const openSockets = /* @__PURE__ */ new Set();
648
736
  let isShuttingDown = false;
737
+ const hotReload = options.watch ? new HotReloadServer() : null;
649
738
  server.on("connection", (socket) => {
650
739
  openSockets.add(socket);
651
740
  socket.on("close", () => {
652
741
  openSockets.delete(socket);
653
742
  });
654
743
  });
744
+ if (hotReload) {
745
+ server.on("upgrade", (req, socket, head) => hotReload.handleUpgrade(req, socket, head));
746
+ }
655
747
  const url = `http://localhost:${port}`;
656
748
  console.log(`TokVista running at ${url}`);
657
749
  console.log(`Using tokens: ${resolvedTokenPath}`);
658
750
  if (configPath) {
659
751
  console.log(`Using config: ${configPath}`);
660
752
  }
753
+ if (options.watch) {
754
+ console.log("Watching for changes...");
755
+ const watcher = watchFile(resolvedTokenPath, async () => {
756
+ try {
757
+ cachedTokens = await readTokens(resolvedTokenPath);
758
+ hotReload == null ? void 0 : hotReload.reload();
759
+ console.log("Tokens reloaded");
760
+ } catch (error) {
761
+ console.error("Failed to reload tokens:", error.message);
762
+ }
763
+ });
764
+ const cleanup = () => {
765
+ watcher.close();
766
+ hotReload == null ? void 0 : hotReload.close();
767
+ };
768
+ process.on("exit", cleanup);
769
+ }
661
770
  if (options.openBrowser) {
662
771
  try {
663
772
  await openBrowser(url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokvista",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Interactive visual documentation for design tokens.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -80,7 +80,7 @@
80
80
  "url": "https://www.linkedin.com/in/nibin-kurian"
81
81
  },
82
82
  "engines": {
83
- "node": ">=16.0.0"
83
+ "node": ">=20.0.0"
84
84
  },
85
85
  "license": "MIT",
86
86
  "peerDependencies": {