tokvista 1.5.0 → 1.5.1

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 |
@@ -8,6 +8,70 @@ 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;
@@ -351,7 +420,8 @@ async function runInitCommand(cwd, options) {
351
420
  tokenFileArg: void 0,
352
421
  configPathArg: configPath,
353
422
  port: options.port,
354
- openBrowser: options.openBrowser
423
+ openBrowser: options.openBrowser,
424
+ watch: true
355
425
  };
356
426
  }
357
427
  function serializeForInlineScript(value) {
@@ -528,12 +598,13 @@ async function buildRuntimeConfig(config, configPath, cwd) {
528
598
  ...typeof config.showSearch === "boolean" ? { showSearch: config.showSearch } : {}
529
599
  };
530
600
  }
531
- function buildHtml(tokens, runtimeConfig, css, appBundle) {
601
+ function buildHtml(tokens, runtimeConfig, css, appBundle, enableHotReload) {
532
602
  const serializedTokens = serializeForInlineScript(tokens);
533
603
  const serializedConfig = serializeForInlineScript(runtimeConfig);
534
604
  const safeCss = escapeInlineTag(css, "style");
535
605
  const safeAppBundle = escapeInlineTag(appBundle, "script");
536
606
  const pageTitle = runtimeConfig.title || "TokVista";
607
+ 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
608
  return `<!doctype html>
538
609
  <html lang="en">
539
610
  <head>
@@ -547,6 +618,7 @@ function buildHtml(tokens, runtimeConfig, css, appBundle) {
547
618
  <script>window.__TOKVISTA_TOKENS__ = ${serializedTokens};</script>
548
619
  <script>window.__TOKVISTA_CONFIG__ = ${serializedConfig};</script>
549
620
  <script type="module">${safeAppBundle}</script>
621
+ ${hotReloadScript}
550
622
  </body>
551
623
  </html>`;
552
624
  }
@@ -555,7 +627,7 @@ function resolveDistAsset(relativePath) {
555
627
  const binDir = path.dirname(currentFilePath);
556
628
  return path.resolve(binDir, "..", relativePath);
557
629
  }
558
- function handleRequest(request, response, html) {
630
+ function handleRequest(request, response, getHtml) {
559
631
  const requestUrl = request.url ?? "/";
560
632
  const pathname = requestUrl.split("?")[0];
561
633
  if (pathname === "/" || pathname === "") {
@@ -563,7 +635,7 @@ function handleRequest(request, response, html) {
563
635
  "content-type": "text/html; charset=utf-8",
564
636
  "cache-control": "no-store"
565
637
  });
566
- response.end(html);
638
+ response.end(getHtml());
567
639
  return;
568
640
  }
569
641
  if (pathname === "/favicon.ico") {
@@ -577,11 +649,11 @@ function handleRequest(request, response, html) {
577
649
  function isPortInUse(error) {
578
650
  return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
579
651
  }
580
- async function startServer(html, preferredPort) {
652
+ async function startServer(getHtml, preferredPort) {
581
653
  for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt += 1) {
582
654
  const port = preferredPort + attempt;
583
655
  const server = createServer(
584
- (request, response) => handleRequest(request, response, html)
656
+ (request, response) => handleRequest(request, response, getHtml)
585
657
  );
586
658
  try {
587
659
  await new Promise((resolve, reject) => {
@@ -637,27 +709,48 @@ async function runServeCommand(cwd, options) {
637
709
  throw new Error(`Token file not found: ${resolvedTokenPath}`);
638
710
  }
639
711
  const runtimeConfig = await buildRuntimeConfig(config, configPath, cwd);
640
- const [tokens, css, appBundle] = await Promise.all([
641
- readTokens(resolvedTokenPath),
712
+ const [css, appBundle] = await Promise.all([
642
713
  readFile(resolveDistAsset("styles.css"), "utf8"),
643
714
  readFile(resolveDistAsset("cli/browser.js"), "utf8")
644
715
  ]);
645
- const html = buildHtml(tokens, runtimeConfig, css, appBundle);
646
- const { server, port } = await startServer(html, options.port);
716
+ let cachedTokens = await readTokens(resolvedTokenPath);
717
+ const getHtml = () => buildHtml(cachedTokens, runtimeConfig, css, appBundle, options.watch);
718
+ const { server, port } = await startServer(getHtml, options.port);
647
719
  const openSockets = /* @__PURE__ */ new Set();
648
720
  let isShuttingDown = false;
721
+ const hotReload = options.watch ? new HotReloadServer() : null;
649
722
  server.on("connection", (socket) => {
650
723
  openSockets.add(socket);
651
724
  socket.on("close", () => {
652
725
  openSockets.delete(socket);
653
726
  });
654
727
  });
728
+ if (hotReload) {
729
+ server.on("upgrade", (req, socket, head) => hotReload.handleUpgrade(req, socket, head));
730
+ }
655
731
  const url = `http://localhost:${port}`;
656
732
  console.log(`TokVista running at ${url}`);
657
733
  console.log(`Using tokens: ${resolvedTokenPath}`);
658
734
  if (configPath) {
659
735
  console.log(`Using config: ${configPath}`);
660
736
  }
737
+ if (options.watch) {
738
+ console.log("Watching for changes...");
739
+ const watcher = watchFile(resolvedTokenPath, async () => {
740
+ try {
741
+ cachedTokens = await readTokens(resolvedTokenPath);
742
+ hotReload == null ? void 0 : hotReload.reload();
743
+ console.log("Tokens reloaded");
744
+ } catch (error) {
745
+ console.error("Failed to reload tokens:", error.message);
746
+ }
747
+ });
748
+ const cleanup = () => {
749
+ watcher.close();
750
+ hotReload == null ? void 0 : hotReload.close();
751
+ };
752
+ process.on("exit", cleanup);
753
+ }
661
754
  if (options.openBrowser) {
662
755
  try {
663
756
  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.1",
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": {