tokvista 1.5.2 → 1.6.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 +22 -0
- package/dist/bin/tokvista.js +848 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,6 +90,25 @@ npx tokvista tokens.json --port 4000
|
|
|
90
90
|
npx tokvista tokens.json --no-open
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### Export Tokens
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Export to CSS
|
|
97
|
+
npx tokvista export tokens.json --format css --output tokens.css
|
|
98
|
+
|
|
99
|
+
# Export to SCSS
|
|
100
|
+
npx tokvista export tokens.json --format scss --output _tokens.scss
|
|
101
|
+
|
|
102
|
+
# Export to JavaScript
|
|
103
|
+
npx tokvista export tokens.json --format json --output tokens.js
|
|
104
|
+
|
|
105
|
+
# Export to Tailwind config
|
|
106
|
+
npx tokvista export tokens.json --format tailwind --output tailwind.config.js
|
|
107
|
+
|
|
108
|
+
# Print to stdout (for piping)
|
|
109
|
+
npx tokvista export tokens.json --format css
|
|
110
|
+
```
|
|
111
|
+
|
|
93
112
|
### Interactive Setup
|
|
94
113
|
|
|
95
114
|
```bash
|
|
@@ -118,8 +137,11 @@ Then run `npx tokvista` to use your config.
|
|
|
118
137
|
|--------|-------------|
|
|
119
138
|
| `tokvista [file]` | Token file path (default: `./tokens.json`) |
|
|
120
139
|
| `tokvista init` | Interactive config setup |
|
|
140
|
+
| `tokvista export <file> --format <type>` | Export tokens (css, scss, json, tailwind) |
|
|
121
141
|
| `--config`, `-c` | Config file path |
|
|
122
142
|
| `--port`, `-p` | Server port (default: `3000`) |
|
|
143
|
+
| `--format` | Export format (export only) |
|
|
144
|
+
| `--output`, `-o` | Output file path (export only) |
|
|
123
145
|
| `--no-open` | Don't open browser |
|
|
124
146
|
| `--no-watch` | Disable live reload |
|
|
125
147
|
| `--no-preview` | Skip preview after init |
|
package/dist/bin/tokvista.js
CHANGED
|
@@ -9,6 +9,758 @@ import path from "path";
|
|
|
9
9
|
import { createInterface } from "readline";
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
11
11
|
|
|
12
|
+
// src/utils/formatDetector.ts
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function hasTokenShape(obj) {
|
|
17
|
+
if (!isRecord(obj)) return false;
|
|
18
|
+
return "value" in obj && "type" in obj || "$value" in obj && "$type" in obj;
|
|
19
|
+
}
|
|
20
|
+
function countTokens(obj, maxDepth = 5, depth = 0) {
|
|
21
|
+
if (depth > maxDepth || !isRecord(obj)) return 0;
|
|
22
|
+
if (hasTokenShape(obj)) return 1;
|
|
23
|
+
let count = 0;
|
|
24
|
+
for (const value of Object.values(obj)) {
|
|
25
|
+
count += countTokens(value, maxDepth, depth + 1);
|
|
26
|
+
}
|
|
27
|
+
return count;
|
|
28
|
+
}
|
|
29
|
+
function detectStyleDictionary(data) {
|
|
30
|
+
const issues = [];
|
|
31
|
+
const keys = Object.keys(data);
|
|
32
|
+
if (keys.length === 0) return { match: false, issues };
|
|
33
|
+
const cssVarKeys = keys.filter((k) => k.startsWith("--") || k.startsWith("$"));
|
|
34
|
+
const hasCssVars = cssVarKeys.length > keys.length * 0.5;
|
|
35
|
+
if (hasCssVars) {
|
|
36
|
+
const allStrings = Object.values(data).every((v) => typeof v === "string" || typeof v === "number");
|
|
37
|
+
if (allStrings) return { match: true, issues };
|
|
38
|
+
issues.push("Style Dictionary format detected but some values are not primitives");
|
|
39
|
+
}
|
|
40
|
+
return { match: false, issues };
|
|
41
|
+
}
|
|
42
|
+
function detectSupernova(data) {
|
|
43
|
+
const issues = [];
|
|
44
|
+
if ("tokens" in data && Array.isArray(data.tokens)) {
|
|
45
|
+
const tokens = data.tokens;
|
|
46
|
+
if (tokens.length > 0) {
|
|
47
|
+
const first = tokens[0];
|
|
48
|
+
if (isRecord(first) && "id" in first && "name" in first && "value" in first) {
|
|
49
|
+
return { match: true, issues };
|
|
50
|
+
}
|
|
51
|
+
issues.push("Supernova format detected but tokens array has unexpected structure");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { match: false, issues };
|
|
55
|
+
}
|
|
56
|
+
function detectFigmaAPI(data) {
|
|
57
|
+
const issues = [];
|
|
58
|
+
if ("meta" in data && isRecord(data.meta)) {
|
|
59
|
+
const meta = data.meta;
|
|
60
|
+
if ("variables" in meta || "variableCollections" in meta) {
|
|
61
|
+
return { match: true, issues };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if ("nodes" in data && isRecord(data.nodes)) {
|
|
65
|
+
issues.push("Figma API response detected but needs variable extraction");
|
|
66
|
+
return { match: true, issues };
|
|
67
|
+
}
|
|
68
|
+
return { match: false, issues };
|
|
69
|
+
}
|
|
70
|
+
function detectW3C(data) {
|
|
71
|
+
const issues = [];
|
|
72
|
+
const tokenCount = countTokens(data);
|
|
73
|
+
if (tokenCount === 0) return { match: false, issues };
|
|
74
|
+
let w3cCount = 0;
|
|
75
|
+
let totalChecked = 0;
|
|
76
|
+
const checkW3C = (obj, depth = 0) => {
|
|
77
|
+
if (depth > 10 || !isRecord(obj)) return;
|
|
78
|
+
if ("$value" in obj && "$type" in obj) {
|
|
79
|
+
w3cCount++;
|
|
80
|
+
totalChecked++;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if ("value" in obj && "type" in obj) {
|
|
84
|
+
totalChecked++;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
for (const value of Object.values(obj)) {
|
|
88
|
+
checkW3C(value, depth + 1);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
checkW3C(data);
|
|
92
|
+
if (totalChecked > 0 && w3cCount > totalChecked * 0.5) {
|
|
93
|
+
return { match: true, issues };
|
|
94
|
+
}
|
|
95
|
+
return { match: false, issues };
|
|
96
|
+
}
|
|
97
|
+
function detectTokenStudio(data) {
|
|
98
|
+
const issues = [];
|
|
99
|
+
const tokenCount = countTokens(data);
|
|
100
|
+
if (tokenCount === 0) return { match: false, issues };
|
|
101
|
+
let studioCount = 0;
|
|
102
|
+
let totalChecked = 0;
|
|
103
|
+
const checkStudio = (obj, depth = 0) => {
|
|
104
|
+
if (depth > 10 || !isRecord(obj)) return;
|
|
105
|
+
if ("value" in obj && "type" in obj && !("$value" in obj)) {
|
|
106
|
+
studioCount++;
|
|
107
|
+
totalChecked++;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if ("$value" in obj) {
|
|
111
|
+
totalChecked++;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const value of Object.values(obj)) {
|
|
115
|
+
checkStudio(value, depth + 1);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
checkStudio(data);
|
|
119
|
+
if (totalChecked > 0 && studioCount > totalChecked * 0.5) {
|
|
120
|
+
return { match: true, issues };
|
|
121
|
+
}
|
|
122
|
+
return { match: false, issues };
|
|
123
|
+
}
|
|
124
|
+
function detectTokenFormat(input) {
|
|
125
|
+
if (Array.isArray(input)) {
|
|
126
|
+
if (input.length === 0) {
|
|
127
|
+
return {
|
|
128
|
+
format: "unknown",
|
|
129
|
+
confidence: 0,
|
|
130
|
+
issues: ["Token array is empty"],
|
|
131
|
+
suggestions: ["Add tokens to the array"]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const first = input[0];
|
|
135
|
+
if (isRecord(first) && "id" in first && "tokenType" in first && "value" in first) {
|
|
136
|
+
return { format: "supernova", confidence: 0.95, issues: [], suggestions: [] };
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
format: "unknown",
|
|
140
|
+
confidence: 0,
|
|
141
|
+
issues: ["Array format detected but structure is not recognized"],
|
|
142
|
+
suggestions: ["Supernova format expects: [{ id, name, tokenType, value, category }]"]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (!isRecord(input)) {
|
|
146
|
+
return {
|
|
147
|
+
format: "unknown",
|
|
148
|
+
confidence: 0,
|
|
149
|
+
issues: ["Input is not a valid JSON object or array"],
|
|
150
|
+
suggestions: ["Ensure your token file contains a valid JSON object or array"]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const data = input;
|
|
154
|
+
const allIssues = [];
|
|
155
|
+
const suggestions = [];
|
|
156
|
+
if ("$format" in data && typeof data.$format === "string") {
|
|
157
|
+
const format = data.$format.toLowerCase();
|
|
158
|
+
if (format.includes("tokvista") || format.includes("token-studio")) {
|
|
159
|
+
return { format: "token-studio", confidence: 1, issues: [], suggestions: [] };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const figmaResult = detectFigmaAPI(data);
|
|
163
|
+
if (figmaResult.match) {
|
|
164
|
+
allIssues.push(...figmaResult.issues);
|
|
165
|
+
suggestions.push("Extract variables from meta.variables or use Figma plugin export");
|
|
166
|
+
return { format: "figma-api", confidence: 0.9, issues: allIssues, suggestions };
|
|
167
|
+
}
|
|
168
|
+
const supernovaResult = detectSupernova(data);
|
|
169
|
+
if (supernovaResult.match) {
|
|
170
|
+
allIssues.push(...supernovaResult.issues);
|
|
171
|
+
return { format: "supernova", confidence: 0.9, issues: allIssues, suggestions };
|
|
172
|
+
}
|
|
173
|
+
const styleDictResult = detectStyleDictionary(data);
|
|
174
|
+
if (styleDictResult.match) {
|
|
175
|
+
allIssues.push(...styleDictResult.issues);
|
|
176
|
+
return { format: "style-dictionary", confidence: 0.85, issues: allIssues, suggestions };
|
|
177
|
+
}
|
|
178
|
+
const w3cResult = detectW3C(data);
|
|
179
|
+
const studioResult = detectTokenStudio(data);
|
|
180
|
+
if (w3cResult.match && studioResult.match) {
|
|
181
|
+
return { format: "token-studio", confidence: 0.8, issues: [], suggestions: [] };
|
|
182
|
+
}
|
|
183
|
+
if (w3cResult.match) {
|
|
184
|
+
return { format: "w3c", confidence: 0.8, issues: [], suggestions: [] };
|
|
185
|
+
}
|
|
186
|
+
if (studioResult.match) {
|
|
187
|
+
return { format: "token-studio", confidence: 0.8, issues: [], suggestions: [] };
|
|
188
|
+
}
|
|
189
|
+
const tokenCount = countTokens(data);
|
|
190
|
+
if (tokenCount === 0) {
|
|
191
|
+
allIssues.push("No valid tokens found with {type, value} or {$type, $value} structure");
|
|
192
|
+
suggestions.push('Tokens should have format: { "type": "color", "value": "#fff" }');
|
|
193
|
+
suggestions.push('Or W3C format: { "$type": "color", "$value": "#fff" }');
|
|
194
|
+
} else {
|
|
195
|
+
allIssues.push(`Found ${tokenCount} potential tokens but format is unclear`);
|
|
196
|
+
suggestions.push("Check that tokens have consistent {type, value} structure");
|
|
197
|
+
}
|
|
198
|
+
return { format: "unknown", confidence: 0, issues: allIssues, suggestions };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/utils/formatNormalizers.ts
|
|
202
|
+
function isRecord2(value) {
|
|
203
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
204
|
+
}
|
|
205
|
+
function normalizeW3C(data) {
|
|
206
|
+
const normalize = (obj) => {
|
|
207
|
+
if (!isRecord2(obj)) return obj;
|
|
208
|
+
if ("$value" in obj && "$type" in obj) {
|
|
209
|
+
const result2 = {
|
|
210
|
+
value: obj.$value,
|
|
211
|
+
type: obj.$type
|
|
212
|
+
};
|
|
213
|
+
if (obj.$description) {
|
|
214
|
+
result2.description = obj.$description;
|
|
215
|
+
}
|
|
216
|
+
return result2;
|
|
217
|
+
}
|
|
218
|
+
const result = {};
|
|
219
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
220
|
+
if (key.startsWith("$") && !["$value", "$type", "$description"].includes(key)) {
|
|
221
|
+
result[key] = value;
|
|
222
|
+
} else if (!key.startsWith("$")) {
|
|
223
|
+
result[key] = normalize(value);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
};
|
|
228
|
+
return normalize(data);
|
|
229
|
+
}
|
|
230
|
+
function normalizeStyleDictionary(data) {
|
|
231
|
+
const tokens = {};
|
|
232
|
+
for (const [key, value] of Object.entries(data)) {
|
|
233
|
+
if (typeof value !== "string" && typeof value !== "number") continue;
|
|
234
|
+
let cleanKey = key;
|
|
235
|
+
if (key.startsWith("--")) cleanKey = key.slice(2);
|
|
236
|
+
if (key.startsWith("$")) cleanKey = key.slice(1);
|
|
237
|
+
const parts = cleanKey.split("-").filter(Boolean);
|
|
238
|
+
if (parts.length === 0) continue;
|
|
239
|
+
let type = "string";
|
|
240
|
+
const valueStr = String(value);
|
|
241
|
+
if (valueStr.match(/^#[0-9a-f]{3,8}$/i) || valueStr.match(/^rgba?\(/i)) {
|
|
242
|
+
type = "color";
|
|
243
|
+
} else if (valueStr.match(/^\d+px$/)) {
|
|
244
|
+
if (parts.some((p) => p.includes("space") || p.includes("spacing") || p.includes("gap"))) {
|
|
245
|
+
type = "spacing";
|
|
246
|
+
} else if (parts.some((p) => p.includes("radius") || p.includes("round"))) {
|
|
247
|
+
type = "borderRadius";
|
|
248
|
+
} else if (parts.some((p) => p.includes("size") || p.includes("width") || p.includes("height"))) {
|
|
249
|
+
type = "sizing";
|
|
250
|
+
} else {
|
|
251
|
+
type = "dimension";
|
|
252
|
+
}
|
|
253
|
+
} else if (valueStr.match(/^\d+$/)) {
|
|
254
|
+
type = "number";
|
|
255
|
+
}
|
|
256
|
+
let current = tokens;
|
|
257
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
258
|
+
const part = parts[i];
|
|
259
|
+
if (!current[part]) current[part] = {};
|
|
260
|
+
current = current[part];
|
|
261
|
+
}
|
|
262
|
+
const lastPart = parts[parts.length - 1];
|
|
263
|
+
current[lastPart] = { type, value };
|
|
264
|
+
}
|
|
265
|
+
return { tokens };
|
|
266
|
+
}
|
|
267
|
+
function normalizeSupernova(data) {
|
|
268
|
+
const tokenArray = Array.isArray(data) ? data : isRecord2(data) && Array.isArray(data.tokens) ? data.tokens : [];
|
|
269
|
+
const tokens = {};
|
|
270
|
+
for (const token of tokenArray) {
|
|
271
|
+
if (!isRecord2(token)) continue;
|
|
272
|
+
const id = token.id;
|
|
273
|
+
const name = token.name || id;
|
|
274
|
+
const tokenValue = token.value;
|
|
275
|
+
const tokenType = token.tokenType || token.type || "string";
|
|
276
|
+
const category = token.category;
|
|
277
|
+
if (!name || tokenValue === void 0) continue;
|
|
278
|
+
let value = tokenValue;
|
|
279
|
+
if (isRecord2(tokenValue)) {
|
|
280
|
+
if ("hex" in tokenValue) value = tokenValue.hex;
|
|
281
|
+
else if ("measure" in tokenValue && "unit" in tokenValue) {
|
|
282
|
+
value = `${tokenValue.measure}${tokenValue.unit}`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
let type = "string";
|
|
286
|
+
if (tokenType === "Color") type = "color";
|
|
287
|
+
else if (tokenType === "Dimension") type = "dimension";
|
|
288
|
+
else if (tokenType === "Number") type = "number";
|
|
289
|
+
else type = tokenType.toLowerCase();
|
|
290
|
+
const pathParts = [];
|
|
291
|
+
if (category) pathParts.push(category);
|
|
292
|
+
pathParts.push(...name.split("/").filter(Boolean));
|
|
293
|
+
if (pathParts.length === 0) continue;
|
|
294
|
+
let current = tokens;
|
|
295
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
296
|
+
const part = pathParts[i];
|
|
297
|
+
if (!current[part]) current[part] = {};
|
|
298
|
+
current = current[part];
|
|
299
|
+
}
|
|
300
|
+
const lastPart = pathParts[pathParts.length - 1];
|
|
301
|
+
current[lastPart] = { type, value };
|
|
302
|
+
}
|
|
303
|
+
return tokens;
|
|
304
|
+
}
|
|
305
|
+
function normalizeFigmaAPI(data) {
|
|
306
|
+
if ("meta" in data && isRecord2(data.meta)) {
|
|
307
|
+
const meta = data.meta;
|
|
308
|
+
if ("variables" in meta && isRecord2(meta.variables)) {
|
|
309
|
+
const tokens = {};
|
|
310
|
+
for (const [key, variable] of Object.entries(meta.variables)) {
|
|
311
|
+
if (!isRecord2(variable)) continue;
|
|
312
|
+
const name = variable.name || key;
|
|
313
|
+
const resolvedType = variable.resolvedType;
|
|
314
|
+
const valuesByMode = variable.valuesByMode;
|
|
315
|
+
if (!name || !valuesByMode || !isRecord2(valuesByMode)) continue;
|
|
316
|
+
const firstModeValue = Object.values(valuesByMode)[0];
|
|
317
|
+
let type = "string";
|
|
318
|
+
if (resolvedType === "COLOR") type = "color";
|
|
319
|
+
else if (resolvedType === "FLOAT") type = "number";
|
|
320
|
+
else if (resolvedType === "STRING") type = "string";
|
|
321
|
+
const parts = name.split("/").filter(Boolean);
|
|
322
|
+
if (parts.length === 0) continue;
|
|
323
|
+
let current = tokens;
|
|
324
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
325
|
+
const part = parts[i];
|
|
326
|
+
if (!current[part]) current[part] = {};
|
|
327
|
+
current = current[part];
|
|
328
|
+
}
|
|
329
|
+
const lastPart = parts[parts.length - 1];
|
|
330
|
+
current[lastPart] = { type, value: firstModeValue };
|
|
331
|
+
}
|
|
332
|
+
return { tokens };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return data;
|
|
336
|
+
}
|
|
337
|
+
function normalizeTokenFormat(data, detectedFormat) {
|
|
338
|
+
if (!isRecord2(data) && !Array.isArray(data)) return {};
|
|
339
|
+
switch (detectedFormat) {
|
|
340
|
+
case "w3c":
|
|
341
|
+
return isRecord2(data) ? normalizeW3C(data) : {};
|
|
342
|
+
case "style-dictionary":
|
|
343
|
+
return isRecord2(data) ? normalizeStyleDictionary(data) : {};
|
|
344
|
+
case "supernova":
|
|
345
|
+
return normalizeSupernova(data);
|
|
346
|
+
case "figma-api":
|
|
347
|
+
return isRecord2(data) ? normalizeFigmaAPI(data) : {};
|
|
348
|
+
case "token-studio":
|
|
349
|
+
case "unknown":
|
|
350
|
+
default:
|
|
351
|
+
return isRecord2(data) ? data : {};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/utils/core.ts
|
|
356
|
+
function isTokenValue(obj) {
|
|
357
|
+
return typeof obj === "object" && obj !== null && "value" in obj && "type" in obj;
|
|
358
|
+
}
|
|
359
|
+
function isRecord3(value) {
|
|
360
|
+
return typeof value === "object" && value !== null;
|
|
361
|
+
}
|
|
362
|
+
function canonicalKey(value) {
|
|
363
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
364
|
+
}
|
|
365
|
+
function hasStructuredSets(record) {
|
|
366
|
+
if (Object.keys(record).some((key) => {
|
|
367
|
+
const normalized = canonicalKey(key);
|
|
368
|
+
return normalized === "foundationvalue" || normalized === "semanticvalue" || normalized.startsWith("components");
|
|
369
|
+
})) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
return Object.keys(record).some((key) => {
|
|
373
|
+
const normalized = canonicalKey(key);
|
|
374
|
+
return normalized === "foundation" || normalized === "semantic" || normalized === "components";
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
function getCaseInsensitiveRecord(record, ...candidateNames) {
|
|
378
|
+
const candidates = new Set(candidateNames.map((name) => canonicalKey(name)));
|
|
379
|
+
for (const [key, value] of Object.entries(record)) {
|
|
380
|
+
if (!isRecord3(value)) continue;
|
|
381
|
+
if (candidates.has(canonicalKey(key))) {
|
|
382
|
+
return value;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
function isModeLikeKey(key) {
|
|
388
|
+
const normalized = canonicalKey(key);
|
|
389
|
+
return normalized === "value" || normalized === "default" || normalized === "mode" || /^mode\d+$/.test(normalized) || /^theme\d+$/.test(normalized);
|
|
390
|
+
}
|
|
391
|
+
function mergeModeRecords(record) {
|
|
392
|
+
const modeEntries = Object.entries(record).filter(([key, value]) => isModeLikeKey(key) && isRecord3(value));
|
|
393
|
+
if (modeEntries.length === 0) {
|
|
394
|
+
return record;
|
|
395
|
+
}
|
|
396
|
+
return modeEntries.reduce((acc, [, value]) => {
|
|
397
|
+
return deepMergeRecords(acc, value);
|
|
398
|
+
}, {});
|
|
399
|
+
}
|
|
400
|
+
function getPrefixedRecord(record, prefix) {
|
|
401
|
+
const target = `${prefix}value`;
|
|
402
|
+
for (const [key, value] of Object.entries(record)) {
|
|
403
|
+
if (!isRecord3(value)) continue;
|
|
404
|
+
if (canonicalKey(key) === target) {
|
|
405
|
+
return value;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
function getDirectCollectionRecord(record, collectionName) {
|
|
411
|
+
const direct = getCaseInsensitiveRecord(record, collectionName);
|
|
412
|
+
if (!direct) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const valueRecord = getCaseInsensitiveRecord(direct, "value");
|
|
416
|
+
if (valueRecord) {
|
|
417
|
+
return mergeModeRecords(valueRecord);
|
|
418
|
+
}
|
|
419
|
+
return mergeModeRecords(direct);
|
|
420
|
+
}
|
|
421
|
+
function getComponentsRecord(record) {
|
|
422
|
+
const mergedFromPrefixed = Object.entries(record).filter(([key, value]) => isRecord3(value) && canonicalKey(key).startsWith("components")).reduce((acc, [, value]) => {
|
|
423
|
+
return deepMergeRecords(acc, value);
|
|
424
|
+
}, {});
|
|
425
|
+
const directComponents = getCaseInsensitiveRecord(record, "components");
|
|
426
|
+
if (!directComponents) {
|
|
427
|
+
return mergedFromPrefixed;
|
|
428
|
+
}
|
|
429
|
+
return deepMergeRecords(mergedFromPrefixed, mergeModeRecords(directComponents));
|
|
430
|
+
}
|
|
431
|
+
function normalizeTokenSetsRoot(input) {
|
|
432
|
+
if (!isRecord3(input)) return {};
|
|
433
|
+
const detection = detectTokenFormat(input);
|
|
434
|
+
let normalized = input;
|
|
435
|
+
if (detection.format !== "token-studio" && detection.format !== "unknown") {
|
|
436
|
+
if (typeof console !== "undefined" && console.info) {
|
|
437
|
+
console.info(`[Tokvista] Detected ${detection.format} format (confidence: ${detection.confidence}), normalizing...`);
|
|
438
|
+
}
|
|
439
|
+
normalized = normalizeTokenFormat(input, detection.format);
|
|
440
|
+
} else if (detection.format === "unknown" && detection.confidence === 0) {
|
|
441
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
442
|
+
console.warn("[Tokvista] Unknown token format detected:", detection.issues);
|
|
443
|
+
if (detection.suggestions.length > 0) {
|
|
444
|
+
console.warn("[Tokvista] Suggestions:", detection.suggestions);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const directRoot = isRecord3(normalized.tokens) ? normalized.tokens : normalized;
|
|
449
|
+
const candidateKeys = Object.keys(directRoot).filter((key) => !key.startsWith("$"));
|
|
450
|
+
if (candidateKeys.length === 1) {
|
|
451
|
+
const inner = directRoot[candidateKeys[0]];
|
|
452
|
+
if (isRecord3(inner) && hasStructuredSets(inner)) {
|
|
453
|
+
return inner;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return directRoot;
|
|
457
|
+
}
|
|
458
|
+
function getFoundationTokenTree(tokens) {
|
|
459
|
+
const normalizedRoot = normalizeTokenSetsRoot(tokens);
|
|
460
|
+
const source = getPrefixedRecord(normalizedRoot, "foundation") || getDirectCollectionRecord(normalizedRoot, "foundation") || normalizedRoot;
|
|
461
|
+
if (!isRecord3(source)) return {};
|
|
462
|
+
const keys = Object.keys(source).filter((key) => !key.startsWith("$"));
|
|
463
|
+
if (keys.length === 1 && keys[0].toLowerCase() === "base") {
|
|
464
|
+
const baseValue = source[keys[0]];
|
|
465
|
+
if (isRecord3(baseValue)) {
|
|
466
|
+
return baseValue;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return source;
|
|
470
|
+
}
|
|
471
|
+
function extractFoundationSet(tokens) {
|
|
472
|
+
return getFoundationTokenTree(tokens);
|
|
473
|
+
}
|
|
474
|
+
function extractSemanticSet(tokens) {
|
|
475
|
+
const normalizedRoot = normalizeTokenSetsRoot(tokens);
|
|
476
|
+
const semantic = getPrefixedRecord(normalizedRoot, "semantic") || getDirectCollectionRecord(normalizedRoot, "semantic");
|
|
477
|
+
return semantic || {};
|
|
478
|
+
}
|
|
479
|
+
function extractComponentSet(tokens) {
|
|
480
|
+
const normalizedRoot = normalizeTokenSetsRoot(tokens);
|
|
481
|
+
return getComponentsRecord(normalizedRoot);
|
|
482
|
+
}
|
|
483
|
+
function findAllTokens(obj, path2 = []) {
|
|
484
|
+
const tokens = [];
|
|
485
|
+
if (!obj || typeof obj !== "object") return tokens;
|
|
486
|
+
if (isTokenValue(obj)) {
|
|
487
|
+
tokens.push({ path: path2.join("."), token: obj });
|
|
488
|
+
return tokens;
|
|
489
|
+
}
|
|
490
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
491
|
+
tokens.push(...findAllTokens(value, [...path2, key]));
|
|
492
|
+
}
|
|
493
|
+
return tokens;
|
|
494
|
+
}
|
|
495
|
+
function createTokenMap(tokens) {
|
|
496
|
+
const map = {};
|
|
497
|
+
const normalizedRoot = normalizeTokenSetsRoot(tokens);
|
|
498
|
+
Object.entries(normalizedRoot).forEach(([setKey, setData]) => {
|
|
499
|
+
if (["global", "$themes", "$metadata"].includes(setKey)) return;
|
|
500
|
+
const allTokens = findAllTokens(setData);
|
|
501
|
+
allTokens.forEach(({ path: path2, token }) => {
|
|
502
|
+
map[path2] = typeof token.value === "string" ? token.value : String(token.value);
|
|
503
|
+
map[`${setKey}.${path2}`] = typeof token.value === "string" ? token.value : String(token.value);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
const addStructuredEntries = (prefix, value) => {
|
|
507
|
+
const allTokens = findAllTokens(value);
|
|
508
|
+
allTokens.forEach(({ path: path2, token }) => {
|
|
509
|
+
const tokenValue = typeof token.value === "string" ? token.value : String(token.value);
|
|
510
|
+
map[`${prefix}.${path2}`] = tokenValue;
|
|
511
|
+
});
|
|
512
|
+
};
|
|
513
|
+
addStructuredEntries("Foundation", extractFoundationSet(normalizedRoot));
|
|
514
|
+
addStructuredEntries("Semantic", extractSemanticSet(normalizedRoot));
|
|
515
|
+
addStructuredEntries("Components", extractComponentSet(normalizedRoot));
|
|
516
|
+
return map;
|
|
517
|
+
}
|
|
518
|
+
function resolveTokenValue(value, tokenMap, maxDepth = 10) {
|
|
519
|
+
if (!value || typeof value !== "string") return value;
|
|
520
|
+
let currentValue = value;
|
|
521
|
+
let depth = 0;
|
|
522
|
+
while (currentValue.startsWith("{") && currentValue.endsWith("}") && depth < maxDepth) {
|
|
523
|
+
const refPath = currentValue.slice(1, -1);
|
|
524
|
+
let resolved = tokenMap[refPath];
|
|
525
|
+
if (resolved === void 0 && refPath.includes(".")) {
|
|
526
|
+
const withoutCollectionPrefix = refPath.slice(refPath.indexOf(".") + 1);
|
|
527
|
+
resolved = tokenMap[withoutCollectionPrefix];
|
|
528
|
+
}
|
|
529
|
+
if (resolved !== void 0) {
|
|
530
|
+
currentValue = resolved;
|
|
531
|
+
} else {
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
depth++;
|
|
535
|
+
}
|
|
536
|
+
return currentValue;
|
|
537
|
+
}
|
|
538
|
+
function deepMergeRecords(target, source) {
|
|
539
|
+
Object.entries(source).forEach(([key, nextValue]) => {
|
|
540
|
+
const currentValue = target[key];
|
|
541
|
+
const currentIsObject = typeof currentValue === "object" && currentValue !== null;
|
|
542
|
+
const nextIsObject = typeof nextValue === "object" && nextValue !== null;
|
|
543
|
+
const currentIsTokenLeaf = currentIsObject && "value" in currentValue;
|
|
544
|
+
const nextIsTokenLeaf = nextIsObject && "value" in nextValue;
|
|
545
|
+
if (currentIsObject && nextIsObject && !currentIsTokenLeaf && !nextIsTokenLeaf) {
|
|
546
|
+
target[key] = deepMergeRecords(
|
|
547
|
+
currentValue,
|
|
548
|
+
nextValue
|
|
549
|
+
);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
target[key] = nextValue;
|
|
553
|
+
});
|
|
554
|
+
return target;
|
|
555
|
+
}
|
|
556
|
+
function isTokenLike(value) {
|
|
557
|
+
return isRecord3(value) && "value" in value;
|
|
558
|
+
}
|
|
559
|
+
function normalizeColorPath(path2) {
|
|
560
|
+
const wrappers = /* @__PURE__ */ new Set(["color", "colors", "palette", "palettes", "base", "foundation", "value"]);
|
|
561
|
+
const filtered = path2.filter((part) => !wrappers.has(part.toLowerCase()));
|
|
562
|
+
return filtered.length > 0 ? filtered : path2;
|
|
563
|
+
}
|
|
564
|
+
function determineTokenType(name, tokenType) {
|
|
565
|
+
const n = name.toLowerCase();
|
|
566
|
+
const rawType = String(tokenType || "").toLowerCase();
|
|
567
|
+
if (rawType === "color") return "color";
|
|
568
|
+
if (rawType === "spacing") return "spacing";
|
|
569
|
+
if (rawType === "sizing" || rawType === "size") return "size";
|
|
570
|
+
if (rawType === "borderradius" || rawType === "radius") return "radius";
|
|
571
|
+
if (rawType.includes("font") || rawType.includes("line")) return "typography";
|
|
572
|
+
if (n.includes("color") || n.includes("fill") || n.includes("stroke") || n.includes("text") || n.includes("bg")) return "color";
|
|
573
|
+
if (n.includes("space") || n.includes("spacing") || n.includes("gap") || n.includes("padding") || n.includes("margin")) return "spacing";
|
|
574
|
+
if (n.includes("size") || n.includes("width") || n.includes("height")) return "size";
|
|
575
|
+
if (n.includes("radius") || n.includes("round")) return "radius";
|
|
576
|
+
if (n.includes("font") || n.includes("line-height") || n.includes("typography") || n.includes("letter")) return "typography";
|
|
577
|
+
return "component";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/utils/exportUtils.ts
|
|
581
|
+
function formatTokenValue(value, format, tokenMap) {
|
|
582
|
+
if (typeof value !== "string") return String(value);
|
|
583
|
+
const aliasMatch = value.match(/^\{(.+)\}$/);
|
|
584
|
+
if (aliasMatch) {
|
|
585
|
+
const path2 = aliasMatch[1];
|
|
586
|
+
let cleanPath = path2.replace(/\./g, "-");
|
|
587
|
+
if (cleanPath.startsWith("base-")) {
|
|
588
|
+
const afterBase = cleanPath.slice(5).toLowerCase();
|
|
589
|
+
const isSpatial = ["space", "size", "radius", "line-height", "border-width"].some((k) => afterBase.includes(k));
|
|
590
|
+
if (isSpatial) {
|
|
591
|
+
cleanPath = cleanPath.slice(5);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (format === "css") {
|
|
595
|
+
return `var(--${cleanPath})`;
|
|
596
|
+
} else if (format === "scss") {
|
|
597
|
+
return `$${cleanPath}`;
|
|
598
|
+
} else if (format === "js" && tokenMap) {
|
|
599
|
+
return resolveTokenValue(value, tokenMap);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return value;
|
|
603
|
+
}
|
|
604
|
+
function getFlattenedTokens(tokens) {
|
|
605
|
+
const flattened = [];
|
|
606
|
+
const pushToken = (name, cssVariable, value, type, category) => {
|
|
607
|
+
flattened.push({ name, cssVariable, value, type, category });
|
|
608
|
+
};
|
|
609
|
+
const foundationRoot = getFoundationTokenTree(tokens);
|
|
610
|
+
const walkFoundation = (node, path2 = []) => {
|
|
611
|
+
if (!isRecord3(node)) return;
|
|
612
|
+
if (isTokenLike(node) && node.value !== null) {
|
|
613
|
+
const joinedPath = path2.join("-");
|
|
614
|
+
const tokenType = determineTokenType(joinedPath, node.type);
|
|
615
|
+
const value = String(node.value);
|
|
616
|
+
if (tokenType === "color") {
|
|
617
|
+
const colorPath = normalizeColorPath(path2);
|
|
618
|
+
const colorName = colorPath.join("-");
|
|
619
|
+
pushToken(`base-${colorName}`, `--base-${colorName}`, value, tokenType, "Foundation");
|
|
620
|
+
} else {
|
|
621
|
+
pushToken(joinedPath, `--${joinedPath}`, value, tokenType, "Foundation");
|
|
622
|
+
}
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
Object.entries(node).forEach(([key, value]) => {
|
|
626
|
+
walkFoundation(value, [...path2, key]);
|
|
627
|
+
});
|
|
628
|
+
};
|
|
629
|
+
if (isRecord3(foundationRoot)) {
|
|
630
|
+
walkFoundation(foundationRoot);
|
|
631
|
+
}
|
|
632
|
+
const semanticSet = extractSemanticSet(tokens);
|
|
633
|
+
if (Object.keys(semanticSet).length > 0) {
|
|
634
|
+
const walkSemantic = (node, path2 = []) => {
|
|
635
|
+
if (!isRecord3(node)) return;
|
|
636
|
+
if (isTokenLike(node) && node.value !== null) {
|
|
637
|
+
const name = path2.join("-");
|
|
638
|
+
const tokenType = determineTokenType(name, node.type);
|
|
639
|
+
pushToken(name, `--${name}`, String(node.value), tokenType, "Semantic");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
Object.entries(node).forEach(([key, value]) => {
|
|
643
|
+
walkSemantic(value, [...path2, key]);
|
|
644
|
+
});
|
|
645
|
+
};
|
|
646
|
+
walkSemantic(semanticSet);
|
|
647
|
+
}
|
|
648
|
+
const mergedComponents = extractComponentSet(tokens);
|
|
649
|
+
Object.entries(mergedComponents).forEach(([compName, comp]) => {
|
|
650
|
+
if (!isRecord3(comp)) return;
|
|
651
|
+
const walkComponent = (node, path2 = []) => {
|
|
652
|
+
if (!isRecord3(node)) return;
|
|
653
|
+
if (isTokenLike(node) && node.value !== null) {
|
|
654
|
+
const suffix = path2.join("-");
|
|
655
|
+
const type = determineTokenType(suffix, node.type);
|
|
656
|
+
const tokenType = type === "size" ? "size" : type;
|
|
657
|
+
const name = suffix ? `${compName}-${suffix}` : compName;
|
|
658
|
+
pushToken(name, `--${name}`, String(node.value), tokenType, `Component (${compName})`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
Object.entries(node).forEach(([key, value]) => {
|
|
662
|
+
walkComponent(value, [...path2, key]);
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
walkComponent(comp);
|
|
666
|
+
});
|
|
667
|
+
return flattened;
|
|
668
|
+
}
|
|
669
|
+
function generateCSS(tokens) {
|
|
670
|
+
const flattened = getFlattenedTokens(tokens);
|
|
671
|
+
let css = ":root {\n";
|
|
672
|
+
const categories = ["Foundation", "Semantic"];
|
|
673
|
+
const componentCategories = Array.from(new Set(flattened.map((t) => t.category))).filter((c) => c.startsWith("Component"));
|
|
674
|
+
[...categories, ...componentCategories].forEach((cat) => {
|
|
675
|
+
const catTokens = flattened.filter((t) => t.category === cat);
|
|
676
|
+
if (catTokens.length > 0) {
|
|
677
|
+
css += ` /* ${cat} */
|
|
678
|
+
`;
|
|
679
|
+
catTokens.forEach((t) => {
|
|
680
|
+
const value = formatTokenValue(t.value, "css");
|
|
681
|
+
css += ` ${t.cssVariable}: ${value};
|
|
682
|
+
`;
|
|
683
|
+
});
|
|
684
|
+
css += "\n";
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
css = css.trim() + "\n}";
|
|
688
|
+
return css;
|
|
689
|
+
}
|
|
690
|
+
function generateSCSS(tokens) {
|
|
691
|
+
const flattened = getFlattenedTokens(tokens);
|
|
692
|
+
let scss = "";
|
|
693
|
+
const categories = ["Foundation", "Semantic"];
|
|
694
|
+
const componentCategories = Array.from(new Set(flattened.map((t) => t.category))).filter((c) => c.startsWith("Component"));
|
|
695
|
+
const allCategories = [...categories, ...componentCategories];
|
|
696
|
+
allCategories.forEach((cat) => {
|
|
697
|
+
const catTokens = flattened.filter((t) => t.category === cat);
|
|
698
|
+
if (catTokens.length > 0) {
|
|
699
|
+
scss += `// ${cat}
|
|
700
|
+
`;
|
|
701
|
+
catTokens.forEach((t) => {
|
|
702
|
+
const scssVar = t.cssVariable.replace("--", "$");
|
|
703
|
+
const value = formatTokenValue(t.value, "scss");
|
|
704
|
+
scss += `${scssVar}: ${value};
|
|
705
|
+
`;
|
|
706
|
+
});
|
|
707
|
+
scss += "\n";
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
scss += "// Token Map\n$tokens: (\n";
|
|
711
|
+
flattened.forEach((t) => {
|
|
712
|
+
const value = formatTokenValue(t.value, "scss");
|
|
713
|
+
scss += ` "${t.name}": ${value},
|
|
714
|
+
`;
|
|
715
|
+
});
|
|
716
|
+
scss += ");";
|
|
717
|
+
return scss;
|
|
718
|
+
}
|
|
719
|
+
function generateJS(tokens) {
|
|
720
|
+
const flattened = getFlattenedTokens(tokens);
|
|
721
|
+
const tokenMap = createTokenMap(tokens);
|
|
722
|
+
const jsObj = {};
|
|
723
|
+
flattened.forEach((t) => {
|
|
724
|
+
jsObj[t.name] = formatTokenValue(t.value, "js", tokenMap);
|
|
725
|
+
});
|
|
726
|
+
return `export const tokens = ${JSON.stringify(jsObj, null, 2)};`;
|
|
727
|
+
}
|
|
728
|
+
function generateTailwind(tokens) {
|
|
729
|
+
const flattened = getFlattenedTokens(tokens);
|
|
730
|
+
const config = {
|
|
731
|
+
theme: {
|
|
732
|
+
extend: {
|
|
733
|
+
colors: {},
|
|
734
|
+
spacing: {},
|
|
735
|
+
width: {},
|
|
736
|
+
borderRadius: {},
|
|
737
|
+
fontSize: {}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
flattened.forEach((t) => {
|
|
742
|
+
const cleanName = t.name;
|
|
743
|
+
if (t.type === "color") {
|
|
744
|
+
config.theme.extend.colors[cleanName] = `var(${t.cssVariable})`;
|
|
745
|
+
} else if (t.type === "spacing") {
|
|
746
|
+
config.theme.extend.spacing[cleanName] = `var(${t.cssVariable})`;
|
|
747
|
+
} else if (t.type === "size") {
|
|
748
|
+
config.theme.extend.width[cleanName] = `var(${t.cssVariable})`;
|
|
749
|
+
} else if (t.type === "radius") {
|
|
750
|
+
config.theme.extend.borderRadius[cleanName] = `var(${t.cssVariable})`;
|
|
751
|
+
} else if (t.type === "typography" && t.name.includes("font-size")) {
|
|
752
|
+
config.theme.extend.fontSize[cleanName] = `var(${t.cssVariable})`;
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
if (Object.keys(config.theme.extend.colors).length === 0) delete config.theme.extend.colors;
|
|
756
|
+
if (Object.keys(config.theme.extend.spacing).length === 0) delete config.theme.extend.spacing;
|
|
757
|
+
if (Object.keys(config.theme.extend.width).length === 0) delete config.theme.extend.width;
|
|
758
|
+
if (Object.keys(config.theme.extend.borderRadius).length === 0) delete config.theme.extend.borderRadius;
|
|
759
|
+
if (Object.keys(config.theme.extend.fontSize).length === 0) delete config.theme.extend.fontSize;
|
|
760
|
+
return `/** @type {import('tailwindcss').Config} */
|
|
761
|
+
module.exports = ${JSON.stringify(config, null, 2)};`;
|
|
762
|
+
}
|
|
763
|
+
|
|
12
764
|
// src/bin/hotreload.ts
|
|
13
765
|
import { createHash } from "crypto";
|
|
14
766
|
var HotReloadServer = class {
|
|
@@ -91,6 +843,7 @@ function printHelp() {
|
|
|
91
843
|
Usage:
|
|
92
844
|
tokvista [tokens.json] [--config tokvista.config.ts] [--port 3000] [--no-open]
|
|
93
845
|
tokvista init [--force] [--port 3000] [--no-open] [--no-preview]
|
|
846
|
+
tokvista export <tokens.json> --format <css|scss|json|tailwind> [--output <file>]
|
|
94
847
|
|
|
95
848
|
Arguments:
|
|
96
849
|
tokens.json Path to your tokens file (overrides config.tokens)
|
|
@@ -99,7 +852,10 @@ Options:
|
|
|
99
852
|
-c, --config Path to TokVista config file
|
|
100
853
|
-f, --force Overwrite existing tokvista.config.ts (init only)
|
|
101
854
|
-p, --port Preferred server port (default: 3000)
|
|
855
|
+
--format Export format: css, scss, json, tailwind (export only)
|
|
856
|
+
--output, -o Output file path (export only)
|
|
102
857
|
--no-open Do not automatically open the browser
|
|
858
|
+
--no-watch Disable live reload (serve only)
|
|
103
859
|
--no-preview Skip starting live preview after init
|
|
104
860
|
-h, --help Show this help message
|
|
105
861
|
`);
|
|
@@ -211,8 +967,62 @@ function parseArgs(args) {
|
|
|
211
967
|
if (args[0] === "init") {
|
|
212
968
|
return parseInitArgs(args.slice(1));
|
|
213
969
|
}
|
|
970
|
+
if (args[0] === "export") {
|
|
971
|
+
return parseExportArgs(args.slice(1));
|
|
972
|
+
}
|
|
214
973
|
return parseServeArgs(args);
|
|
215
974
|
}
|
|
975
|
+
function parseExportArgs(args) {
|
|
976
|
+
let tokenFileArg;
|
|
977
|
+
let format;
|
|
978
|
+
let output;
|
|
979
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
980
|
+
const arg = args[index];
|
|
981
|
+
if (arg === "-h" || arg === "--help") {
|
|
982
|
+
printHelp();
|
|
983
|
+
process.exit(0);
|
|
984
|
+
}
|
|
985
|
+
if (arg === "--format") {
|
|
986
|
+
const next = args[index + 1];
|
|
987
|
+
if (!next) throw new Error("Missing value for --format");
|
|
988
|
+
if (!["css", "scss", "json", "tailwind"].includes(next)) {
|
|
989
|
+
throw new Error("Format must be: css, scss, json, or tailwind");
|
|
990
|
+
}
|
|
991
|
+
format = next;
|
|
992
|
+
index += 1;
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
if (arg === "--output" || arg === "-o") {
|
|
996
|
+
const next = args[index + 1];
|
|
997
|
+
if (!next) throw new Error("Missing value for --output");
|
|
998
|
+
output = next;
|
|
999
|
+
index += 1;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
if (arg.startsWith("--format=")) {
|
|
1003
|
+
const val = arg.slice("--format=".length);
|
|
1004
|
+
if (!["css", "scss", "json", "tailwind"].includes(val)) {
|
|
1005
|
+
throw new Error("Format must be: css, scss, json, or tailwind");
|
|
1006
|
+
}
|
|
1007
|
+
format = val;
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (arg.startsWith("--output=")) {
|
|
1011
|
+
output = arg.slice("--output=".length);
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
if (arg.startsWith("-")) {
|
|
1015
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1016
|
+
}
|
|
1017
|
+
if (tokenFileArg) {
|
|
1018
|
+
throw new Error(`Only one token file is supported. Unexpected value: "${arg}"`);
|
|
1019
|
+
}
|
|
1020
|
+
tokenFileArg = arg;
|
|
1021
|
+
}
|
|
1022
|
+
if (!tokenFileArg) throw new Error("Token file is required for export");
|
|
1023
|
+
if (!format) throw new Error("--format is required (css, scss, json, or tailwind)");
|
|
1024
|
+
return { command: "export", tokenFileArg, format, output };
|
|
1025
|
+
}
|
|
216
1026
|
function formatTitleFromPackageName(packageName) {
|
|
217
1027
|
const trimmed = packageName.trim();
|
|
218
1028
|
if (!trimmed) return "My Design System";
|
|
@@ -801,6 +1611,40 @@ Received ${signal}, shutting down TokVista...`);
|
|
|
801
1611
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
802
1612
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
803
1613
|
}
|
|
1614
|
+
async function runExportCommand(cwd, options) {
|
|
1615
|
+
const resolvedTokenPath = path.resolve(cwd, options.tokenFileArg);
|
|
1616
|
+
if (!existsSync(resolvedTokenPath)) {
|
|
1617
|
+
throw new Error(`Token file not found: ${resolvedTokenPath}`);
|
|
1618
|
+
}
|
|
1619
|
+
const tokens = await readTokens(resolvedTokenPath);
|
|
1620
|
+
let output;
|
|
1621
|
+
let defaultFilename;
|
|
1622
|
+
switch (options.format) {
|
|
1623
|
+
case "css":
|
|
1624
|
+
output = generateCSS(tokens);
|
|
1625
|
+
defaultFilename = "tokens.css";
|
|
1626
|
+
break;
|
|
1627
|
+
case "scss":
|
|
1628
|
+
output = generateSCSS(tokens);
|
|
1629
|
+
defaultFilename = "_tokens.scss";
|
|
1630
|
+
break;
|
|
1631
|
+
case "json":
|
|
1632
|
+
output = generateJS(tokens);
|
|
1633
|
+
defaultFilename = "tokens.js";
|
|
1634
|
+
break;
|
|
1635
|
+
case "tailwind":
|
|
1636
|
+
output = generateTailwind(tokens);
|
|
1637
|
+
defaultFilename = "tailwind.config.js";
|
|
1638
|
+
break;
|
|
1639
|
+
}
|
|
1640
|
+
if (options.output) {
|
|
1641
|
+
const outputPath = path.resolve(cwd, options.output);
|
|
1642
|
+
await writeFile(outputPath, output, "utf8");
|
|
1643
|
+
console.log(`Exported ${options.format} to ${outputPath}`);
|
|
1644
|
+
} else {
|
|
1645
|
+
console.log(output);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
804
1648
|
async function main() {
|
|
805
1649
|
try {
|
|
806
1650
|
const options = parseArgs(process.argv.slice(2));
|
|
@@ -812,6 +1656,10 @@ async function main() {
|
|
|
812
1656
|
}
|
|
813
1657
|
return;
|
|
814
1658
|
}
|
|
1659
|
+
if (options.command === "export") {
|
|
1660
|
+
await runExportCommand(cwd, options);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
815
1663
|
await runServeCommand(cwd, options);
|
|
816
1664
|
} catch (error) {
|
|
817
1665
|
console.error(error.message);
|