tokvista 1.1.0 → 1.3.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 +12 -0
- package/dist/bin/tokvista.js +213 -0
- package/dist/cli/browser.js +216 -0
- package/dist/index.cjs +122 -40
- package/dist/index.js +122 -40
- package/dist/styles.css +1100 -134
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -38,6 +38,18 @@ Design token documentation is often static and hard to scan. **Tokvista** gives
|
|
|
38
38
|
npm install tokvista
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
### Run as CLI (No React Setup)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx tokvista tokens.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Optional flags:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx tokvista ./tokens.json --port 4000 --no-open
|
|
51
|
+
```
|
|
52
|
+
|
|
41
53
|
### Use
|
|
42
54
|
|
|
43
55
|
```tsx
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin/tokvista.ts
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import { createServer } from "http";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var DEFAULT_PORT = 3e3;
|
|
11
|
+
var MAX_PORT_ATTEMPTS = 25;
|
|
12
|
+
function printHelp() {
|
|
13
|
+
console.log(`TokVista CLI
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
tokvista [tokens.json] [--port 3000] [--no-open]
|
|
17
|
+
|
|
18
|
+
Arguments:
|
|
19
|
+
tokens.json Path to your tokens file (default: ./tokens.json)
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-p, --port Preferred server port (default: 3000)
|
|
23
|
+
--no-open Do not automatically open the browser
|
|
24
|
+
-h, --help Show this help message
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
function parsePort(value) {
|
|
28
|
+
const parsed = Number(value);
|
|
29
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
30
|
+
throw new Error(`Invalid port: "${value}". Use a number between 1 and 65535.`);
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
function parseArgs(args) {
|
|
35
|
+
let tokenFile = "tokens.json";
|
|
36
|
+
let tokenFileSet = false;
|
|
37
|
+
let port = DEFAULT_PORT;
|
|
38
|
+
let openBrowser2 = true;
|
|
39
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
40
|
+
const arg = args[index];
|
|
41
|
+
if (arg === "-h" || arg === "--help") {
|
|
42
|
+
printHelp();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
if (arg === "--no-open") {
|
|
46
|
+
openBrowser2 = false;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg === "--port" || arg === "-p") {
|
|
50
|
+
const next = args[index + 1];
|
|
51
|
+
if (!next) {
|
|
52
|
+
throw new Error(`Missing value for ${arg}.`);
|
|
53
|
+
}
|
|
54
|
+
port = parsePort(next);
|
|
55
|
+
index += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg.startsWith("--port=")) {
|
|
59
|
+
port = parsePort(arg.slice("--port=".length));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg.startsWith("-")) {
|
|
63
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
64
|
+
}
|
|
65
|
+
if (tokenFileSet) {
|
|
66
|
+
throw new Error(`Only one token file is supported. Unexpected value: "${arg}"`);
|
|
67
|
+
}
|
|
68
|
+
tokenFile = arg;
|
|
69
|
+
tokenFileSet = true;
|
|
70
|
+
}
|
|
71
|
+
return { tokenFile, port, openBrowser: openBrowser2 };
|
|
72
|
+
}
|
|
73
|
+
function serializeForInlineScript(value) {
|
|
74
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
75
|
+
}
|
|
76
|
+
function escapeInlineTag(value, tagName) {
|
|
77
|
+
return value.replace(new RegExp(`</${tagName}`, "gi"), `<\\/${tagName}`);
|
|
78
|
+
}
|
|
79
|
+
function buildHtml(tokens, css, appBundle) {
|
|
80
|
+
const serializedTokens = serializeForInlineScript(tokens);
|
|
81
|
+
const safeCss = escapeInlineTag(css, "style");
|
|
82
|
+
const safeAppBundle = escapeInlineTag(appBundle, "script");
|
|
83
|
+
return `<!doctype html>
|
|
84
|
+
<html lang="en">
|
|
85
|
+
<head>
|
|
86
|
+
<meta charset="utf-8" />
|
|
87
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
88
|
+
<title>TokVista</title>
|
|
89
|
+
<style>${safeCss}</style>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<div id="tokvista-root"></div>
|
|
93
|
+
<script>window.__TOKVISTA_TOKENS__ = ${serializedTokens};</script>
|
|
94
|
+
<script type="module">${safeAppBundle}</script>
|
|
95
|
+
</body>
|
|
96
|
+
</html>`;
|
|
97
|
+
}
|
|
98
|
+
function resolveDistAsset(relativePath) {
|
|
99
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
100
|
+
const binDir = path.dirname(currentFilePath);
|
|
101
|
+
return path.resolve(binDir, "..", relativePath);
|
|
102
|
+
}
|
|
103
|
+
function handleRequest(request, response, html) {
|
|
104
|
+
const requestUrl = request.url ?? "/";
|
|
105
|
+
const pathname = requestUrl.split("?")[0];
|
|
106
|
+
if (pathname === "/" || pathname === "") {
|
|
107
|
+
response.writeHead(200, {
|
|
108
|
+
"content-type": "text/html; charset=utf-8",
|
|
109
|
+
"cache-control": "no-store"
|
|
110
|
+
});
|
|
111
|
+
response.end(html);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (pathname === "/favicon.ico") {
|
|
115
|
+
response.writeHead(204);
|
|
116
|
+
response.end();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
120
|
+
response.end("Not found");
|
|
121
|
+
}
|
|
122
|
+
function isPortInUse(error) {
|
|
123
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
|
|
124
|
+
}
|
|
125
|
+
async function startServer(html, preferredPort) {
|
|
126
|
+
for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt += 1) {
|
|
127
|
+
const port = preferredPort + attempt;
|
|
128
|
+
const server = createServer(
|
|
129
|
+
(request, response) => handleRequest(request, response, html)
|
|
130
|
+
);
|
|
131
|
+
try {
|
|
132
|
+
await new Promise((resolve, reject) => {
|
|
133
|
+
server.once("error", reject);
|
|
134
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
135
|
+
});
|
|
136
|
+
return { server, port };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
server.close();
|
|
139
|
+
if (isPortInUse(error)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Could not find an open port in range ${preferredPort}-${preferredPort + MAX_PORT_ATTEMPTS - 1}.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
function openBrowser(url) {
|
|
150
|
+
const platform = process.platform;
|
|
151
|
+
const command = platform === "darwin" ? { cmd: "open", args: [url] } : platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", url] } : { cmd: "xdg-open", args: [url] };
|
|
152
|
+
const child = spawn(command.cmd, command.args, {
|
|
153
|
+
stdio: "ignore",
|
|
154
|
+
detached: true
|
|
155
|
+
});
|
|
156
|
+
child.unref();
|
|
157
|
+
}
|
|
158
|
+
async function readTokens(tokenPath) {
|
|
159
|
+
const raw = await readFile(tokenPath, "utf8");
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(raw);
|
|
162
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
163
|
+
throw new Error("Token file must contain a JSON object at the root.");
|
|
164
|
+
}
|
|
165
|
+
return parsed;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Failed to parse JSON from ${tokenPath}: ${error.message}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function main() {
|
|
173
|
+
try {
|
|
174
|
+
const options = parseArgs(process.argv.slice(2));
|
|
175
|
+
const resolvedTokenPath = path.resolve(process.cwd(), options.tokenFile);
|
|
176
|
+
if (!existsSync(resolvedTokenPath)) {
|
|
177
|
+
throw new Error(`Token file not found: ${resolvedTokenPath}`);
|
|
178
|
+
}
|
|
179
|
+
const [tokens, css, appBundle] = await Promise.all([
|
|
180
|
+
readTokens(resolvedTokenPath),
|
|
181
|
+
readFile(resolveDistAsset("styles.css"), "utf8"),
|
|
182
|
+
readFile(resolveDistAsset("cli/browser.js"), "utf8")
|
|
183
|
+
]);
|
|
184
|
+
const html = buildHtml(tokens, css, appBundle);
|
|
185
|
+
const { server, port } = await startServer(html, options.port);
|
|
186
|
+
const url = `http://localhost:${port}`;
|
|
187
|
+
console.log(`TokVista running at ${url}`);
|
|
188
|
+
console.log(`Using tokens: ${resolvedTokenPath}`);
|
|
189
|
+
if (options.openBrowser) {
|
|
190
|
+
try {
|
|
191
|
+
openBrowser(url);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.warn(
|
|
194
|
+
`Could not auto-open browser. Open this URL manually: ${url}
|
|
195
|
+
${error.message}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
console.log("Browser auto-open disabled (--no-open).");
|
|
200
|
+
}
|
|
201
|
+
const shutdown = () => {
|
|
202
|
+
server.close(() => {
|
|
203
|
+
process.exit(0);
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
process.on("SIGINT", shutdown);
|
|
207
|
+
process.on("SIGTERM", shutdown);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error(error.message);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
void main();
|