@vizejs/vite-plugin-musea 0.11.0 → 0.12.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/dist/{a11y-Bvx5TJb8.d.ts → a11y-cQIJXM5k.d.ts} +2 -2
- package/dist/{a11y-Bvx5TJb8.d.ts.map → a11y-cQIJXM5k.d.ts.map} +1 -1
- package/dist/a11y.d.ts +2 -2
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +50 -40
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2144 -2015
- package/dist/index.js.map +1 -1
- package/dist/{vrt-Vb4aqPZE.d.ts → vrt-BuMkTrLK.d.ts} +22 -2
- package/dist/vrt-BuMkTrLK.d.ts.map +1 -0
- package/dist/{vrt-DP87vGIA.js → vrt-CrjRhMVE.js} +126 -97
- package/dist/vrt-CrjRhMVE.js.map +1 -0
- package/dist/vrt.d.ts +1 -1
- package/dist/vrt.js +1 -1
- package/package.json +4 -4
- package/dist/vrt-DP87vGIA.js.map +0 -1
- package/dist/vrt-Vb4aqPZE.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1660 +1,210 @@
|
|
|
1
1
|
import { MuseaA11yRunner } from "./a11y-C6xqILwZ.js";
|
|
2
2
|
import { generateArtFile, writeArtFile } from "./autogen-ymQnARZK.js";
|
|
3
|
-
import { MuseaVrtRunner, generateVrtJsonReport, generateVrtReport } from "./vrt-
|
|
3
|
+
import { MuseaVrtRunner, generateVrtJsonReport, generateVrtReport } from "./vrt-CrjRhMVE.js";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import { vizeConfigStore } from "@vizejs/vite-plugin";
|
|
8
8
|
|
|
9
|
-
//#region src/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return flattenTokens(tokens);
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Parse tokens from a directory.
|
|
23
|
-
*/
|
|
24
|
-
async function parseTokenDirectory(dirPath) {
|
|
25
|
-
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
26
|
-
const categories = [];
|
|
27
|
-
for (const entry of entries) if (entry.isFile() && (entry.name.endsWith(".json") || entry.name.endsWith(".tokens.json"))) {
|
|
28
|
-
const filePath = path.join(dirPath, entry.name);
|
|
29
|
-
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
30
|
-
const tokens = JSON.parse(content);
|
|
31
|
-
const categoryName = path.basename(entry.name, path.extname(entry.name)).replace(".tokens", "");
|
|
32
|
-
categories.push({
|
|
33
|
-
name: formatCategoryName(categoryName),
|
|
34
|
-
tokens: extractTokens(tokens),
|
|
35
|
-
subcategories: extractSubcategories(tokens)
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
return categories;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Flatten nested token structure into categories.
|
|
42
|
-
*/
|
|
43
|
-
function flattenTokens(tokens, prefix = []) {
|
|
44
|
-
const categories = [];
|
|
45
|
-
for (const [key, value] of Object.entries(tokens)) {
|
|
46
|
-
if (isTokenValue(value)) continue;
|
|
47
|
-
if (typeof value === "object" && value !== null) {
|
|
48
|
-
const categoryTokens = extractTokens(value);
|
|
49
|
-
const subcategories = flattenTokens(value, [...prefix, key]);
|
|
50
|
-
if (Object.keys(categoryTokens).length > 0 || subcategories.length > 0) categories.push({
|
|
51
|
-
name: formatCategoryName(key),
|
|
52
|
-
tokens: categoryTokens,
|
|
53
|
-
subcategories: subcategories.length > 0 ? subcategories : void 0
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return categories;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Extract token values from an object.
|
|
61
|
-
*/
|
|
62
|
-
function extractTokens(obj) {
|
|
63
|
-
const tokens = {};
|
|
64
|
-
for (const [key, value] of Object.entries(obj)) if (isTokenValue(value)) tokens[key] = normalizeToken(value);
|
|
65
|
-
return tokens;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Extract subcategories from an object.
|
|
69
|
-
*/
|
|
70
|
-
function extractSubcategories(obj) {
|
|
71
|
-
const subcategories = [];
|
|
72
|
-
for (const [key, value] of Object.entries(obj)) if (!isTokenValue(value) && typeof value === "object" && value !== null) {
|
|
73
|
-
const categoryTokens = extractTokens(value);
|
|
74
|
-
const nested = extractSubcategories(value);
|
|
75
|
-
if (Object.keys(categoryTokens).length > 0 || nested && nested.length > 0) subcategories.push({
|
|
76
|
-
name: formatCategoryName(key),
|
|
77
|
-
tokens: categoryTokens,
|
|
78
|
-
subcategories: nested
|
|
79
|
-
});
|
|
9
|
+
//#region src/native-loader.ts
|
|
10
|
+
let native = null;
|
|
11
|
+
function loadNative() {
|
|
12
|
+
if (native) return native;
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
try {
|
|
15
|
+
native = require("@vizejs/native");
|
|
16
|
+
return native;
|
|
17
|
+
} catch (e) {
|
|
18
|
+
throw new Error(`Failed to load @vizejs/native. Make sure it's installed and built:\n${String(e)}`);
|
|
80
19
|
}
|
|
81
|
-
return subcategories.length > 0 ? subcategories : void 0;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Check if a value is a token definition.
|
|
85
|
-
*/
|
|
86
|
-
function isTokenValue(value) {
|
|
87
|
-
if (typeof value !== "object" || value === null) return false;
|
|
88
|
-
const obj = value;
|
|
89
|
-
return "value" in obj && (typeof obj.value === "string" || typeof obj.value === "number") || "$value" in obj && (typeof obj.$value === "string" || typeof obj.$value === "number");
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Normalize token to DesignToken interface.
|
|
93
|
-
*/
|
|
94
|
-
function normalizeToken(raw) {
|
|
95
|
-
const token = {
|
|
96
|
-
value: raw.value ?? raw.$value,
|
|
97
|
-
type: raw.type ?? raw.$type,
|
|
98
|
-
description: raw.description,
|
|
99
|
-
attributes: raw.attributes
|
|
100
|
-
};
|
|
101
|
-
if (raw.$tier === "primitive" || raw.$tier === "semantic") token.$tier = raw.$tier;
|
|
102
|
-
if (typeof raw.$reference === "string") token.$reference = raw.$reference;
|
|
103
|
-
return token;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Format category name for display.
|
|
107
|
-
*/
|
|
108
|
-
function formatCategoryName(name) {
|
|
109
|
-
return name.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
|
|
110
20
|
}
|
|
111
21
|
/**
|
|
112
|
-
*
|
|
22
|
+
* JS-based fallback for SFC analysis when native `analyzeSfc` is not available.
|
|
23
|
+
* Uses regex parsing to extract props and emits from Vue SFC source.
|
|
113
24
|
*/
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
25
|
+
function analyzeSfcFallback(source, _options) {
|
|
26
|
+
try {
|
|
27
|
+
const props = [];
|
|
28
|
+
const emits = [];
|
|
29
|
+
const scriptSetupMatch = source.match(/<script\s+[^>]*setup[^>]*>([\s\S]*?)<\/script>/);
|
|
30
|
+
if (!scriptSetupMatch) {
|
|
31
|
+
const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
32
|
+
if (!scriptMatch) return {
|
|
33
|
+
props: [],
|
|
34
|
+
emits: []
|
|
35
|
+
};
|
|
122
36
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
37
|
+
const scriptContent = scriptSetupMatch?.[1] || "";
|
|
38
|
+
const propsMatch = scriptContent.match(/defineProps\s*<\s*\{([\s\S]*?)\}>\s*\(/);
|
|
39
|
+
const propsMatch2 = scriptContent.match(/defineProps\s*<\s*\{([\s\S]*?)\}>/);
|
|
40
|
+
const propsBody = propsMatch?.[1] || propsMatch2?.[1];
|
|
41
|
+
if (propsBody) {
|
|
42
|
+
const lines = propsBody.split("\n");
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < lines.length) {
|
|
45
|
+
const line = lines[i].trim();
|
|
46
|
+
if (line.startsWith("/**") || line.startsWith("*") || line.startsWith("*/")) {
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const propMatch = line.match(/^(\w+)(\?)?:\s*(.+?)(?:;?\s*)$/);
|
|
51
|
+
if (propMatch) {
|
|
52
|
+
const name = propMatch[1];
|
|
53
|
+
const optional = !!propMatch[2];
|
|
54
|
+
let type = propMatch[3].replace(/;$/, "").trim();
|
|
55
|
+
const defaultPattern = new RegExp(`\\b${name}\\s*=\\s*([^,}\\n]+)`);
|
|
56
|
+
const defaultMatch = scriptContent.match(defaultPattern);
|
|
57
|
+
const defaultValue = defaultMatch ? defaultMatch[1].trim() : void 0;
|
|
58
|
+
props.push({
|
|
59
|
+
name,
|
|
60
|
+
type,
|
|
61
|
+
required: !optional && defaultValue === void 0,
|
|
62
|
+
...defaultValue !== void 0 ? { default_value: defaultValue } : {}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
126
67
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
* Resolve references in categories, setting $tier, $reference, and $resolvedValue.
|
|
134
|
-
*/
|
|
135
|
-
function resolveReferences(categories, tokenMap) {
|
|
136
|
-
for (const cat of categories) {
|
|
137
|
-
for (const token of Object.values(cat.tokens)) resolveTokenReference(token, tokenMap);
|
|
138
|
-
if (cat.subcategories) resolveReferences(cat.subcategories, tokenMap);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function resolveTokenReference(token, tokenMap) {
|
|
142
|
-
if (typeof token.value === "string") {
|
|
143
|
-
const match = token.value.match(REFERENCE_PATTERN);
|
|
144
|
-
if (match) {
|
|
145
|
-
token.$tier = token.$tier ?? "semantic";
|
|
146
|
-
token.$reference = match[1];
|
|
147
|
-
token.$resolvedValue = resolveValue(match[1], tokenMap, 0, new Set());
|
|
148
|
-
return;
|
|
68
|
+
const emitsMatch = scriptContent.match(/defineEmits\s*<\s*\{([\s\S]*?)\}>/);
|
|
69
|
+
if (emitsMatch) {
|
|
70
|
+
const emitsBody = emitsMatch[1];
|
|
71
|
+
const emitRegex = /(\w+)\s*:/g;
|
|
72
|
+
let match;
|
|
73
|
+
while ((match = emitRegex.exec(emitsBody)) !== null) emits.push(match[1]);
|
|
149
74
|
}
|
|
75
|
+
return {
|
|
76
|
+
props,
|
|
77
|
+
emits
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return {
|
|
81
|
+
props: [],
|
|
82
|
+
emits: []
|
|
83
|
+
};
|
|
150
84
|
}
|
|
151
|
-
token.$tier = token.$tier ?? "primitive";
|
|
152
|
-
}
|
|
153
|
-
function resolveValue(ref, tokenMap, depth, visited) {
|
|
154
|
-
if (depth >= MAX_RESOLVE_DEPTH || visited.has(ref)) return void 0;
|
|
155
|
-
visited.add(ref);
|
|
156
|
-
const target = tokenMap[ref];
|
|
157
|
-
if (!target) return void 0;
|
|
158
|
-
if (typeof target.value === "string") {
|
|
159
|
-
const match = target.value.match(REFERENCE_PATTERN);
|
|
160
|
-
if (match) return resolveValue(match[1], tokenMap, depth + 1, visited);
|
|
161
|
-
}
|
|
162
|
-
return target.value;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Read raw JSON token file.
|
|
166
|
-
*/
|
|
167
|
-
async function readRawTokenFile(tokensPath) {
|
|
168
|
-
const content = await fs.promises.readFile(tokensPath, "utf-8");
|
|
169
|
-
return JSON.parse(content);
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Write raw JSON token file atomically (write tmp, rename).
|
|
173
|
-
*/
|
|
174
|
-
async function writeRawTokenFile(tokensPath, data) {
|
|
175
|
-
const tmpPath = tokensPath + ".tmp";
|
|
176
|
-
await fs.promises.writeFile(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
177
|
-
await fs.promises.rename(tmpPath, tokensPath);
|
|
178
85
|
}
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/gallery.ts
|
|
179
89
|
/**
|
|
180
|
-
*
|
|
90
|
+
* Gallery HTML generation for the Musea component gallery.
|
|
91
|
+
*
|
|
92
|
+
* Contains the inline gallery SPA template (used as a fallback when the
|
|
93
|
+
* pre-built gallery is not available) and the gallery virtual module.
|
|
181
94
|
*/
|
|
182
|
-
function setTokenAtPath(data, dotPath, token) {
|
|
183
|
-
const parts = dotPath.split(".");
|
|
184
|
-
let current = data;
|
|
185
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
186
|
-
const key = parts[i];
|
|
187
|
-
if (typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
188
|
-
current = current[key];
|
|
189
|
-
}
|
|
190
|
-
const leafKey = parts[parts.length - 1];
|
|
191
|
-
const raw = { value: token.value };
|
|
192
|
-
if (token.type) raw.type = token.type;
|
|
193
|
-
if (token.description) raw.description = token.description;
|
|
194
|
-
if (token.$tier) raw.$tier = token.$tier;
|
|
195
|
-
if (token.$reference) raw.$reference = token.$reference;
|
|
196
|
-
if (token.attributes) raw.attributes = token.attributes;
|
|
197
|
-
current[leafKey] = raw;
|
|
198
|
-
}
|
|
199
95
|
/**
|
|
200
|
-
*
|
|
96
|
+
* Generate the inline gallery HTML page.
|
|
201
97
|
*/
|
|
202
|
-
function
|
|
203
|
-
const
|
|
204
|
-
const parents = [];
|
|
205
|
-
let current = data;
|
|
206
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
207
|
-
const key = parts[i];
|
|
208
|
-
if (typeof current[key] !== "object" || current[key] === null) return false;
|
|
209
|
-
parents.push({
|
|
210
|
-
obj: current,
|
|
211
|
-
key
|
|
212
|
-
});
|
|
213
|
-
current = current[key];
|
|
214
|
-
}
|
|
215
|
-
const leafKey = parts[parts.length - 1];
|
|
216
|
-
if (!(leafKey in current)) return false;
|
|
217
|
-
delete current[leafKey];
|
|
218
|
-
for (let i = parents.length - 1; i >= 0; i--) {
|
|
219
|
-
const { obj, key } = parents[i];
|
|
220
|
-
const child = obj[key];
|
|
221
|
-
if (Object.keys(child).length === 0) delete obj[key];
|
|
222
|
-
else break;
|
|
223
|
-
}
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Validate that a semantic reference points to an existing token and has no cycles.
|
|
228
|
-
*/
|
|
229
|
-
function validateSemanticReference(tokenMap, reference, selfPath) {
|
|
230
|
-
if (!tokenMap[reference]) return {
|
|
231
|
-
valid: false,
|
|
232
|
-
error: `Reference target "${reference}" does not exist`
|
|
233
|
-
};
|
|
234
|
-
const visited = new Set();
|
|
235
|
-
if (selfPath) visited.add(selfPath);
|
|
236
|
-
let current = reference;
|
|
237
|
-
let depth = 0;
|
|
238
|
-
while (depth < MAX_RESOLVE_DEPTH) {
|
|
239
|
-
if (visited.has(current)) return {
|
|
240
|
-
valid: false,
|
|
241
|
-
error: `Circular reference detected at "${current}"`
|
|
242
|
-
};
|
|
243
|
-
visited.add(current);
|
|
244
|
-
const target = tokenMap[current];
|
|
245
|
-
if (!target) break;
|
|
246
|
-
if (typeof target.value === "string") {
|
|
247
|
-
const match = target.value.match(REFERENCE_PATTERN);
|
|
248
|
-
if (match) {
|
|
249
|
-
current = match[1];
|
|
250
|
-
depth++;
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
if (depth >= MAX_RESOLVE_DEPTH) return {
|
|
257
|
-
valid: false,
|
|
258
|
-
error: "Reference chain too deep (max 10)"
|
|
259
|
-
};
|
|
260
|
-
return { valid: true };
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Find all tokens that reference the given path.
|
|
264
|
-
*/
|
|
265
|
-
function findDependentTokens(tokenMap, targetPath) {
|
|
266
|
-
const dependents = [];
|
|
267
|
-
for (const [path$1, token] of Object.entries(tokenMap)) if (typeof token.value === "string") {
|
|
268
|
-
const match = token.value.match(REFERENCE_PATTERN);
|
|
269
|
-
if (match && match[1] === targetPath) dependents.push(path$1);
|
|
270
|
-
}
|
|
271
|
-
return dependents;
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Generate HTML documentation for tokens.
|
|
275
|
-
*/
|
|
276
|
-
function generateTokensHtml(categories) {
|
|
277
|
-
const renderToken = (name, token) => {
|
|
278
|
-
const isColor = typeof token.value === "string" && (token.value.startsWith("#") || token.value.startsWith("rgb") || token.value.startsWith("hsl") || token.type === "color");
|
|
279
|
-
return `
|
|
280
|
-
<div class="token">
|
|
281
|
-
<div class="token-preview">
|
|
282
|
-
${isColor ? `<div class="color-swatch" style="background: ${token.value}"></div>` : ""}
|
|
283
|
-
</div>
|
|
284
|
-
<div class="token-info">
|
|
285
|
-
<div class="token-name">${name}</div>
|
|
286
|
-
<div class="token-value">${token.value}</div>
|
|
287
|
-
${token.description ? `<div class="token-description">${token.description}</div>` : ""}
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
`;
|
|
291
|
-
};
|
|
292
|
-
const renderCategory = (category, level = 2) => {
|
|
293
|
-
const heading = `h${Math.min(level, 6)}`;
|
|
294
|
-
let html = `<${heading}>${category.name}</${heading}>`;
|
|
295
|
-
html += "<div class=\"tokens-grid\">";
|
|
296
|
-
for (const [name, token] of Object.entries(category.tokens)) html += renderToken(name, token);
|
|
297
|
-
html += "</div>";
|
|
298
|
-
if (category.subcategories) for (const sub of category.subcategories) html += renderCategory(sub, level + 1);
|
|
299
|
-
return html;
|
|
300
|
-
};
|
|
98
|
+
function generateGalleryHtml(basePath, themeConfig) {
|
|
99
|
+
const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : "";
|
|
301
100
|
return `<!DOCTYPE html>
|
|
302
101
|
<html lang="en">
|
|
303
102
|
<head>
|
|
304
103
|
<meta charset="UTF-8">
|
|
305
104
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
306
|
-
<title>
|
|
105
|
+
<title>Musea - Component Gallery</title>
|
|
106
|
+
<script>window.__MUSEA_BASE_PATH__='${basePath}';${themeScript}</script>
|
|
307
107
|
<style>
|
|
308
108
|
:root {
|
|
309
|
-
--musea-bg: #
|
|
310
|
-
--musea-bg-secondary: #
|
|
311
|
-
--musea-
|
|
312
|
-
--musea-
|
|
313
|
-
--musea-accent: #
|
|
314
|
-
--musea-
|
|
109
|
+
--musea-bg-primary: #E6E2D6;
|
|
110
|
+
--musea-bg-secondary: #ddd9cd;
|
|
111
|
+
--musea-bg-tertiary: #d4d0c4;
|
|
112
|
+
--musea-bg-elevated: #E6E2D6;
|
|
113
|
+
--musea-accent: #121212;
|
|
114
|
+
--musea-accent-hover: #2a2a2a;
|
|
115
|
+
--musea-accent-subtle: rgba(18, 18, 18, 0.08);
|
|
116
|
+
--musea-text: #121212;
|
|
117
|
+
--musea-text-secondary: #3a3a3a;
|
|
118
|
+
--musea-text-muted: #6b6b6b;
|
|
119
|
+
--musea-border: #c8c4b8;
|
|
120
|
+
--musea-border-subtle: #d4d0c4;
|
|
121
|
+
--musea-success: #16a34a;
|
|
122
|
+
--musea-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
|
123
|
+
--musea-radius-sm: 4px;
|
|
124
|
+
--musea-radius-md: 6px;
|
|
125
|
+
--musea-radius-lg: 8px;
|
|
126
|
+
--musea-transition: 0.15s ease;
|
|
315
127
|
}
|
|
128
|
+
|
|
316
129
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
130
|
+
|
|
317
131
|
body {
|
|
318
|
-
font-family: '
|
|
319
|
-
background: var(--musea-bg);
|
|
132
|
+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
133
|
+
background: var(--musea-bg-primary);
|
|
320
134
|
color: var(--musea-text);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
h1 { margin-bottom: 2rem; color: var(--musea-accent); }
|
|
325
|
-
h2 { margin: 2rem 0 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--musea-border); }
|
|
326
|
-
h3, h4, h5, h6 { margin: 1.5rem 0 0.75rem; }
|
|
327
|
-
.tokens-grid {
|
|
328
|
-
display: grid;
|
|
329
|
-
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
330
|
-
gap: 1rem;
|
|
331
|
-
margin-bottom: 1.5rem;
|
|
135
|
+
min-height: 100vh;
|
|
136
|
+
line-height: 1.5;
|
|
137
|
+
-webkit-font-smoothing: antialiased;
|
|
332
138
|
}
|
|
333
|
-
|
|
139
|
+
|
|
140
|
+
/* Header */
|
|
141
|
+
.header {
|
|
334
142
|
background: var(--musea-bg-secondary);
|
|
335
|
-
border: 1px solid var(--musea-border);
|
|
336
|
-
|
|
337
|
-
|
|
143
|
+
border-bottom: 1px solid var(--musea-border);
|
|
144
|
+
padding: 0 1.5rem;
|
|
145
|
+
height: 56px;
|
|
338
146
|
display: flex;
|
|
339
|
-
gap: 1rem;
|
|
340
147
|
align-items: center;
|
|
148
|
+
justify-content: space-between;
|
|
149
|
+
position: sticky;
|
|
150
|
+
top: 0;
|
|
151
|
+
z-index: 100;
|
|
341
152
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
width: 48px;
|
|
345
|
-
height: 48px;
|
|
153
|
+
|
|
154
|
+
.header-left {
|
|
346
155
|
display: flex;
|
|
347
156
|
align-items: center;
|
|
348
|
-
|
|
157
|
+
gap: 1.5rem;
|
|
349
158
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
159
|
+
|
|
160
|
+
.logo {
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
gap: 0.5rem;
|
|
164
|
+
font-size: 1.125rem;
|
|
165
|
+
font-weight: 700;
|
|
166
|
+
color: var(--musea-accent);
|
|
167
|
+
text-decoration: none;
|
|
355
168
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
169
|
+
|
|
170
|
+
.logo-svg {
|
|
171
|
+
width: 32px;
|
|
172
|
+
height: 32px;
|
|
173
|
+
flex-shrink: 0;
|
|
359
174
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
175
|
+
|
|
176
|
+
.logo-icon svg {
|
|
177
|
+
width: 16px;
|
|
178
|
+
height: 16px;
|
|
179
|
+
color: var(--musea-text);
|
|
364
180
|
}
|
|
365
|
-
|
|
181
|
+
|
|
182
|
+
.header-subtitle {
|
|
366
183
|
color: var(--musea-text-muted);
|
|
367
|
-
font-
|
|
368
|
-
font-
|
|
369
|
-
|
|
184
|
+
font-size: 0.8125rem;
|
|
185
|
+
font-weight: 500;
|
|
186
|
+
padding-left: 1.5rem;
|
|
187
|
+
border-left: 1px solid var(--musea-border);
|
|
370
188
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
let md = `\n${heading} ${category.name}\n\n`;
|
|
391
|
-
if (Object.keys(category.tokens).length > 0) {
|
|
392
|
-
md += "| Token | Value | Description |\n";
|
|
393
|
-
md += "|-------|-------|-------------|\n";
|
|
394
|
-
for (const [name, token] of Object.entries(category.tokens)) {
|
|
395
|
-
const desc = token.description || "-";
|
|
396
|
-
md += `| \`${name}\` | \`${token.value}\` | ${desc} |\n`;
|
|
397
|
-
}
|
|
398
|
-
md += "\n";
|
|
399
|
-
}
|
|
400
|
-
if (category.subcategories) for (const sub of category.subcategories) md += renderCategory(sub, level + 1);
|
|
401
|
-
return md;
|
|
402
|
-
};
|
|
403
|
-
let markdown = "# Design Tokens\n\n";
|
|
404
|
-
markdown += `> Generated by Musea on ${new Date().toISOString()}\n`;
|
|
405
|
-
for (const category of categories) markdown += renderCategory(category);
|
|
406
|
-
return markdown;
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Style Dictionary plugin for Musea.
|
|
410
|
-
*/
|
|
411
|
-
async function processStyleDictionary(config) {
|
|
412
|
-
const categories = await parseTokens(config.tokensPath);
|
|
413
|
-
const outputDir = config.outputDir ?? ".vize/tokens";
|
|
414
|
-
const outputFormat = config.outputFormat ?? "html";
|
|
415
|
-
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
416
|
-
let content;
|
|
417
|
-
let filename;
|
|
418
|
-
switch (outputFormat) {
|
|
419
|
-
case "html":
|
|
420
|
-
content = generateTokensHtml(categories);
|
|
421
|
-
filename = "tokens.html";
|
|
422
|
-
break;
|
|
423
|
-
case "markdown":
|
|
424
|
-
content = generateTokensMarkdown(categories);
|
|
425
|
-
filename = "tokens.md";
|
|
426
|
-
break;
|
|
427
|
-
case "json":
|
|
428
|
-
default:
|
|
429
|
-
content = JSON.stringify({ categories }, null, 2);
|
|
430
|
-
filename = "tokens.json";
|
|
431
|
-
}
|
|
432
|
-
const outputPath = path.join(outputDir, filename);
|
|
433
|
-
await fs.promises.writeFile(outputPath, content, "utf-8");
|
|
434
|
-
console.log(`[musea] Generated token documentation: ${outputPath}`);
|
|
435
|
-
return {
|
|
436
|
-
categories,
|
|
437
|
-
metadata: {
|
|
438
|
-
name: path.basename(config.tokensPath),
|
|
439
|
-
generatedAt: new Date().toISOString()
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Normalize a token value for comparison.
|
|
445
|
-
* - Lowercase, trim
|
|
446
|
-
* - Leading-zero: `.5rem` → `0.5rem`
|
|
447
|
-
* - Short hex: `#fff` → `#ffffff`
|
|
448
|
-
*/
|
|
449
|
-
function normalizeTokenValue(value) {
|
|
450
|
-
let v = String(value).trim().toLowerCase();
|
|
451
|
-
const shortHex = v.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$/);
|
|
452
|
-
if (shortHex) {
|
|
453
|
-
const [, r, g, b, a] = shortHex;
|
|
454
|
-
v = a ? `#${r}${r}${g}${g}${b}${b}${a}${a}` : `#${r}${r}${g}${g}${b}${b}`;
|
|
455
|
-
}
|
|
456
|
-
v = v.replace(/(?<![0-9])\.(\d)/g, "0.$1");
|
|
457
|
-
return v;
|
|
458
|
-
}
|
|
459
|
-
const STYLE_BLOCK_RE = /<style[^>]*>([\s\S]*?)<\/style>/g;
|
|
460
|
-
const CSS_PROPERTY_RE = /^\s*([\w-]+)\s*:\s*(.+?)\s*;?\s*$/;
|
|
461
|
-
/**
|
|
462
|
-
* Scan art file sources for token value matches in `<style>` blocks.
|
|
463
|
-
*/
|
|
464
|
-
function scanTokenUsage(artFiles, tokenMap) {
|
|
465
|
-
const valueLookup = new Map();
|
|
466
|
-
for (const [tokenPath, token] of Object.entries(tokenMap)) {
|
|
467
|
-
const rawValue = token.$resolvedValue ?? token.value;
|
|
468
|
-
const normalized = normalizeTokenValue(rawValue);
|
|
469
|
-
if (!normalized) continue;
|
|
470
|
-
const existing = valueLookup.get(normalized);
|
|
471
|
-
if (existing) existing.push(tokenPath);
|
|
472
|
-
else valueLookup.set(normalized, [tokenPath]);
|
|
473
|
-
}
|
|
474
|
-
const usageMap = {};
|
|
475
|
-
for (const [artPath, artInfo] of artFiles) {
|
|
476
|
-
let source;
|
|
477
|
-
try {
|
|
478
|
-
source = fs.readFileSync(artPath, "utf-8");
|
|
479
|
-
} catch {
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
const allLines = source.split("\n");
|
|
483
|
-
const styleRegions = [];
|
|
484
|
-
let match;
|
|
485
|
-
STYLE_BLOCK_RE.lastIndex = 0;
|
|
486
|
-
while ((match = STYLE_BLOCK_RE.exec(source)) !== null) {
|
|
487
|
-
const beforeMatch = source.slice(0, match.index);
|
|
488
|
-
const startTag = source.slice(match.index, match.index + match[0].indexOf(match[1]));
|
|
489
|
-
const startLine = beforeMatch.split("\n").length + startTag.split("\n").length - 1;
|
|
490
|
-
styleRegions.push({
|
|
491
|
-
startLine,
|
|
492
|
-
content: match[1]
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
for (const region of styleRegions) {
|
|
496
|
-
const lines = region.content.split("\n");
|
|
497
|
-
for (let i = 0; i < lines.length; i++) {
|
|
498
|
-
const line = lines[i];
|
|
499
|
-
const propMatch = line.match(CSS_PROPERTY_RE);
|
|
500
|
-
if (!propMatch) continue;
|
|
501
|
-
const property = propMatch[1];
|
|
502
|
-
const valueStr = propMatch[2];
|
|
503
|
-
const valueParts = valueStr.split(/\s+/);
|
|
504
|
-
for (const part of valueParts) {
|
|
505
|
-
const normalizedPart = normalizeTokenValue(part);
|
|
506
|
-
const matchingTokens = valueLookup.get(normalizedPart);
|
|
507
|
-
if (!matchingTokens) continue;
|
|
508
|
-
const lineNumber = region.startLine + i;
|
|
509
|
-
const lineContent = allLines[lineNumber - 1]?.trim() ?? line.trim();
|
|
510
|
-
for (const tokenPath of matchingTokens) {
|
|
511
|
-
if (!usageMap[tokenPath]) usageMap[tokenPath] = [];
|
|
512
|
-
let entry = usageMap[tokenPath].find((e) => e.artPath === artPath);
|
|
513
|
-
if (!entry) {
|
|
514
|
-
entry = {
|
|
515
|
-
artPath,
|
|
516
|
-
artTitle: artInfo.metadata.title,
|
|
517
|
-
artCategory: artInfo.metadata.category,
|
|
518
|
-
matches: []
|
|
519
|
-
};
|
|
520
|
-
usageMap[tokenPath].push(entry);
|
|
521
|
-
}
|
|
522
|
-
if (!entry.matches.some((m) => m.line === lineNumber && m.property === property)) entry.matches.push({
|
|
523
|
-
line: lineNumber,
|
|
524
|
-
lineContent,
|
|
525
|
-
property
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
return usageMap;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
//#endregion
|
|
536
|
-
//#region src/index.ts
|
|
537
|
-
const VIRTUAL_MUSEA_PREFIX = "\0musea:";
|
|
538
|
-
const VIRTUAL_GALLERY = "\0musea-gallery";
|
|
539
|
-
const VIRTUAL_MANIFEST = "\0musea-manifest";
|
|
540
|
-
let native = null;
|
|
541
|
-
function loadNative() {
|
|
542
|
-
if (native) return native;
|
|
543
|
-
const require = createRequire(import.meta.url);
|
|
544
|
-
try {
|
|
545
|
-
native = require("@vizejs/native");
|
|
546
|
-
return native;
|
|
547
|
-
} catch (e) {
|
|
548
|
-
throw new Error(`Failed to load @vizejs/native. Make sure it's installed and built:\n${String(e)}`);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* JS-based fallback for SFC analysis when native `analyzeSfc` is not available.
|
|
553
|
-
* Uses regex parsing to extract props and emits from Vue SFC source.
|
|
554
|
-
*/
|
|
555
|
-
function analyzeSfcFallback(source, _options) {
|
|
556
|
-
try {
|
|
557
|
-
const props = [];
|
|
558
|
-
const emits = [];
|
|
559
|
-
const scriptSetupMatch = source.match(/<script\s+[^>]*setup[^>]*>([\s\S]*?)<\/script>/);
|
|
560
|
-
if (!scriptSetupMatch) {
|
|
561
|
-
const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
562
|
-
if (!scriptMatch) return {
|
|
563
|
-
props: [],
|
|
564
|
-
emits: []
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
const scriptContent = scriptSetupMatch?.[1] || "";
|
|
568
|
-
const propsMatch = scriptContent.match(/defineProps\s*<\s*\{([\s\S]*?)\}>\s*\(/);
|
|
569
|
-
const propsMatch2 = scriptContent.match(/defineProps\s*<\s*\{([\s\S]*?)\}>/);
|
|
570
|
-
const propsBody = propsMatch?.[1] || propsMatch2?.[1];
|
|
571
|
-
if (propsBody) {
|
|
572
|
-
const lines = propsBody.split("\n");
|
|
573
|
-
let i = 0;
|
|
574
|
-
while (i < lines.length) {
|
|
575
|
-
const line = lines[i].trim();
|
|
576
|
-
if (line.startsWith("/**") || line.startsWith("*") || line.startsWith("*/")) {
|
|
577
|
-
i++;
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
const propMatch = line.match(/^(\w+)(\?)?:\s*(.+?)(?:;?\s*)$/);
|
|
581
|
-
if (propMatch) {
|
|
582
|
-
const name = propMatch[1];
|
|
583
|
-
const optional = !!propMatch[2];
|
|
584
|
-
let type = propMatch[3].replace(/;$/, "").trim();
|
|
585
|
-
const defaultPattern = new RegExp(`\\b${name}\\s*=\\s*([^,}\\n]+)`);
|
|
586
|
-
const defaultMatch = scriptContent.match(defaultPattern);
|
|
587
|
-
const defaultValue = defaultMatch ? defaultMatch[1].trim() : void 0;
|
|
588
|
-
props.push({
|
|
589
|
-
name,
|
|
590
|
-
type,
|
|
591
|
-
required: !optional && defaultValue === void 0,
|
|
592
|
-
...defaultValue !== void 0 ? { default_value: defaultValue } : {}
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
i++;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
const emitsMatch = scriptContent.match(/defineEmits\s*<\s*\{([\s\S]*?)\}>/);
|
|
599
|
-
if (emitsMatch) {
|
|
600
|
-
const emitsBody = emitsMatch[1];
|
|
601
|
-
const emitRegex = /(\w+)\s*:/g;
|
|
602
|
-
let match;
|
|
603
|
-
while ((match = emitRegex.exec(emitsBody)) !== null) emits.push(match[1]);
|
|
604
|
-
}
|
|
605
|
-
return {
|
|
606
|
-
props,
|
|
607
|
-
emits
|
|
608
|
-
};
|
|
609
|
-
} catch {
|
|
610
|
-
return {
|
|
611
|
-
props: [],
|
|
612
|
-
emits: []
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
/**
|
|
617
|
-
* Build the theme config object from plugin options for runtime injection.
|
|
618
|
-
*/
|
|
619
|
-
function buildThemeConfig(theme) {
|
|
620
|
-
if (!theme) return void 0;
|
|
621
|
-
if (typeof theme === "string") return { default: theme };
|
|
622
|
-
const themes = Array.isArray(theme) ? theme : [theme];
|
|
623
|
-
const custom = {};
|
|
624
|
-
for (const t of themes) custom[t.name] = {
|
|
625
|
-
base: t.base,
|
|
626
|
-
colors: t.colors
|
|
627
|
-
};
|
|
628
|
-
return {
|
|
629
|
-
default: themes[0].name,
|
|
630
|
-
custom
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Create Musea Vite plugin.
|
|
635
|
-
*/
|
|
636
|
-
function musea(options = {}) {
|
|
637
|
-
let include = options.include ?? ["**/*.art.vue"];
|
|
638
|
-
let exclude = options.exclude ?? ["node_modules/**", "dist/**"];
|
|
639
|
-
let basePath = options.basePath ?? "/__musea__";
|
|
640
|
-
let storybookCompat = options.storybookCompat ?? false;
|
|
641
|
-
const storybookOutDir = options.storybookOutDir ?? ".storybook/stories";
|
|
642
|
-
let inlineArt = options.inlineArt ?? false;
|
|
643
|
-
const tokensPath = options.tokensPath;
|
|
644
|
-
const themeConfig = buildThemeConfig(options.theme);
|
|
645
|
-
const previewCss = options.previewCss ?? [];
|
|
646
|
-
const previewSetup = options.previewSetup;
|
|
647
|
-
let config;
|
|
648
|
-
let server = null;
|
|
649
|
-
const artFiles = new Map();
|
|
650
|
-
let resolvedPreviewCss = [];
|
|
651
|
-
let resolvedPreviewSetup = null;
|
|
652
|
-
const mainPlugin = {
|
|
653
|
-
name: "vite-plugin-musea",
|
|
654
|
-
enforce: "pre",
|
|
655
|
-
config() {
|
|
656
|
-
return { resolve: { alias: { vue: "vue/dist/vue.esm-bundler.js" } } };
|
|
657
|
-
},
|
|
658
|
-
configResolved(resolvedConfig) {
|
|
659
|
-
config = resolvedConfig;
|
|
660
|
-
const vizeConfig = vizeConfigStore.get(resolvedConfig.root);
|
|
661
|
-
if (vizeConfig?.musea) {
|
|
662
|
-
const mc = vizeConfig.musea;
|
|
663
|
-
if (!options.include && mc.include) include = mc.include;
|
|
664
|
-
if (!options.exclude && mc.exclude) exclude = mc.exclude;
|
|
665
|
-
if (!options.basePath && mc.basePath) basePath = mc.basePath;
|
|
666
|
-
if (options.storybookCompat === void 0 && mc.storybookCompat !== void 0) storybookCompat = mc.storybookCompat;
|
|
667
|
-
if (options.inlineArt === void 0 && mc.inlineArt !== void 0) inlineArt = mc.inlineArt;
|
|
668
|
-
}
|
|
669
|
-
resolvedPreviewCss = previewCss.map((cssPath) => path.isAbsolute(cssPath) ? cssPath : path.resolve(resolvedConfig.root, cssPath));
|
|
670
|
-
if (previewSetup) resolvedPreviewSetup = path.isAbsolute(previewSetup) ? previewSetup : path.resolve(resolvedConfig.root, previewSetup);
|
|
671
|
-
},
|
|
672
|
-
configureServer(devServer) {
|
|
673
|
-
server = devServer;
|
|
674
|
-
devServer.middlewares.use(basePath, async (req, res, next) => {
|
|
675
|
-
const url = req.url || "/";
|
|
676
|
-
if (url === "/" || url === "/index.html" || url.startsWith("/tokens") || url.startsWith("/component/") || url.startsWith("/tests")) {
|
|
677
|
-
const galleryDistDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "gallery");
|
|
678
|
-
const indexHtmlPath = path.join(galleryDistDir, "index.html");
|
|
679
|
-
try {
|
|
680
|
-
await fs.promises.access(indexHtmlPath);
|
|
681
|
-
let html = await fs.promises.readFile(indexHtmlPath, "utf-8");
|
|
682
|
-
const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : "";
|
|
683
|
-
html = html.replace("</head>", `<script>window.__MUSEA_BASE_PATH__='${basePath}';${themeScript}</script></head>`);
|
|
684
|
-
res.setHeader("Content-Type", "text/html");
|
|
685
|
-
res.end(html);
|
|
686
|
-
return;
|
|
687
|
-
} catch {
|
|
688
|
-
const html = generateGalleryHtml(basePath, themeConfig);
|
|
689
|
-
res.setHeader("Content-Type", "text/html");
|
|
690
|
-
res.end(html);
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
if (url.startsWith("/assets/")) {
|
|
695
|
-
const galleryDistDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "gallery");
|
|
696
|
-
const filePath = path.join(galleryDistDir, url);
|
|
697
|
-
try {
|
|
698
|
-
const stat = await fs.promises.stat(filePath);
|
|
699
|
-
if (stat.isFile()) {
|
|
700
|
-
const content = await fs.promises.readFile(filePath);
|
|
701
|
-
const ext = path.extname(filePath);
|
|
702
|
-
const mimeTypes = {
|
|
703
|
-
".js": "application/javascript",
|
|
704
|
-
".css": "text/css",
|
|
705
|
-
".svg": "image/svg+xml",
|
|
706
|
-
".png": "image/png",
|
|
707
|
-
".ico": "image/x-icon",
|
|
708
|
-
".woff2": "font/woff2",
|
|
709
|
-
".woff": "font/woff"
|
|
710
|
-
};
|
|
711
|
-
res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream");
|
|
712
|
-
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
713
|
-
res.end(content);
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
} catch {}
|
|
717
|
-
}
|
|
718
|
-
next();
|
|
719
|
-
});
|
|
720
|
-
devServer.middlewares.use(`${basePath}/vendor/axe-core.min.js`, async (_req, res, _next) => {
|
|
721
|
-
try {
|
|
722
|
-
const require = createRequire(import.meta.url);
|
|
723
|
-
const axeCorePath = require.resolve("axe-core/axe.min.js");
|
|
724
|
-
const content = await fs.promises.readFile(axeCorePath, "utf-8");
|
|
725
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
726
|
-
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
727
|
-
res.end(content);
|
|
728
|
-
} catch {
|
|
729
|
-
res.statusCode = 404;
|
|
730
|
-
res.end("axe-core not installed");
|
|
731
|
-
}
|
|
732
|
-
});
|
|
733
|
-
devServer.middlewares.use(`${basePath}/preview-module`, async (req, res, _next) => {
|
|
734
|
-
const url = new URL(req.url || "", `http://localhost`);
|
|
735
|
-
const artPath = url.searchParams.get("art");
|
|
736
|
-
const variantName = url.searchParams.get("variant");
|
|
737
|
-
if (!artPath || !variantName) {
|
|
738
|
-
res.statusCode = 400;
|
|
739
|
-
res.end("Missing art or variant parameter");
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
const art = artFiles.get(artPath);
|
|
743
|
-
if (!art) {
|
|
744
|
-
res.statusCode = 404;
|
|
745
|
-
res.end("Art not found");
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
const variant = art.variants.find((v) => v.name === variantName);
|
|
749
|
-
if (!variant) {
|
|
750
|
-
res.statusCode = 404;
|
|
751
|
-
res.end("Variant not found");
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
const variantComponentName = toPascalCase(variant.name);
|
|
755
|
-
const moduleCode = generatePreviewModule(art, variantComponentName, variant.name, resolvedPreviewCss, resolvedPreviewSetup);
|
|
756
|
-
try {
|
|
757
|
-
const result = await devServer.transformRequest(`virtual:musea-preview:${artPath}:${variantName}`);
|
|
758
|
-
if (result) {
|
|
759
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
760
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
761
|
-
res.end(result.code);
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
} catch {}
|
|
765
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
766
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
767
|
-
res.end(moduleCode);
|
|
768
|
-
});
|
|
769
|
-
devServer.middlewares.use(`${basePath}/preview`, async (req, res, _next) => {
|
|
770
|
-
const url = new URL(req.url || "", `http://localhost`);
|
|
771
|
-
const artPath = url.searchParams.get("art");
|
|
772
|
-
const variantName = url.searchParams.get("variant");
|
|
773
|
-
if (!artPath || !variantName) {
|
|
774
|
-
res.statusCode = 400;
|
|
775
|
-
res.end("Missing art or variant parameter");
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
const art = artFiles.get(artPath);
|
|
779
|
-
if (!art) {
|
|
780
|
-
res.statusCode = 404;
|
|
781
|
-
res.end("Art not found");
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
const variant = art.variants.find((v) => v.name === variantName);
|
|
785
|
-
if (!variant) {
|
|
786
|
-
res.statusCode = 404;
|
|
787
|
-
res.end("Variant not found");
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
const html = generatePreviewHtml(art, variant, basePath, config.base);
|
|
791
|
-
res.setHeader("Content-Type", "text/html");
|
|
792
|
-
res.end(html);
|
|
793
|
-
});
|
|
794
|
-
devServer.middlewares.use(`${basePath}/art`, async (req, res, next) => {
|
|
795
|
-
const url = new URL(req.url || "", "http://localhost");
|
|
796
|
-
const artPath = decodeURIComponent(url.pathname.slice(1));
|
|
797
|
-
if (!artPath) {
|
|
798
|
-
next();
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
const art = artFiles.get(artPath);
|
|
802
|
-
if (!art) {
|
|
803
|
-
res.statusCode = 404;
|
|
804
|
-
res.end("Art not found: " + artPath);
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
try {
|
|
808
|
-
const virtualId = `virtual:musea-art:${artPath}`;
|
|
809
|
-
const result = await devServer.transformRequest(virtualId);
|
|
810
|
-
if (result) {
|
|
811
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
812
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
813
|
-
res.end(result.code);
|
|
814
|
-
} else {
|
|
815
|
-
const moduleCode = generateArtModule(art, artPath);
|
|
816
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
817
|
-
res.end(moduleCode);
|
|
818
|
-
}
|
|
819
|
-
} catch (err) {
|
|
820
|
-
console.error("[musea] Failed to transform art module:", err);
|
|
821
|
-
const moduleCode = generateArtModule(art, artPath);
|
|
822
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
823
|
-
res.end(moduleCode);
|
|
824
|
-
}
|
|
825
|
-
});
|
|
826
|
-
devServer.middlewares.use(`${basePath}/api`, async (req, res, next) => {
|
|
827
|
-
const sendJson = (data, status = 200) => {
|
|
828
|
-
res.statusCode = status;
|
|
829
|
-
res.setHeader("Content-Type", "application/json");
|
|
830
|
-
res.end(JSON.stringify(data));
|
|
831
|
-
};
|
|
832
|
-
const sendError = (message, status = 500) => {
|
|
833
|
-
sendJson({ error: message }, status);
|
|
834
|
-
};
|
|
835
|
-
if (req.url === "/arts" && req.method === "GET") {
|
|
836
|
-
sendJson(Array.from(artFiles.values()));
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
if (req.url === "/tokens/usage" && req.method === "GET") {
|
|
840
|
-
if (!tokensPath) {
|
|
841
|
-
sendJson({});
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
try {
|
|
845
|
-
const absoluteTokensPath = path.resolve(config.root, tokensPath);
|
|
846
|
-
const categories = await parseTokens(absoluteTokensPath);
|
|
847
|
-
const tokenMap = buildTokenMap(categories);
|
|
848
|
-
resolveReferences(categories, tokenMap);
|
|
849
|
-
const resolvedTokenMap = buildTokenMap(categories);
|
|
850
|
-
const usage = scanTokenUsage(artFiles, resolvedTokenMap);
|
|
851
|
-
sendJson(usage);
|
|
852
|
-
} catch (e) {
|
|
853
|
-
console.error("[musea] Failed to scan token usage:", e);
|
|
854
|
-
sendJson({});
|
|
855
|
-
}
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
if (req.url === "/tokens" && req.method === "GET") {
|
|
859
|
-
if (!tokensPath) {
|
|
860
|
-
sendJson({
|
|
861
|
-
categories: [],
|
|
862
|
-
tokenMap: {},
|
|
863
|
-
meta: {
|
|
864
|
-
filePath: "",
|
|
865
|
-
tokenCount: 0,
|
|
866
|
-
primitiveCount: 0,
|
|
867
|
-
semanticCount: 0
|
|
868
|
-
}
|
|
869
|
-
});
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
try {
|
|
873
|
-
const absoluteTokensPath = path.resolve(config.root, tokensPath);
|
|
874
|
-
const categories = await parseTokens(absoluteTokensPath);
|
|
875
|
-
const tokenMap = buildTokenMap(categories);
|
|
876
|
-
resolveReferences(categories, tokenMap);
|
|
877
|
-
const resolvedTokenMap = buildTokenMap(categories);
|
|
878
|
-
let primitiveCount = 0;
|
|
879
|
-
let semanticCount = 0;
|
|
880
|
-
for (const token of Object.values(resolvedTokenMap)) if (token.$tier === "semantic") semanticCount++;
|
|
881
|
-
else primitiveCount++;
|
|
882
|
-
sendJson({
|
|
883
|
-
categories,
|
|
884
|
-
tokenMap: resolvedTokenMap,
|
|
885
|
-
meta: {
|
|
886
|
-
filePath: absoluteTokensPath,
|
|
887
|
-
tokenCount: Object.keys(resolvedTokenMap).length,
|
|
888
|
-
primitiveCount,
|
|
889
|
-
semanticCount
|
|
890
|
-
}
|
|
891
|
-
});
|
|
892
|
-
} catch (e) {
|
|
893
|
-
console.error("[musea] Failed to load tokens:", e);
|
|
894
|
-
sendJson({
|
|
895
|
-
categories: [],
|
|
896
|
-
tokenMap: {},
|
|
897
|
-
error: String(e)
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
if (req.url === "/tokens" && req.method === "POST") {
|
|
903
|
-
if (!tokensPath) {
|
|
904
|
-
sendError("No tokens path configured", 400);
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
let body = "";
|
|
908
|
-
req.on("data", (chunk) => {
|
|
909
|
-
body += chunk;
|
|
910
|
-
});
|
|
911
|
-
req.on("end", async () => {
|
|
912
|
-
try {
|
|
913
|
-
const { path: dotPath, token } = JSON.parse(body);
|
|
914
|
-
if (!dotPath || !token || token.value === void 0) {
|
|
915
|
-
sendError("Missing required fields: path, token.value", 400);
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
const absoluteTokensPath = path.resolve(config.root, tokensPath);
|
|
919
|
-
const rawData = await readRawTokenFile(absoluteTokensPath);
|
|
920
|
-
const currentCategories = await parseTokens(absoluteTokensPath);
|
|
921
|
-
const currentMap = buildTokenMap(currentCategories);
|
|
922
|
-
if (currentMap[dotPath]) {
|
|
923
|
-
sendError(`Token already exists at path "${dotPath}"`, 409);
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
if (token.$reference) {
|
|
927
|
-
const validation = validateSemanticReference(currentMap, token.$reference, dotPath);
|
|
928
|
-
if (!validation.valid) {
|
|
929
|
-
sendError(validation.error, 400);
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
token.value = `{${token.$reference}}`;
|
|
933
|
-
token.$tier = "semantic";
|
|
934
|
-
}
|
|
935
|
-
setTokenAtPath(rawData, dotPath, token);
|
|
936
|
-
await writeRawTokenFile(absoluteTokensPath, rawData);
|
|
937
|
-
const categories = await parseTokens(absoluteTokensPath);
|
|
938
|
-
const tokenMap = buildTokenMap(categories);
|
|
939
|
-
resolveReferences(categories, tokenMap);
|
|
940
|
-
const resolvedTokenMap = buildTokenMap(categories);
|
|
941
|
-
sendJson({
|
|
942
|
-
categories,
|
|
943
|
-
tokenMap: resolvedTokenMap
|
|
944
|
-
}, 201);
|
|
945
|
-
} catch (e) {
|
|
946
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
if (req.url === "/tokens" && req.method === "PUT") {
|
|
952
|
-
if (!tokensPath) {
|
|
953
|
-
sendError("No tokens path configured", 400);
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
let body = "";
|
|
957
|
-
req.on("data", (chunk) => {
|
|
958
|
-
body += chunk;
|
|
959
|
-
});
|
|
960
|
-
req.on("end", async () => {
|
|
961
|
-
try {
|
|
962
|
-
const { path: dotPath, token } = JSON.parse(body);
|
|
963
|
-
if (!dotPath || !token || token.value === void 0) {
|
|
964
|
-
sendError("Missing required fields: path, token.value", 400);
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
const absoluteTokensPath = path.resolve(config.root, tokensPath);
|
|
968
|
-
if (token.$reference) {
|
|
969
|
-
const currentCategories = await parseTokens(absoluteTokensPath);
|
|
970
|
-
const currentMap = buildTokenMap(currentCategories);
|
|
971
|
-
const validation = validateSemanticReference(currentMap, token.$reference, dotPath);
|
|
972
|
-
if (!validation.valid) {
|
|
973
|
-
sendError(validation.error, 400);
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
token.value = `{${token.$reference}}`;
|
|
977
|
-
token.$tier = "semantic";
|
|
978
|
-
}
|
|
979
|
-
const rawData = await readRawTokenFile(absoluteTokensPath);
|
|
980
|
-
setTokenAtPath(rawData, dotPath, token);
|
|
981
|
-
await writeRawTokenFile(absoluteTokensPath, rawData);
|
|
982
|
-
const categories = await parseTokens(absoluteTokensPath);
|
|
983
|
-
const tokenMap = buildTokenMap(categories);
|
|
984
|
-
resolveReferences(categories, tokenMap);
|
|
985
|
-
const resolvedTokenMap = buildTokenMap(categories);
|
|
986
|
-
sendJson({
|
|
987
|
-
categories,
|
|
988
|
-
tokenMap: resolvedTokenMap
|
|
989
|
-
});
|
|
990
|
-
} catch (e) {
|
|
991
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
992
|
-
}
|
|
993
|
-
});
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
if (req.url === "/tokens" && req.method === "DELETE") {
|
|
997
|
-
if (!tokensPath) {
|
|
998
|
-
sendError("No tokens path configured", 400);
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
let body = "";
|
|
1002
|
-
req.on("data", (chunk) => {
|
|
1003
|
-
body += chunk;
|
|
1004
|
-
});
|
|
1005
|
-
req.on("end", async () => {
|
|
1006
|
-
try {
|
|
1007
|
-
const { path: dotPath } = JSON.parse(body);
|
|
1008
|
-
if (!dotPath) {
|
|
1009
|
-
sendError("Missing required field: path", 400);
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
const absoluteTokensPath = path.resolve(config.root, tokensPath);
|
|
1013
|
-
const currentCategories = await parseTokens(absoluteTokensPath);
|
|
1014
|
-
const currentMap = buildTokenMap(currentCategories);
|
|
1015
|
-
const dependents = findDependentTokens(currentMap, dotPath);
|
|
1016
|
-
const rawData = await readRawTokenFile(absoluteTokensPath);
|
|
1017
|
-
const deleted = deleteTokenAtPath(rawData, dotPath);
|
|
1018
|
-
if (!deleted) {
|
|
1019
|
-
sendError(`Token not found at path "${dotPath}"`, 404);
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
await writeRawTokenFile(absoluteTokensPath, rawData);
|
|
1023
|
-
const categories = await parseTokens(absoluteTokensPath);
|
|
1024
|
-
const tokenMap = buildTokenMap(categories);
|
|
1025
|
-
resolveReferences(categories, tokenMap);
|
|
1026
|
-
const resolvedTokenMap = buildTokenMap(categories);
|
|
1027
|
-
sendJson({
|
|
1028
|
-
categories,
|
|
1029
|
-
tokenMap: resolvedTokenMap,
|
|
1030
|
-
dependentsWarning: dependents.length > 0 ? dependents : void 0
|
|
1031
|
-
});
|
|
1032
|
-
} catch (e) {
|
|
1033
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
1034
|
-
}
|
|
1035
|
-
});
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
1038
|
-
if (req.url?.startsWith("/arts/") && req.method === "PUT") {
|
|
1039
|
-
const rest = req.url.slice(6);
|
|
1040
|
-
const sourceMatch = rest.match(/^(.+)\/source$/);
|
|
1041
|
-
if (sourceMatch) {
|
|
1042
|
-
const artPath = decodeURIComponent(sourceMatch[1]);
|
|
1043
|
-
const art = artFiles.get(artPath);
|
|
1044
|
-
if (!art) {
|
|
1045
|
-
sendError("Art not found", 404);
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
let body = "";
|
|
1049
|
-
req.on("data", (chunk) => {
|
|
1050
|
-
body += chunk;
|
|
1051
|
-
});
|
|
1052
|
-
req.on("end", async () => {
|
|
1053
|
-
try {
|
|
1054
|
-
const { source } = JSON.parse(body);
|
|
1055
|
-
if (typeof source !== "string") {
|
|
1056
|
-
sendError("Missing required field: source", 400);
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
await fs.promises.writeFile(artPath, source, "utf-8");
|
|
1060
|
-
await processArtFile(artPath);
|
|
1061
|
-
sendJson({ success: true });
|
|
1062
|
-
} catch (e) {
|
|
1063
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
1064
|
-
}
|
|
1065
|
-
});
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
next();
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
if (req.url?.startsWith("/arts/") && req.method === "GET") {
|
|
1072
|
-
const rest = req.url.slice(6);
|
|
1073
|
-
const sourceMatch = rest.match(/^(.+)\/source$/);
|
|
1074
|
-
const paletteMatch = rest.match(/^(.+)\/palette$/);
|
|
1075
|
-
const analysisMatch = rest.match(/^(.+)\/analysis$/);
|
|
1076
|
-
const docsMatch = rest.match(/^(.+)\/docs$/);
|
|
1077
|
-
const a11yMatch = rest.match(/^(.+)\/variants\/([^/]+)\/a11y$/);
|
|
1078
|
-
if (sourceMatch) {
|
|
1079
|
-
const artPath$1 = decodeURIComponent(sourceMatch[1]);
|
|
1080
|
-
const art$1 = artFiles.get(artPath$1);
|
|
1081
|
-
if (!art$1) {
|
|
1082
|
-
sendError("Art not found", 404);
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
try {
|
|
1086
|
-
const source = await fs.promises.readFile(artPath$1, "utf-8");
|
|
1087
|
-
sendJson({
|
|
1088
|
-
source,
|
|
1089
|
-
path: artPath$1
|
|
1090
|
-
});
|
|
1091
|
-
} catch (e) {
|
|
1092
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
1093
|
-
}
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
if (paletteMatch) {
|
|
1097
|
-
const artPath$1 = decodeURIComponent(paletteMatch[1]);
|
|
1098
|
-
const art$1 = artFiles.get(artPath$1);
|
|
1099
|
-
if (!art$1) {
|
|
1100
|
-
sendError("Art not found", 404);
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
try {
|
|
1104
|
-
const source = await fs.promises.readFile(artPath$1, "utf-8");
|
|
1105
|
-
const binding = loadNative();
|
|
1106
|
-
let palette;
|
|
1107
|
-
if (binding.generateArtPalette) palette = binding.generateArtPalette(source, { filename: artPath$1 });
|
|
1108
|
-
else palette = {
|
|
1109
|
-
title: art$1.metadata.title,
|
|
1110
|
-
controls: [],
|
|
1111
|
-
groups: [],
|
|
1112
|
-
json: "{}",
|
|
1113
|
-
typescript: ""
|
|
1114
|
-
};
|
|
1115
|
-
if (palette.controls.length === 0 && art$1.metadata.component) {
|
|
1116
|
-
const resolvedComponentPath = path.isAbsolute(art$1.metadata.component) ? art$1.metadata.component : path.resolve(path.dirname(artPath$1), art$1.metadata.component);
|
|
1117
|
-
try {
|
|
1118
|
-
const componentSource = await fs.promises.readFile(resolvedComponentPath, "utf-8");
|
|
1119
|
-
const analysis = binding.analyzeSfc ? binding.analyzeSfc(componentSource, { filename: resolvedComponentPath }) : analyzeSfcFallback(componentSource, { filename: resolvedComponentPath });
|
|
1120
|
-
if (analysis.props.length > 0) {
|
|
1121
|
-
palette.controls = analysis.props.map((prop) => {
|
|
1122
|
-
let control = "text";
|
|
1123
|
-
if (prop.type === "boolean") control = "boolean";
|
|
1124
|
-
else if (prop.type === "number") control = "number";
|
|
1125
|
-
else if (prop.type.includes("|") && !prop.type.includes("=>")) control = "select";
|
|
1126
|
-
const options$1 = [];
|
|
1127
|
-
if (control === "select") {
|
|
1128
|
-
const optionMatches = prop.type.match(/"([^"]+)"/g);
|
|
1129
|
-
if (optionMatches) for (const opt of optionMatches) {
|
|
1130
|
-
const val = opt.replace(/"/g, "");
|
|
1131
|
-
options$1.push({
|
|
1132
|
-
label: val,
|
|
1133
|
-
value: val
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
return {
|
|
1138
|
-
name: prop.name,
|
|
1139
|
-
control,
|
|
1140
|
-
default_value: prop.default_value !== void 0 ? prop.default_value === "true" ? true : prop.default_value === "false" ? false : typeof prop.default_value === "string" && prop.default_value.startsWith("\"") ? prop.default_value.replace(/^"|"$/g, "") : prop.default_value : void 0,
|
|
1141
|
-
description: void 0,
|
|
1142
|
-
required: prop.required,
|
|
1143
|
-
options: options$1,
|
|
1144
|
-
range: void 0,
|
|
1145
|
-
group: void 0
|
|
1146
|
-
};
|
|
1147
|
-
});
|
|
1148
|
-
palette.json = JSON.stringify({
|
|
1149
|
-
title: palette.title,
|
|
1150
|
-
controls: palette.controls
|
|
1151
|
-
}, null, 2);
|
|
1152
|
-
palette.typescript = `export interface ${palette.title}Props {\n${palette.controls.map((c) => ` ${c.name}${c.required ? "" : "?"}: ${c.control === "boolean" ? "boolean" : c.control === "number" ? "number" : c.control === "select" ? c.options.map((o) => `"${String(o.value)}"`).join(" | ") : "string"};`).join("\n")}\n}\n`;
|
|
1153
|
-
}
|
|
1154
|
-
} catch {}
|
|
1155
|
-
}
|
|
1156
|
-
sendJson(palette);
|
|
1157
|
-
} catch (e) {
|
|
1158
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
1159
|
-
}
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
if (analysisMatch) {
|
|
1163
|
-
const artPath$1 = decodeURIComponent(analysisMatch[1]);
|
|
1164
|
-
const art$1 = artFiles.get(artPath$1);
|
|
1165
|
-
if (!art$1) {
|
|
1166
|
-
sendError("Art not found", 404);
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
try {
|
|
1170
|
-
const resolvedComponentPath = art$1.isInline && art$1.componentPath ? art$1.componentPath : art$1.metadata.component ? path.isAbsolute(art$1.metadata.component) ? art$1.metadata.component : path.resolve(path.dirname(artPath$1), art$1.metadata.component) : null;
|
|
1171
|
-
if (resolvedComponentPath) {
|
|
1172
|
-
const source = await fs.promises.readFile(resolvedComponentPath, "utf-8");
|
|
1173
|
-
const binding = loadNative();
|
|
1174
|
-
if (binding.analyzeSfc) {
|
|
1175
|
-
const analysis = binding.analyzeSfc(source, { filename: resolvedComponentPath });
|
|
1176
|
-
sendJson(analysis);
|
|
1177
|
-
} else {
|
|
1178
|
-
const analysis = analyzeSfcFallback(source, { filename: resolvedComponentPath });
|
|
1179
|
-
sendJson(analysis);
|
|
1180
|
-
}
|
|
1181
|
-
} else sendJson({
|
|
1182
|
-
props: [],
|
|
1183
|
-
emits: []
|
|
1184
|
-
});
|
|
1185
|
-
} catch (e) {
|
|
1186
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
1187
|
-
}
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
if (docsMatch) {
|
|
1191
|
-
const artPath$1 = decodeURIComponent(docsMatch[1]);
|
|
1192
|
-
const art$1 = artFiles.get(artPath$1);
|
|
1193
|
-
if (!art$1) {
|
|
1194
|
-
sendError("Art not found", 404);
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
|
-
try {
|
|
1198
|
-
const source = await fs.promises.readFile(artPath$1, "utf-8");
|
|
1199
|
-
const binding = loadNative();
|
|
1200
|
-
if (binding.generateArtDoc) {
|
|
1201
|
-
const doc = binding.generateArtDoc(source, { filename: artPath$1 });
|
|
1202
|
-
let markdown = doc.markdown || "";
|
|
1203
|
-
const componentName = art$1.metadata.title || "Component";
|
|
1204
|
-
markdown = markdown.replace(/<Self(\s|>|\/)/g, `<${componentName}$1`).replace(/<\/Self>/g, `</${componentName}>`);
|
|
1205
|
-
markdown = markdown.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
|
|
1206
|
-
const lines = code.split("\n");
|
|
1207
|
-
let minIndent = Infinity;
|
|
1208
|
-
for (const line of lines) if (line.trim()) {
|
|
1209
|
-
const indent = line.match(/^(\s*)/)?.[1].length || 0;
|
|
1210
|
-
minIndent = Math.min(minIndent, indent);
|
|
1211
|
-
}
|
|
1212
|
-
if (minIndent === Infinity) minIndent = 0;
|
|
1213
|
-
let formatted;
|
|
1214
|
-
if (minIndent > 0) formatted = lines.map((line) => line.slice(minIndent)).join("\n");
|
|
1215
|
-
else {
|
|
1216
|
-
let restIndent = Infinity;
|
|
1217
|
-
for (let i = 1; i < lines.length; i++) if (lines[i].trim()) {
|
|
1218
|
-
const indent = lines[i].match(/^(\s*)/)?.[1].length || 0;
|
|
1219
|
-
restIndent = Math.min(restIndent, indent);
|
|
1220
|
-
}
|
|
1221
|
-
if (restIndent === Infinity || restIndent === 0) formatted = lines.join("\n");
|
|
1222
|
-
else formatted = lines.map((line, i) => i === 0 ? line : line.slice(restIndent)).join("\n");
|
|
1223
|
-
}
|
|
1224
|
-
return "```" + lang + "\n" + formatted + "```";
|
|
1225
|
-
});
|
|
1226
|
-
sendJson({
|
|
1227
|
-
...doc,
|
|
1228
|
-
markdown
|
|
1229
|
-
});
|
|
1230
|
-
} else sendJson({
|
|
1231
|
-
markdown: "",
|
|
1232
|
-
title: art$1.metadata.title,
|
|
1233
|
-
variant_count: art$1.variants.length
|
|
1234
|
-
});
|
|
1235
|
-
} catch (e) {
|
|
1236
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
1237
|
-
}
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
if (a11yMatch) {
|
|
1241
|
-
const artPath$1 = decodeURIComponent(a11yMatch[1]);
|
|
1242
|
-
const _variantName = decodeURIComponent(a11yMatch[2]);
|
|
1243
|
-
const art$1 = artFiles.get(artPath$1);
|
|
1244
|
-
if (!art$1) {
|
|
1245
|
-
sendError("Art not found", 404);
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
sendJson({
|
|
1249
|
-
violations: [],
|
|
1250
|
-
passes: 0,
|
|
1251
|
-
incomplete: 0
|
|
1252
|
-
});
|
|
1253
|
-
return;
|
|
1254
|
-
}
|
|
1255
|
-
const artPath = decodeURIComponent(rest);
|
|
1256
|
-
const art = artFiles.get(artPath);
|
|
1257
|
-
if (art) sendJson(art);
|
|
1258
|
-
else sendError("Art not found", 404);
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
if (req.url === "/preview-with-props" && req.method === "POST") {
|
|
1262
|
-
let body = "";
|
|
1263
|
-
req.on("data", (chunk) => {
|
|
1264
|
-
body += chunk;
|
|
1265
|
-
});
|
|
1266
|
-
req.on("end", () => {
|
|
1267
|
-
try {
|
|
1268
|
-
const { artPath: reqArtPath, variantName, props: propsOverride } = JSON.parse(body);
|
|
1269
|
-
const art = artFiles.get(reqArtPath);
|
|
1270
|
-
if (!art) {
|
|
1271
|
-
sendError("Art not found", 404);
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
const variant = art.variants.find((v) => v.name === variantName);
|
|
1275
|
-
if (!variant) {
|
|
1276
|
-
sendError("Variant not found", 404);
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
const variantComponentName = toPascalCase(variant.name);
|
|
1280
|
-
const moduleCode = generatePreviewModuleWithProps(art, variantComponentName, variant.name, propsOverride, resolvedPreviewCss, resolvedPreviewSetup);
|
|
1281
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
1282
|
-
res.end(moduleCode);
|
|
1283
|
-
} catch (e) {
|
|
1284
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
1285
|
-
}
|
|
1286
|
-
});
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
if (req.url === "/generate" && req.method === "POST") {
|
|
1290
|
-
let body = "";
|
|
1291
|
-
req.on("data", (chunk) => {
|
|
1292
|
-
body += chunk;
|
|
1293
|
-
});
|
|
1294
|
-
req.on("end", async () => {
|
|
1295
|
-
try {
|
|
1296
|
-
const { componentPath: reqComponentPath, options: autogenOptions } = JSON.parse(body);
|
|
1297
|
-
const { generateArtFile: genArt } = await import("./autogen.js");
|
|
1298
|
-
const result = await genArt(reqComponentPath, autogenOptions);
|
|
1299
|
-
sendJson({
|
|
1300
|
-
generated: true,
|
|
1301
|
-
componentName: result.componentName,
|
|
1302
|
-
variants: result.variants,
|
|
1303
|
-
artFileContent: result.artFileContent
|
|
1304
|
-
});
|
|
1305
|
-
} catch (e) {
|
|
1306
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
1307
|
-
}
|
|
1308
|
-
});
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
if (req.url === "/run-vrt" && req.method === "POST") {
|
|
1312
|
-
let body = "";
|
|
1313
|
-
req.on("data", (chunk) => {
|
|
1314
|
-
body += chunk;
|
|
1315
|
-
});
|
|
1316
|
-
req.on("end", async () => {
|
|
1317
|
-
try {
|
|
1318
|
-
const { artPath, updateSnapshots } = JSON.parse(body);
|
|
1319
|
-
const { MuseaVrtRunner: MuseaVrtRunner$1 } = await import("./vrt.js");
|
|
1320
|
-
const runner = new MuseaVrtRunner$1({ snapshotDir: path.resolve(config.root, ".vize/snapshots") });
|
|
1321
|
-
const port = devServer.config.server.port || 5173;
|
|
1322
|
-
const baseUrl = `http://localhost:${port}`;
|
|
1323
|
-
let artsToTest = Array.from(artFiles.values());
|
|
1324
|
-
if (artPath) artsToTest = artsToTest.filter((a) => a.path === artPath);
|
|
1325
|
-
await runner.start();
|
|
1326
|
-
const results = await runner.runTests(artsToTest, baseUrl, { updateSnapshots });
|
|
1327
|
-
const summary = runner.getSummary(results);
|
|
1328
|
-
await runner.stop();
|
|
1329
|
-
sendJson({
|
|
1330
|
-
success: true,
|
|
1331
|
-
summary,
|
|
1332
|
-
results: results.map((r) => ({
|
|
1333
|
-
artPath: r.artPath,
|
|
1334
|
-
variantName: r.variantName,
|
|
1335
|
-
viewport: r.viewport.name,
|
|
1336
|
-
passed: r.passed,
|
|
1337
|
-
isNew: r.isNew,
|
|
1338
|
-
diffPercentage: r.diffPercentage,
|
|
1339
|
-
error: r.error
|
|
1340
|
-
}))
|
|
1341
|
-
});
|
|
1342
|
-
} catch (e) {
|
|
1343
|
-
sendError(e instanceof Error ? e.message : String(e));
|
|
1344
|
-
}
|
|
1345
|
-
});
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
next();
|
|
1349
|
-
});
|
|
1350
|
-
devServer.watcher.on("change", async (file) => {
|
|
1351
|
-
if (file.endsWith(".art.vue") && shouldProcess(file, include, exclude, config.root)) {
|
|
1352
|
-
await processArtFile(file);
|
|
1353
|
-
console.log(`[musea] Reloaded: ${path.relative(config.root, file)}`);
|
|
1354
|
-
}
|
|
1355
|
-
if (inlineArt && file.endsWith(".vue") && !file.endsWith(".art.vue")) {
|
|
1356
|
-
const hadArt = artFiles.has(file);
|
|
1357
|
-
const source = await fs.promises.readFile(file, "utf-8");
|
|
1358
|
-
if (source.includes("<art")) {
|
|
1359
|
-
await processArtFile(file);
|
|
1360
|
-
console.log(`[musea] Reloaded inline art: ${path.relative(config.root, file)}`);
|
|
1361
|
-
} else if (hadArt) {
|
|
1362
|
-
artFiles.delete(file);
|
|
1363
|
-
console.log(`[musea] Removed inline art: ${path.relative(config.root, file)}`);
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
});
|
|
1367
|
-
devServer.watcher.on("add", async (file) => {
|
|
1368
|
-
if (file.endsWith(".art.vue") && shouldProcess(file, include, exclude, config.root)) {
|
|
1369
|
-
await processArtFile(file);
|
|
1370
|
-
console.log(`[musea] Added: ${path.relative(config.root, file)}`);
|
|
1371
|
-
}
|
|
1372
|
-
if (inlineArt && file.endsWith(".vue") && !file.endsWith(".art.vue")) {
|
|
1373
|
-
const source = await fs.promises.readFile(file, "utf-8");
|
|
1374
|
-
if (source.includes("<art")) {
|
|
1375
|
-
await processArtFile(file);
|
|
1376
|
-
console.log(`[musea] Added inline art: ${path.relative(config.root, file)}`);
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
});
|
|
1380
|
-
devServer.watcher.on("unlink", (file) => {
|
|
1381
|
-
if (artFiles.has(file)) {
|
|
1382
|
-
artFiles.delete(file);
|
|
1383
|
-
console.log(`[musea] Removed: ${path.relative(config.root, file)}`);
|
|
1384
|
-
}
|
|
1385
|
-
});
|
|
1386
|
-
return () => {
|
|
1387
|
-
devServer.httpServer?.once("listening", () => {
|
|
1388
|
-
const address = devServer.httpServer?.address();
|
|
1389
|
-
if (address && typeof address === "object") {
|
|
1390
|
-
const protocol = devServer.config.server.https ? "https" : "http";
|
|
1391
|
-
const rawHost = address.address;
|
|
1392
|
-
const host = rawHost === "::" || rawHost === "::1" || rawHost === "0.0.0.0" || rawHost === "127.0.0.1" ? "localhost" : rawHost;
|
|
1393
|
-
const port = address.port;
|
|
1394
|
-
const url = `${protocol}://${host}:${port}${basePath}`;
|
|
1395
|
-
console.log();
|
|
1396
|
-
console.log(` \x1b[36m➜\x1b[0m \x1b[1mMusea Gallery:\x1b[0m \x1b[36m${url}\x1b[0m`);
|
|
1397
|
-
}
|
|
1398
|
-
});
|
|
1399
|
-
};
|
|
1400
|
-
},
|
|
1401
|
-
async buildStart() {
|
|
1402
|
-
console.log(`[musea] config.root: ${config.root}, include: ${JSON.stringify(include)}`);
|
|
1403
|
-
const files = await scanArtFiles(config.root, include, exclude, inlineArt);
|
|
1404
|
-
console.log(`[musea] Found ${files.length} art files`);
|
|
1405
|
-
for (const file of files) await processArtFile(file);
|
|
1406
|
-
if (storybookCompat) await generateStorybookFiles(artFiles, config.root, storybookOutDir);
|
|
1407
|
-
},
|
|
1408
|
-
resolveId(id) {
|
|
1409
|
-
if (id === VIRTUAL_GALLERY) return VIRTUAL_GALLERY;
|
|
1410
|
-
if (id === VIRTUAL_MANIFEST) return VIRTUAL_MANIFEST;
|
|
1411
|
-
if (id.startsWith("virtual:musea-preview:")) return "\0musea-preview:" + id.slice(22);
|
|
1412
|
-
if (id.startsWith("virtual:musea-art:")) {
|
|
1413
|
-
const artPath = id.slice(18);
|
|
1414
|
-
if (artFiles.has(artPath)) return "\0musea-art:" + artPath + "?musea-virtual";
|
|
1415
|
-
}
|
|
1416
|
-
if (id.endsWith(".art.vue")) {
|
|
1417
|
-
const resolved = path.resolve(config.root, id);
|
|
1418
|
-
if (artFiles.has(resolved)) return VIRTUAL_MUSEA_PREFIX + resolved + "?musea-virtual";
|
|
1419
|
-
}
|
|
1420
|
-
if (inlineArt && id.endsWith(".vue") && !id.endsWith(".art.vue")) {
|
|
1421
|
-
const resolved = path.resolve(config.root, id);
|
|
1422
|
-
if (artFiles.has(resolved)) return VIRTUAL_MUSEA_PREFIX + resolved + "?musea-virtual";
|
|
1423
|
-
}
|
|
1424
|
-
return null;
|
|
1425
|
-
},
|
|
1426
|
-
load(id) {
|
|
1427
|
-
if (id === VIRTUAL_GALLERY) return generateGalleryModule(basePath);
|
|
1428
|
-
if (id === VIRTUAL_MANIFEST) return generateManifestModule(artFiles);
|
|
1429
|
-
if (id.startsWith("\0musea-preview:")) {
|
|
1430
|
-
const rest = id.slice(15);
|
|
1431
|
-
const lastColonIndex = rest.lastIndexOf(":");
|
|
1432
|
-
if (lastColonIndex !== -1) {
|
|
1433
|
-
const artPath = rest.slice(0, lastColonIndex);
|
|
1434
|
-
const variantName = rest.slice(lastColonIndex + 1);
|
|
1435
|
-
const art = artFiles.get(artPath);
|
|
1436
|
-
if (art) {
|
|
1437
|
-
const variantComponentName = toPascalCase(variantName);
|
|
1438
|
-
return generatePreviewModule(art, variantComponentName, variantName, resolvedPreviewCss, resolvedPreviewSetup);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
if (id.startsWith("\0musea-art:")) {
|
|
1443
|
-
const artPath = id.slice(11).replace(/\?musea-virtual$/, "");
|
|
1444
|
-
const art = artFiles.get(artPath);
|
|
1445
|
-
if (art) return generateArtModule(art, artPath);
|
|
1446
|
-
}
|
|
1447
|
-
if (id.startsWith(VIRTUAL_MUSEA_PREFIX)) {
|
|
1448
|
-
const realPath = id.slice(VIRTUAL_MUSEA_PREFIX.length).replace(/\?musea-virtual$/, "");
|
|
1449
|
-
const art = artFiles.get(realPath);
|
|
1450
|
-
if (art) return generateArtModule(art, realPath);
|
|
1451
|
-
}
|
|
1452
|
-
return null;
|
|
1453
|
-
},
|
|
1454
|
-
async handleHotUpdate(ctx) {
|
|
1455
|
-
const { file } = ctx;
|
|
1456
|
-
if (file.endsWith(".art.vue") && artFiles.has(file)) {
|
|
1457
|
-
await processArtFile(file);
|
|
1458
|
-
const virtualId = VIRTUAL_MUSEA_PREFIX + file + "?musea-virtual";
|
|
1459
|
-
const modules = server?.moduleGraph.getModulesByFile(virtualId);
|
|
1460
|
-
if (modules) return [...modules];
|
|
1461
|
-
}
|
|
1462
|
-
if (inlineArt && file.endsWith(".vue") && !file.endsWith(".art.vue") && artFiles.has(file)) {
|
|
1463
|
-
await processArtFile(file);
|
|
1464
|
-
const virtualId = VIRTUAL_MUSEA_PREFIX + file;
|
|
1465
|
-
const modules = server?.moduleGraph.getModulesByFile(virtualId);
|
|
1466
|
-
if (modules) return [...modules];
|
|
1467
|
-
}
|
|
1468
|
-
return void 0;
|
|
1469
|
-
}
|
|
1470
|
-
};
|
|
1471
|
-
async function processArtFile(filePath) {
|
|
1472
|
-
try {
|
|
1473
|
-
const source = await fs.promises.readFile(filePath, "utf-8");
|
|
1474
|
-
const binding = loadNative();
|
|
1475
|
-
const parsed = binding.parseArt(source, { filename: filePath });
|
|
1476
|
-
if (!parsed.variants || parsed.variants.length === 0) return;
|
|
1477
|
-
const isInline = !filePath.endsWith(".art.vue");
|
|
1478
|
-
const info = {
|
|
1479
|
-
path: filePath,
|
|
1480
|
-
metadata: {
|
|
1481
|
-
title: parsed.metadata.title || (isInline ? path.basename(filePath, ".vue") : ""),
|
|
1482
|
-
description: parsed.metadata.description,
|
|
1483
|
-
component: isInline ? void 0 : parsed.metadata.component,
|
|
1484
|
-
category: parsed.metadata.category,
|
|
1485
|
-
tags: parsed.metadata.tags,
|
|
1486
|
-
status: parsed.metadata.status,
|
|
1487
|
-
order: parsed.metadata.order
|
|
1488
|
-
},
|
|
1489
|
-
variants: parsed.variants.map((v) => ({
|
|
1490
|
-
name: v.name,
|
|
1491
|
-
template: v.template,
|
|
1492
|
-
isDefault: v.isDefault,
|
|
1493
|
-
skipVrt: v.skipVrt
|
|
1494
|
-
})),
|
|
1495
|
-
hasScriptSetup: isInline ? false : parsed.hasScriptSetup,
|
|
1496
|
-
scriptSetupContent: !isInline && parsed.hasScriptSetup ? extractScriptSetupContent(source) : void 0,
|
|
1497
|
-
hasScript: parsed.hasScript,
|
|
1498
|
-
styleCount: parsed.styleCount,
|
|
1499
|
-
isInline,
|
|
1500
|
-
componentPath: isInline ? filePath : void 0
|
|
1501
|
-
};
|
|
1502
|
-
artFiles.set(filePath, info);
|
|
1503
|
-
} catch (e) {
|
|
1504
|
-
console.error(`[musea] Failed to process ${filePath}:`, e);
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
return [mainPlugin];
|
|
1508
|
-
}
|
|
1509
|
-
function shouldProcess(file, include, exclude, root) {
|
|
1510
|
-
const relative = path.relative(root, file);
|
|
1511
|
-
for (const pattern of exclude) if (matchGlob(relative, pattern)) return false;
|
|
1512
|
-
for (const pattern of include) if (matchGlob(relative, pattern)) return true;
|
|
1513
|
-
return false;
|
|
1514
|
-
}
|
|
1515
|
-
function matchGlob(filepath, pattern) {
|
|
1516
|
-
const PLACEHOLDER = "<<GLOBSTAR>>";
|
|
1517
|
-
const regex = pattern.replaceAll("**", PLACEHOLDER).replace(/\./g, "\\.").replace(/\*/g, "[^/]*").replaceAll(PLACEHOLDER, ".*");
|
|
1518
|
-
return new RegExp(`^${regex}$`).test(filepath);
|
|
1519
|
-
}
|
|
1520
|
-
async function scanArtFiles(root, include, exclude, scanInlineArt = false) {
|
|
1521
|
-
const files = [];
|
|
1522
|
-
async function scan(dir) {
|
|
1523
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
1524
|
-
for (const entry of entries) {
|
|
1525
|
-
const fullPath = path.join(dir, entry.name);
|
|
1526
|
-
const relative = path.relative(root, fullPath);
|
|
1527
|
-
let excluded = false;
|
|
1528
|
-
for (const pattern of exclude) if (matchGlob(relative, pattern) || matchGlob(entry.name, pattern)) {
|
|
1529
|
-
excluded = true;
|
|
1530
|
-
break;
|
|
1531
|
-
}
|
|
1532
|
-
if (excluded) continue;
|
|
1533
|
-
if (entry.isDirectory()) await scan(fullPath);
|
|
1534
|
-
else if (entry.isFile() && entry.name.endsWith(".art.vue")) {
|
|
1535
|
-
for (const pattern of include) if (matchGlob(relative, pattern)) {
|
|
1536
|
-
files.push(fullPath);
|
|
1537
|
-
break;
|
|
1538
|
-
}
|
|
1539
|
-
} else if (scanInlineArt && entry.isFile() && entry.name.endsWith(".vue") && !entry.name.endsWith(".art.vue")) {
|
|
1540
|
-
const content = await fs.promises.readFile(fullPath, "utf-8");
|
|
1541
|
-
if (content.includes("<art")) files.push(fullPath);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
await scan(root);
|
|
1546
|
-
return files;
|
|
1547
|
-
}
|
|
1548
|
-
function generateGalleryHtml(basePath, themeConfig) {
|
|
1549
|
-
const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : "";
|
|
1550
|
-
return `<!DOCTYPE html>
|
|
1551
|
-
<html lang="en">
|
|
1552
|
-
<head>
|
|
1553
|
-
<meta charset="UTF-8">
|
|
1554
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1555
|
-
<title>Musea - Component Gallery</title>
|
|
1556
|
-
<script>window.__MUSEA_BASE_PATH__='${basePath}';${themeScript}</script>
|
|
1557
|
-
<style>
|
|
1558
|
-
:root {
|
|
1559
|
-
--musea-bg-primary: #E6E2D6;
|
|
1560
|
-
--musea-bg-secondary: #ddd9cd;
|
|
1561
|
-
--musea-bg-tertiary: #d4d0c4;
|
|
1562
|
-
--musea-bg-elevated: #E6E2D6;
|
|
1563
|
-
--musea-accent: #121212;
|
|
1564
|
-
--musea-accent-hover: #2a2a2a;
|
|
1565
|
-
--musea-accent-subtle: rgba(18, 18, 18, 0.08);
|
|
1566
|
-
--musea-text: #121212;
|
|
1567
|
-
--musea-text-secondary: #3a3a3a;
|
|
1568
|
-
--musea-text-muted: #6b6b6b;
|
|
1569
|
-
--musea-border: #c8c4b8;
|
|
1570
|
-
--musea-border-subtle: #d4d0c4;
|
|
1571
|
-
--musea-success: #16a34a;
|
|
1572
|
-
--musea-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
|
1573
|
-
--musea-radius-sm: 4px;
|
|
1574
|
-
--musea-radius-md: 6px;
|
|
1575
|
-
--musea-radius-lg: 8px;
|
|
1576
|
-
--musea-transition: 0.15s ease;
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1580
|
-
|
|
1581
|
-
body {
|
|
1582
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
1583
|
-
background: var(--musea-bg-primary);
|
|
1584
|
-
color: var(--musea-text);
|
|
1585
|
-
min-height: 100vh;
|
|
1586
|
-
line-height: 1.5;
|
|
1587
|
-
-webkit-font-smoothing: antialiased;
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
/* Header */
|
|
1591
|
-
.header {
|
|
1592
|
-
background: var(--musea-bg-secondary);
|
|
1593
|
-
border-bottom: 1px solid var(--musea-border);
|
|
1594
|
-
padding: 0 1.5rem;
|
|
1595
|
-
height: 56px;
|
|
1596
|
-
display: flex;
|
|
1597
|
-
align-items: center;
|
|
1598
|
-
justify-content: space-between;
|
|
1599
|
-
position: sticky;
|
|
1600
|
-
top: 0;
|
|
1601
|
-
z-index: 100;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
.header-left {
|
|
1605
|
-
display: flex;
|
|
1606
|
-
align-items: center;
|
|
1607
|
-
gap: 1.5rem;
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
.logo {
|
|
1611
|
-
display: flex;
|
|
1612
|
-
align-items: center;
|
|
1613
|
-
gap: 0.5rem;
|
|
1614
|
-
font-size: 1.125rem;
|
|
1615
|
-
font-weight: 700;
|
|
1616
|
-
color: var(--musea-accent);
|
|
1617
|
-
text-decoration: none;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
.logo-svg {
|
|
1621
|
-
width: 32px;
|
|
1622
|
-
height: 32px;
|
|
1623
|
-
flex-shrink: 0;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
.logo-icon svg {
|
|
1627
|
-
width: 16px;
|
|
1628
|
-
height: 16px;
|
|
1629
|
-
color: var(--musea-text);
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
.header-subtitle {
|
|
1633
|
-
color: var(--musea-text-muted);
|
|
1634
|
-
font-size: 0.8125rem;
|
|
1635
|
-
font-weight: 500;
|
|
1636
|
-
padding-left: 1.5rem;
|
|
1637
|
-
border-left: 1px solid var(--musea-border);
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
.search-container {
|
|
1641
|
-
position: relative;
|
|
1642
|
-
width: 280px;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
.search-input {
|
|
1646
|
-
width: 100%;
|
|
1647
|
-
background: var(--musea-bg-tertiary);
|
|
1648
|
-
border: 1px solid var(--musea-border);
|
|
1649
|
-
border-radius: var(--musea-radius-md);
|
|
1650
|
-
padding: 0.5rem 0.75rem 0.5rem 2.25rem;
|
|
1651
|
-
color: var(--musea-text);
|
|
1652
|
-
font-size: 0.8125rem;
|
|
1653
|
-
outline: none;
|
|
1654
|
-
transition: border-color var(--musea-transition), background var(--musea-transition);
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
.search-input::placeholder {
|
|
189
|
+
|
|
190
|
+
.search-container {
|
|
191
|
+
position: relative;
|
|
192
|
+
width: 280px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.search-input {
|
|
196
|
+
width: 100%;
|
|
197
|
+
background: var(--musea-bg-tertiary);
|
|
198
|
+
border: 1px solid var(--musea-border);
|
|
199
|
+
border-radius: var(--musea-radius-md);
|
|
200
|
+
padding: 0.5rem 0.75rem 0.5rem 2.25rem;
|
|
201
|
+
color: var(--musea-text);
|
|
202
|
+
font-size: 0.8125rem;
|
|
203
|
+
outline: none;
|
|
204
|
+
transition: border-color var(--musea-transition), background var(--musea-transition);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.search-input::placeholder {
|
|
1658
208
|
color: var(--musea-text-muted);
|
|
1659
209
|
}
|
|
1660
210
|
|
|
@@ -2262,15 +812,103 @@ function generateGalleryHtml(basePath, themeConfig) {
|
|
|
2262
812
|
</body>
|
|
2263
813
|
</html>`;
|
|
2264
814
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
815
|
+
/**
|
|
816
|
+
* Generate the virtual gallery module code.
|
|
817
|
+
*/
|
|
818
|
+
function generateGalleryModule(basePath) {
|
|
819
|
+
return `
|
|
820
|
+
export const basePath = '${basePath}';
|
|
821
|
+
export async function loadArts() {
|
|
822
|
+
const res = await fetch(basePath + '/api/arts');
|
|
823
|
+
return res.json();
|
|
824
|
+
}
|
|
825
|
+
`;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/utils.ts
|
|
830
|
+
function shouldProcess(file, include, exclude, root) {
|
|
831
|
+
const relative = path.relative(root, file);
|
|
832
|
+
for (const pattern of exclude) if (matchGlob(relative, pattern)) return false;
|
|
833
|
+
for (const pattern of include) if (matchGlob(relative, pattern)) return true;
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
function matchGlob(filepath, pattern) {
|
|
837
|
+
const PLACEHOLDER = "<<GLOBSTAR>>";
|
|
838
|
+
const regex = pattern.replaceAll("**", PLACEHOLDER).replace(/\./g, "\\.").replace(/\*/g, "[^/]*").replaceAll(PLACEHOLDER, ".*");
|
|
839
|
+
return new RegExp(`^${regex}$`).test(filepath);
|
|
840
|
+
}
|
|
841
|
+
async function scanArtFiles(root, include, exclude, scanInlineArt = false) {
|
|
842
|
+
const files = [];
|
|
843
|
+
async function scan(dir) {
|
|
844
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
845
|
+
for (const entry of entries) {
|
|
846
|
+
const fullPath = path.join(dir, entry.name);
|
|
847
|
+
const relative = path.relative(root, fullPath);
|
|
848
|
+
let excluded = false;
|
|
849
|
+
for (const pattern of exclude) if (matchGlob(relative, pattern) || matchGlob(entry.name, pattern)) {
|
|
850
|
+
excluded = true;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
if (excluded) continue;
|
|
854
|
+
if (entry.isDirectory()) await scan(fullPath);
|
|
855
|
+
else if (entry.isFile() && entry.name.endsWith(".art.vue")) {
|
|
856
|
+
for (const pattern of include) if (matchGlob(relative, pattern)) {
|
|
857
|
+
files.push(fullPath);
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
} else if (scanInlineArt && entry.isFile() && entry.name.endsWith(".vue") && !entry.name.endsWith(".art.vue")) {
|
|
861
|
+
const content = await fs.promises.readFile(fullPath, "utf-8");
|
|
862
|
+
if (content.includes("<art")) files.push(fullPath);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
await scan(root);
|
|
867
|
+
return files;
|
|
2271
868
|
}
|
|
2272
|
-
|
|
869
|
+
async function generateStorybookFiles(artFiles, root, outDir) {
|
|
870
|
+
const binding = loadNative();
|
|
871
|
+
const outputDir = path.resolve(root, outDir);
|
|
872
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
873
|
+
for (const [filePath, _art] of artFiles) try {
|
|
874
|
+
const source = await fs.promises.readFile(filePath, "utf-8");
|
|
875
|
+
const csf = binding.artToCsf(source, { filename: filePath });
|
|
876
|
+
const outputPath = path.join(outputDir, csf.filename);
|
|
877
|
+
await fs.promises.writeFile(outputPath, csf.code, "utf-8");
|
|
878
|
+
console.log(`[musea] Generated: ${path.relative(root, outputPath)}`);
|
|
879
|
+
} catch (e) {
|
|
880
|
+
console.error(`[musea] Failed to generate CSF for ${filePath}:`, e);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function toPascalCase(str) {
|
|
884
|
+
return str.split(/[\s\-_]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
885
|
+
}
|
|
886
|
+
function escapeTemplate(str) {
|
|
887
|
+
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
888
|
+
}
|
|
889
|
+
function escapeHtml(str) {
|
|
890
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Build the theme config object from plugin options for runtime injection.
|
|
894
|
+
*/
|
|
895
|
+
function buildThemeConfig(theme) {
|
|
896
|
+
if (!theme) return void 0;
|
|
897
|
+
if (typeof theme === "string") return { default: theme };
|
|
898
|
+
const themes = Array.isArray(theme) ? theme : [theme];
|
|
899
|
+
const custom = {};
|
|
900
|
+
for (const t of themes) custom[t.name] = {
|
|
901
|
+
base: t.base,
|
|
902
|
+
colors: t.colors
|
|
903
|
+
};
|
|
904
|
+
return {
|
|
905
|
+
default: themes[0].name,
|
|
906
|
+
custom
|
|
907
|
+
};
|
|
2273
908
|
}
|
|
909
|
+
|
|
910
|
+
//#endregion
|
|
911
|
+
//#region src/preview.ts
|
|
2274
912
|
const MUSEA_ADDONS_INIT_CODE = `
|
|
2275
913
|
function __museaInitAddons(container, variantName) {
|
|
2276
914
|
// === DOM event capture ===
|
|
@@ -2553,422 +1191,1913 @@ function __museaInitAddons(container, variantName) {
|
|
|
2553
1191
|
break;
|
|
2554
1192
|
}
|
|
2555
1193
|
}
|
|
2556
|
-
});
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// Notify parent that iframe is ready
|
|
1197
|
+
window.parent.postMessage({ type: 'musea:ready', payload: {} }, '*');
|
|
1198
|
+
}
|
|
1199
|
+
`;
|
|
1200
|
+
function generatePreviewModule(art, variantComponentName, variantName, cssImports = [], previewSetup = null) {
|
|
1201
|
+
const artModuleId = `virtual:musea-art:${art.path}`;
|
|
1202
|
+
const escapedVariantName = escapeTemplate(variantName);
|
|
1203
|
+
const cssImportStatements = cssImports.map((cssPath) => `import '${cssPath}';`).join("\n");
|
|
1204
|
+
const setupImport = previewSetup ? `import __museaPreviewSetup from '${previewSetup}';` : "";
|
|
1205
|
+
const setupCall = previewSetup ? "await __museaPreviewSetup(app);" : "";
|
|
1206
|
+
return `
|
|
1207
|
+
${cssImportStatements}
|
|
1208
|
+
${setupImport}
|
|
1209
|
+
import { createApp, reactive, h } from 'vue';
|
|
1210
|
+
import * as artModule from '${artModuleId}';
|
|
1211
|
+
|
|
1212
|
+
const container = document.getElementById('app');
|
|
1213
|
+
|
|
1214
|
+
${MUSEA_ADDONS_INIT_CODE}
|
|
1215
|
+
|
|
1216
|
+
let currentApp = null;
|
|
1217
|
+
const propsOverride = reactive({});
|
|
1218
|
+
const slotsOverride = reactive({ default: '' });
|
|
1219
|
+
|
|
1220
|
+
window.__museaSetProps = (props) => {
|
|
1221
|
+
// Clear old keys
|
|
1222
|
+
for (const key of Object.keys(propsOverride)) {
|
|
1223
|
+
delete propsOverride[key];
|
|
1224
|
+
}
|
|
1225
|
+
Object.assign(propsOverride, props);
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
window.__museaSetSlots = (slots) => {
|
|
1229
|
+
for (const key of Object.keys(slotsOverride)) {
|
|
1230
|
+
delete slotsOverride[key];
|
|
1231
|
+
}
|
|
1232
|
+
Object.assign(slotsOverride, slots);
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
async function mount() {
|
|
1236
|
+
try {
|
|
1237
|
+
// Get the specific variant component
|
|
1238
|
+
const VariantComponent = artModule['${variantComponentName}'];
|
|
1239
|
+
const RawComponent = artModule.__component__;
|
|
1240
|
+
|
|
1241
|
+
if (!VariantComponent) {
|
|
1242
|
+
throw new Error('Variant component "${variantComponentName}" not found in art module');
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Create and mount the app
|
|
1246
|
+
const app = createApp(VariantComponent);
|
|
1247
|
+
${setupCall}
|
|
1248
|
+
container.innerHTML = '';
|
|
1249
|
+
container.className = 'musea-variant';
|
|
1250
|
+
app.mount(container);
|
|
1251
|
+
currentApp = app;
|
|
1252
|
+
|
|
1253
|
+
console.log('[musea-preview] Mounted variant: ${escapedVariantName}');
|
|
1254
|
+
__museaInitAddons(container, '${escapedVariantName}');
|
|
1255
|
+
|
|
1256
|
+
// Override set-props to remount with raw component + props
|
|
1257
|
+
const TargetComponent = RawComponent || VariantComponent;
|
|
1258
|
+
window.__museaSetProps = (props) => {
|
|
1259
|
+
for (const key of Object.keys(propsOverride)) {
|
|
1260
|
+
delete propsOverride[key];
|
|
1261
|
+
}
|
|
1262
|
+
Object.assign(propsOverride, props);
|
|
1263
|
+
remountWithProps(TargetComponent);
|
|
1264
|
+
};
|
|
1265
|
+
window.__museaSetSlots = (slots) => {
|
|
1266
|
+
for (const key of Object.keys(slotsOverride)) {
|
|
1267
|
+
delete slotsOverride[key];
|
|
1268
|
+
}
|
|
1269
|
+
Object.assign(slotsOverride, slots);
|
|
1270
|
+
remountWithProps(TargetComponent);
|
|
1271
|
+
};
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
console.error('[musea-preview] Failed to mount:', error);
|
|
1274
|
+
container.innerHTML = \`
|
|
1275
|
+
<div class="musea-error">
|
|
1276
|
+
<div class="musea-error-title">Failed to render component</div>
|
|
1277
|
+
<div>\${error.message}</div>
|
|
1278
|
+
<pre>\${error.stack || ''}</pre>
|
|
1279
|
+
</div>
|
|
1280
|
+
\`;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async function remountWithProps(Component) {
|
|
1285
|
+
if (currentApp) {
|
|
1286
|
+
currentApp.unmount();
|
|
1287
|
+
}
|
|
1288
|
+
const app = createApp({
|
|
1289
|
+
setup() {
|
|
1290
|
+
return () => {
|
|
1291
|
+
const slotFns = {};
|
|
1292
|
+
for (const [name, content] of Object.entries(slotsOverride)) {
|
|
1293
|
+
if (content) slotFns[name] = () => h('span', { innerHTML: content });
|
|
1294
|
+
}
|
|
1295
|
+
return h(Component, { ...propsOverride }, slotFns);
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
${setupCall}
|
|
1300
|
+
container.innerHTML = '';
|
|
1301
|
+
app.mount(container);
|
|
1302
|
+
currentApp = app;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
mount();
|
|
1306
|
+
`;
|
|
1307
|
+
}
|
|
1308
|
+
function generatePreviewModuleWithProps(art, variantComponentName, variantName, propsOverride, cssImports = [], previewSetup = null) {
|
|
1309
|
+
const artModuleId = `virtual:musea-art:${art.path}`;
|
|
1310
|
+
const escapedVariantName = escapeTemplate(variantName);
|
|
1311
|
+
const propsJson = JSON.stringify(propsOverride);
|
|
1312
|
+
const cssImportStatements = cssImports.map((cssPath) => `import '${cssPath}';`).join("\n");
|
|
1313
|
+
const setupImport = previewSetup ? `import __museaPreviewSetup from '${previewSetup}';` : "";
|
|
1314
|
+
const setupCall = previewSetup ? "await __museaPreviewSetup(app);" : "";
|
|
1315
|
+
return `
|
|
1316
|
+
${cssImportStatements}
|
|
1317
|
+
${setupImport}
|
|
1318
|
+
import { createApp, h } from 'vue';
|
|
1319
|
+
import * as artModule from '${artModuleId}';
|
|
1320
|
+
|
|
1321
|
+
const container = document.getElementById('app');
|
|
1322
|
+
const propsOverride = ${propsJson};
|
|
1323
|
+
|
|
1324
|
+
${MUSEA_ADDONS_INIT_CODE}
|
|
1325
|
+
|
|
1326
|
+
async function mount() {
|
|
1327
|
+
try {
|
|
1328
|
+
const VariantComponent = artModule['${variantComponentName}'];
|
|
1329
|
+
if (!VariantComponent) {
|
|
1330
|
+
throw new Error('Variant component "${variantComponentName}" not found');
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const WrappedComponent = {
|
|
1334
|
+
render() {
|
|
1335
|
+
return h(VariantComponent, propsOverride);
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
const app = createApp(WrappedComponent);
|
|
1340
|
+
${setupCall}
|
|
1341
|
+
container.innerHTML = '';
|
|
1342
|
+
container.className = 'musea-variant';
|
|
1343
|
+
app.mount(container);
|
|
1344
|
+
console.log('[musea-preview] Mounted variant: ${escapedVariantName} with props override');
|
|
1345
|
+
__museaInitAddons(container, '${escapedVariantName}');
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
console.error('[musea-preview] Failed to mount:', error);
|
|
1348
|
+
container.innerHTML = '<div class="musea-error"><div class="musea-error-title">Failed to render</div><div>' + error.message + '</div></div>';
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
mount();
|
|
1353
|
+
`;
|
|
1354
|
+
}
|
|
1355
|
+
function generatePreviewHtml(art, variant, _basePath, viteBase) {
|
|
1356
|
+
const previewModuleUrl = `${_basePath}/preview-module?art=${encodeURIComponent(art.path)}&variant=${encodeURIComponent(variant.name)}`;
|
|
1357
|
+
const base = (viteBase || "/").replace(/\/$/, "");
|
|
1358
|
+
return `<!DOCTYPE html>
|
|
1359
|
+
<html lang="en">
|
|
1360
|
+
<head>
|
|
1361
|
+
<meta charset="UTF-8">
|
|
1362
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1363
|
+
<title>${escapeHtml(art.metadata.title)} - ${escapeHtml(variant.name)}</title>
|
|
1364
|
+
<script type="module" src="${base}/@vite/client"></script>
|
|
1365
|
+
<style>
|
|
1366
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1367
|
+
html, body {
|
|
1368
|
+
width: 100%;
|
|
1369
|
+
height: 100%;
|
|
1370
|
+
}
|
|
1371
|
+
body {
|
|
1372
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1373
|
+
background: #ffffff;
|
|
1374
|
+
}
|
|
1375
|
+
.musea-variant {
|
|
1376
|
+
min-height: 100vh;
|
|
1377
|
+
}
|
|
1378
|
+
.musea-error {
|
|
1379
|
+
color: #dc2626;
|
|
1380
|
+
background: #fef2f2;
|
|
1381
|
+
border: 1px solid #fecaca;
|
|
1382
|
+
border-radius: 8px;
|
|
1383
|
+
padding: 1rem;
|
|
1384
|
+
font-size: 0.875rem;
|
|
1385
|
+
max-width: 400px;
|
|
1386
|
+
}
|
|
1387
|
+
.musea-error-title {
|
|
1388
|
+
font-weight: 600;
|
|
1389
|
+
margin-bottom: 0.5rem;
|
|
1390
|
+
}
|
|
1391
|
+
.musea-error pre {
|
|
1392
|
+
font-family: monospace;
|
|
1393
|
+
font-size: 0.75rem;
|
|
1394
|
+
white-space: pre-wrap;
|
|
1395
|
+
word-break: break-all;
|
|
1396
|
+
margin-top: 0.5rem;
|
|
1397
|
+
padding: 0.5rem;
|
|
1398
|
+
background: #fff;
|
|
1399
|
+
border-radius: 4px;
|
|
1400
|
+
}
|
|
1401
|
+
.musea-loading {
|
|
1402
|
+
display: flex;
|
|
1403
|
+
align-items: center;
|
|
1404
|
+
gap: 0.75rem;
|
|
1405
|
+
color: #6b7280;
|
|
1406
|
+
font-size: 0.875rem;
|
|
1407
|
+
}
|
|
1408
|
+
.musea-spinner {
|
|
1409
|
+
width: 20px;
|
|
1410
|
+
height: 20px;
|
|
1411
|
+
border: 2px solid #e5e7eb;
|
|
1412
|
+
border-top-color: #3b82f6;
|
|
1413
|
+
border-radius: 50%;
|
|
1414
|
+
animation: spin 0.8s linear infinite;
|
|
1415
|
+
}
|
|
1416
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
1417
|
+
|
|
1418
|
+
/* Musea Addons: Checkerboard background for transparent mode */
|
|
1419
|
+
.musea-bg-checkerboard {
|
|
1420
|
+
background-image:
|
|
1421
|
+
linear-gradient(45deg, #ccc 25%, transparent 25%),
|
|
1422
|
+
linear-gradient(-45deg, #ccc 25%, transparent 25%),
|
|
1423
|
+
linear-gradient(45deg, transparent 75%, #ccc 75%),
|
|
1424
|
+
linear-gradient(-45deg, transparent 75%, #ccc 75%) !important;
|
|
1425
|
+
background-size: 20px 20px !important;
|
|
1426
|
+
background-position: 0 0, 0 10px, 10px -10px, -10px 0 !important;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/* Musea Addons: Measure label */
|
|
1430
|
+
.musea-measure-label {
|
|
1431
|
+
position: fixed;
|
|
1432
|
+
background: #333;
|
|
1433
|
+
color: #fff;
|
|
1434
|
+
font-size: 11px;
|
|
1435
|
+
padding: 2px 6px;
|
|
1436
|
+
border-radius: 3px;
|
|
1437
|
+
pointer-events: none;
|
|
1438
|
+
z-index: 100000;
|
|
1439
|
+
}
|
|
1440
|
+
</style>
|
|
1441
|
+
</head>
|
|
1442
|
+
<body>
|
|
1443
|
+
<div id="app" class="musea-variant" data-art="${escapeHtml(art.path)}" data-variant="${escapeHtml(variant.name)}">
|
|
1444
|
+
<div class="musea-loading">
|
|
1445
|
+
<div class="musea-spinner"></div>
|
|
1446
|
+
Loading component...
|
|
1447
|
+
</div>
|
|
1448
|
+
</div>
|
|
1449
|
+
<script type="module" src="${previewModuleUrl}"></script>
|
|
1450
|
+
</body>
|
|
1451
|
+
</html>`;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
//#endregion
|
|
1455
|
+
//#region src/manifest.ts
|
|
1456
|
+
/**
|
|
1457
|
+
* Generate the virtual manifest module code containing all art file metadata.
|
|
1458
|
+
*/
|
|
1459
|
+
function generateManifestModule(artFiles) {
|
|
1460
|
+
const arts = Array.from(artFiles.values());
|
|
1461
|
+
return `export const arts = ${JSON.stringify(arts, null, 2)};`;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
//#endregion
|
|
1465
|
+
//#region src/art-module.ts
|
|
1466
|
+
/**
|
|
1467
|
+
* Extract the content of the first <script setup> block from a Vue SFC source.
|
|
1468
|
+
*/
|
|
1469
|
+
function extractScriptSetupContent(source) {
|
|
1470
|
+
const match = source.match(/<script\s+[^>]*setup[^>]*>([\s\S]*?)<\/script>/);
|
|
1471
|
+
return match?.[1]?.trim();
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Parse script setup content into imports and setup body.
|
|
1475
|
+
* Returns the import lines, setup body lines, and all identifiers to expose.
|
|
1476
|
+
*/
|
|
1477
|
+
function parseScriptSetupForArt(content) {
|
|
1478
|
+
const lines = content.split("\n");
|
|
1479
|
+
const imports = [];
|
|
1480
|
+
const setupBody = [];
|
|
1481
|
+
const returnNames = new Set();
|
|
1482
|
+
for (const line of lines) {
|
|
1483
|
+
const trimmed = line.trim();
|
|
1484
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
1485
|
+
if (trimmed.startsWith("import ")) {
|
|
1486
|
+
imports.push(line);
|
|
1487
|
+
const defaultMatch = trimmed.match(/^import\s+(\w+)/);
|
|
1488
|
+
if (defaultMatch && defaultMatch[1] !== "type") returnNames.add(defaultMatch[1]);
|
|
1489
|
+
const namedMatch = trimmed.match(/\{([^}]+)\}/);
|
|
1490
|
+
if (namedMatch) for (const part of namedMatch[1].split(",")) {
|
|
1491
|
+
const name = part.trim().split(/\s+as\s+/).pop()?.trim();
|
|
1492
|
+
if (name && !name.startsWith("type ")) returnNames.add(name);
|
|
1493
|
+
}
|
|
1494
|
+
} else {
|
|
1495
|
+
setupBody.push(line);
|
|
1496
|
+
const constMatch = trimmed.match(/^(?:const|let|var)\s+(\w+)/);
|
|
1497
|
+
if (constMatch) returnNames.add(constMatch[1]);
|
|
1498
|
+
const destructMatch = trimmed.match(/^(?:const|let|var)\s+\{([^}]+)\}/);
|
|
1499
|
+
if (destructMatch) for (const part of destructMatch[1].split(",")) {
|
|
1500
|
+
const name = part.trim().split(/\s*:\s*/).shift()?.trim();
|
|
1501
|
+
if (name) returnNames.add(name);
|
|
1502
|
+
}
|
|
1503
|
+
const arrayMatch = trimmed.match(/^(?:const|let|var)\s+\[([^\]]+)\]/);
|
|
1504
|
+
if (arrayMatch) for (const part of arrayMatch[1].split(",")) {
|
|
1505
|
+
const name = part.trim();
|
|
1506
|
+
if (name && name !== "...") returnNames.add(name);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
returnNames.delete("type");
|
|
1511
|
+
return {
|
|
1512
|
+
imports,
|
|
1513
|
+
setupBody,
|
|
1514
|
+
returnNames: [...returnNames]
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
function generateArtModule(art, filePath) {
|
|
1518
|
+
let componentImportPath;
|
|
1519
|
+
let componentName;
|
|
1520
|
+
if (art.isInline && art.componentPath) {
|
|
1521
|
+
componentImportPath = art.componentPath;
|
|
1522
|
+
componentName = path.basename(art.componentPath, ".vue");
|
|
1523
|
+
} else if (art.metadata.component) {
|
|
1524
|
+
const comp = art.metadata.component;
|
|
1525
|
+
componentImportPath = path.isAbsolute(comp) ? comp : path.resolve(path.dirname(filePath), comp);
|
|
1526
|
+
componentName = path.basename(comp, ".vue");
|
|
1527
|
+
}
|
|
1528
|
+
const scriptSetup = art.scriptSetupContent ? parseScriptSetupForArt(art.scriptSetupContent) : null;
|
|
1529
|
+
let code = `
|
|
1530
|
+
// Auto-generated module for: ${path.basename(filePath)}
|
|
1531
|
+
import { defineComponent, h } from 'vue';
|
|
1532
|
+
`;
|
|
1533
|
+
if (scriptSetup) {
|
|
1534
|
+
const artDir = path.dirname(filePath);
|
|
1535
|
+
for (const imp of scriptSetup.imports) {
|
|
1536
|
+
const resolved = imp.replace(/from\s+(['"])(\.[^'"]+)\1/, (_match, quote, relPath) => {
|
|
1537
|
+
const absPath = path.resolve(artDir, relPath);
|
|
1538
|
+
return `from ${quote}${absPath}${quote}`;
|
|
1539
|
+
});
|
|
1540
|
+
code += `${resolved}\n`;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (componentImportPath && componentName) {
|
|
1544
|
+
const alreadyImported = scriptSetup?.imports.some((imp) => {
|
|
1545
|
+
if (imp.includes(`from '${componentImportPath}'`) || imp.includes(`from "${componentImportPath}"`)) return true;
|
|
1546
|
+
return new RegExp(`^import\\s+${componentName}[\\s,]`).test(imp.trim());
|
|
1547
|
+
});
|
|
1548
|
+
if (!alreadyImported) code += `import ${componentName} from '${componentImportPath}';\n`;
|
|
1549
|
+
code += `export const __component__ = ${componentName};\n`;
|
|
1550
|
+
}
|
|
1551
|
+
code += `
|
|
1552
|
+
export const metadata = ${JSON.stringify(art.metadata)};
|
|
1553
|
+
export const variants = ${JSON.stringify(art.variants)};
|
|
1554
|
+
`;
|
|
1555
|
+
for (const variant of art.variants) {
|
|
1556
|
+
const variantComponentName = toPascalCase(variant.name);
|
|
1557
|
+
let template = variant.template;
|
|
1558
|
+
if (componentName) template = template.replace(/<Self/g, `<${componentName}`).replace(/<\/Self>/g, `</${componentName}>`);
|
|
1559
|
+
const escapedTemplate = template.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1560
|
+
const fullTemplate = `<div data-variant="${variant.name}">${escapedTemplate}</div>`;
|
|
1561
|
+
const componentNames = new Set();
|
|
1562
|
+
if (componentName) componentNames.add(componentName);
|
|
1563
|
+
if (scriptSetup) {
|
|
1564
|
+
for (const name of scriptSetup.returnNames) if (/^[A-Z]/.test(name)) componentNames.add(name);
|
|
1565
|
+
}
|
|
1566
|
+
const components = componentNames.size > 0 ? ` components: { ${[...componentNames].join(", ")} },\n` : "";
|
|
1567
|
+
if (scriptSetup && scriptSetup.setupBody.length > 0) code += `
|
|
1568
|
+
export const ${variantComponentName} = defineComponent({
|
|
1569
|
+
name: '${variantComponentName}',
|
|
1570
|
+
${components} setup() {
|
|
1571
|
+
${scriptSetup.setupBody.map((l) => ` ${l}`).join("\n")}
|
|
1572
|
+
return { ${scriptSetup.returnNames.join(", ")} };
|
|
1573
|
+
},
|
|
1574
|
+
template: \`${fullTemplate}\`,
|
|
1575
|
+
});
|
|
1576
|
+
`;
|
|
1577
|
+
else if (componentName) code += `
|
|
1578
|
+
export const ${variantComponentName} = {
|
|
1579
|
+
name: '${variantComponentName}',
|
|
1580
|
+
${components} template: \`${fullTemplate}\`,
|
|
1581
|
+
};
|
|
1582
|
+
`;
|
|
1583
|
+
else code += `
|
|
1584
|
+
export const ${variantComponentName} = {
|
|
1585
|
+
name: '${variantComponentName}',
|
|
1586
|
+
template: \`${fullTemplate}\`,
|
|
1587
|
+
};
|
|
1588
|
+
`;
|
|
1589
|
+
}
|
|
1590
|
+
const defaultVariant = art.variants.find((v) => v.isDefault) || art.variants[0];
|
|
1591
|
+
if (defaultVariant) code += `
|
|
1592
|
+
export default ${toPascalCase(defaultVariant.name)};
|
|
1593
|
+
`;
|
|
1594
|
+
return code;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
//#endregion
|
|
1598
|
+
//#region src/server-middleware.ts
|
|
1599
|
+
/**
|
|
1600
|
+
* Register all Musea middleware on the given dev server.
|
|
1601
|
+
*
|
|
1602
|
+
* This sets up:
|
|
1603
|
+
* - Gallery SPA route (serves built SPA or inline HTML fallback)
|
|
1604
|
+
* - Gallery static assets (/assets/)
|
|
1605
|
+
* - axe-core vendor script
|
|
1606
|
+
* - Preview module route
|
|
1607
|
+
* - VRT preview route
|
|
1608
|
+
* - Art module route
|
|
1609
|
+
*/
|
|
1610
|
+
function registerMiddleware(devServer, ctx) {
|
|
1611
|
+
const { basePath, themeConfig, artFiles } = ctx;
|
|
1612
|
+
devServer.middlewares.use(basePath, async (req, res, next) => {
|
|
1613
|
+
const url = req.url || "/";
|
|
1614
|
+
if (url === "/" || url === "/index.html" || url.startsWith("/tokens") || url.startsWith("/component/") || url.startsWith("/tests")) {
|
|
1615
|
+
const galleryDistDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "gallery");
|
|
1616
|
+
const indexHtmlPath = path.join(galleryDistDir, "index.html");
|
|
1617
|
+
try {
|
|
1618
|
+
await fs.promises.access(indexHtmlPath);
|
|
1619
|
+
let html = await fs.promises.readFile(indexHtmlPath, "utf-8");
|
|
1620
|
+
const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : "";
|
|
1621
|
+
html = html.replace("</head>", `<script>window.__MUSEA_BASE_PATH__='${basePath}';${themeScript}</script></head>`);
|
|
1622
|
+
res.setHeader("Content-Type", "text/html");
|
|
1623
|
+
res.end(html);
|
|
1624
|
+
return;
|
|
1625
|
+
} catch {
|
|
1626
|
+
const html = generateGalleryHtml(basePath, themeConfig);
|
|
1627
|
+
res.setHeader("Content-Type", "text/html");
|
|
1628
|
+
res.end(html);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (url.startsWith("/assets/")) {
|
|
1633
|
+
const galleryDistDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "gallery");
|
|
1634
|
+
const filePath = path.join(galleryDistDir, url);
|
|
1635
|
+
try {
|
|
1636
|
+
const stat = await fs.promises.stat(filePath);
|
|
1637
|
+
if (stat.isFile()) {
|
|
1638
|
+
const content = await fs.promises.readFile(filePath);
|
|
1639
|
+
const ext = path.extname(filePath);
|
|
1640
|
+
const mimeTypes = {
|
|
1641
|
+
".js": "application/javascript",
|
|
1642
|
+
".css": "text/css",
|
|
1643
|
+
".svg": "image/svg+xml",
|
|
1644
|
+
".png": "image/png",
|
|
1645
|
+
".ico": "image/x-icon",
|
|
1646
|
+
".woff2": "font/woff2",
|
|
1647
|
+
".woff": "font/woff"
|
|
1648
|
+
};
|
|
1649
|
+
res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream");
|
|
1650
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1651
|
+
res.end(content);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
} catch {}
|
|
1655
|
+
}
|
|
1656
|
+
next();
|
|
1657
|
+
});
|
|
1658
|
+
devServer.middlewares.use(`${basePath}/vendor/axe-core.min.js`, async (_req, res, _next) => {
|
|
1659
|
+
try {
|
|
1660
|
+
const require = createRequire(import.meta.url);
|
|
1661
|
+
const axeCorePath = require.resolve("axe-core/axe.min.js");
|
|
1662
|
+
const content = await fs.promises.readFile(axeCorePath, "utf-8");
|
|
1663
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
1664
|
+
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
1665
|
+
res.end(content);
|
|
1666
|
+
} catch {
|
|
1667
|
+
res.statusCode = 404;
|
|
1668
|
+
res.end("axe-core not installed");
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
devServer.middlewares.use(`${basePath}/preview-module`, async (req, res, _next) => {
|
|
1672
|
+
const url = new URL(req.url || "", `http://localhost`);
|
|
1673
|
+
const artPath = url.searchParams.get("art");
|
|
1674
|
+
const variantName = url.searchParams.get("variant");
|
|
1675
|
+
if (!artPath || !variantName) {
|
|
1676
|
+
res.statusCode = 400;
|
|
1677
|
+
res.end("Missing art or variant parameter");
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
const art = artFiles.get(artPath);
|
|
1681
|
+
if (!art) {
|
|
1682
|
+
res.statusCode = 404;
|
|
1683
|
+
res.end("Art not found");
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const variant = art.variants.find((v) => v.name === variantName);
|
|
1687
|
+
if (!variant) {
|
|
1688
|
+
res.statusCode = 404;
|
|
1689
|
+
res.end("Variant not found");
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
const variantComponentName = toPascalCase(variant.name);
|
|
1693
|
+
const moduleCode = generatePreviewModule(art, variantComponentName, variant.name, ctx.resolvedPreviewCss, ctx.resolvedPreviewSetup);
|
|
1694
|
+
try {
|
|
1695
|
+
const result = await devServer.transformRequest(`virtual:musea-preview:${artPath}:${variantName}`);
|
|
1696
|
+
if (result) {
|
|
1697
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
1698
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
1699
|
+
res.end(result.code);
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
} catch {}
|
|
1703
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
1704
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
1705
|
+
res.end(moduleCode);
|
|
1706
|
+
});
|
|
1707
|
+
devServer.middlewares.use(`${basePath}/preview`, async (req, res, _next) => {
|
|
1708
|
+
const url = new URL(req.url || "", `http://localhost`);
|
|
1709
|
+
const artPath = url.searchParams.get("art");
|
|
1710
|
+
const variantName = url.searchParams.get("variant");
|
|
1711
|
+
if (!artPath || !variantName) {
|
|
1712
|
+
res.statusCode = 400;
|
|
1713
|
+
res.end("Missing art or variant parameter");
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const art = artFiles.get(artPath);
|
|
1717
|
+
if (!art) {
|
|
1718
|
+
res.statusCode = 404;
|
|
1719
|
+
res.end("Art not found");
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
const variant = art.variants.find((v) => v.name === variantName);
|
|
1723
|
+
if (!variant) {
|
|
1724
|
+
res.statusCode = 404;
|
|
1725
|
+
res.end("Variant not found");
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
const config = devServer.config;
|
|
1729
|
+
const html = generatePreviewHtml(art, variant, basePath, config.base);
|
|
1730
|
+
res.setHeader("Content-Type", "text/html");
|
|
1731
|
+
res.end(html);
|
|
1732
|
+
});
|
|
1733
|
+
devServer.middlewares.use(`${basePath}/art`, async (req, res, next) => {
|
|
1734
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
1735
|
+
const artPath = decodeURIComponent(url.pathname.slice(1));
|
|
1736
|
+
if (!artPath) {
|
|
1737
|
+
next();
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
const art = artFiles.get(artPath);
|
|
1741
|
+
if (!art) {
|
|
1742
|
+
res.statusCode = 404;
|
|
1743
|
+
res.end("Art not found: " + artPath);
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
try {
|
|
1747
|
+
const virtualId = `virtual:musea-art:${artPath}`;
|
|
1748
|
+
const result = await devServer.transformRequest(virtualId);
|
|
1749
|
+
if (result) {
|
|
1750
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
1751
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
1752
|
+
res.end(result.code);
|
|
1753
|
+
} else {
|
|
1754
|
+
const moduleCode = generateArtModule(art, artPath);
|
|
1755
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
1756
|
+
res.end(moduleCode);
|
|
1757
|
+
}
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
console.error("[musea] Failed to transform art module:", err);
|
|
1760
|
+
const moduleCode = generateArtModule(art, artPath);
|
|
1761
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
1762
|
+
res.end(moduleCode);
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
2557
1766
|
|
|
2558
|
-
|
|
2559
|
-
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region src/tokens/parser.ts
|
|
1769
|
+
/**
|
|
1770
|
+
* Parse Style Dictionary tokens file.
|
|
1771
|
+
*/
|
|
1772
|
+
async function parseTokens(tokensPath) {
|
|
1773
|
+
const absolutePath = path.resolve(tokensPath);
|
|
1774
|
+
const stat = await fs.promises.stat(absolutePath);
|
|
1775
|
+
if (stat.isDirectory()) return parseTokenDirectory(absolutePath);
|
|
1776
|
+
const content = await fs.promises.readFile(absolutePath, "utf-8");
|
|
1777
|
+
const tokens = JSON.parse(content);
|
|
1778
|
+
return flattenTokens(tokens);
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Parse tokens from a directory.
|
|
1782
|
+
*/
|
|
1783
|
+
async function parseTokenDirectory(dirPath) {
|
|
1784
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
1785
|
+
const categories = [];
|
|
1786
|
+
for (const entry of entries) if (entry.isFile() && (entry.name.endsWith(".json") || entry.name.endsWith(".tokens.json"))) {
|
|
1787
|
+
const filePath = path.join(dirPath, entry.name);
|
|
1788
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
1789
|
+
const tokens = JSON.parse(content);
|
|
1790
|
+
const categoryName = path.basename(entry.name, path.extname(entry.name)).replace(".tokens", "");
|
|
1791
|
+
categories.push({
|
|
1792
|
+
name: formatCategoryName(categoryName),
|
|
1793
|
+
tokens: extractTokens(tokens),
|
|
1794
|
+
subcategories: extractSubcategories(tokens)
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
return categories;
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Flatten nested token structure into categories.
|
|
1801
|
+
*/
|
|
1802
|
+
function flattenTokens(tokens, prefix = []) {
|
|
1803
|
+
const categories = [];
|
|
1804
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
1805
|
+
if (isTokenValue(value)) continue;
|
|
1806
|
+
if (typeof value === "object" && value !== null) {
|
|
1807
|
+
const categoryTokens = extractTokens(value);
|
|
1808
|
+
const subcategories = flattenTokens(value, [...prefix, key]);
|
|
1809
|
+
if (Object.keys(categoryTokens).length > 0 || subcategories.length > 0) categories.push({
|
|
1810
|
+
name: formatCategoryName(key),
|
|
1811
|
+
tokens: categoryTokens,
|
|
1812
|
+
subcategories: subcategories.length > 0 ? subcategories : void 0
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return categories;
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Extract token values from an object.
|
|
1820
|
+
*/
|
|
1821
|
+
function extractTokens(obj) {
|
|
1822
|
+
const tokens = {};
|
|
1823
|
+
for (const [key, value] of Object.entries(obj)) if (isTokenValue(value)) tokens[key] = normalizeToken(value);
|
|
1824
|
+
return tokens;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Extract subcategories from an object.
|
|
1828
|
+
*/
|
|
1829
|
+
function extractSubcategories(obj) {
|
|
1830
|
+
const subcategories = [];
|
|
1831
|
+
for (const [key, value] of Object.entries(obj)) if (!isTokenValue(value) && typeof value === "object" && value !== null) {
|
|
1832
|
+
const categoryTokens = extractTokens(value);
|
|
1833
|
+
const nested = extractSubcategories(value);
|
|
1834
|
+
if (Object.keys(categoryTokens).length > 0 || nested && nested.length > 0) subcategories.push({
|
|
1835
|
+
name: formatCategoryName(key),
|
|
1836
|
+
tokens: categoryTokens,
|
|
1837
|
+
subcategories: nested
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
return subcategories.length > 0 ? subcategories : void 0;
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Check if a value is a token definition.
|
|
1844
|
+
*/
|
|
1845
|
+
function isTokenValue(value) {
|
|
1846
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1847
|
+
const obj = value;
|
|
1848
|
+
return "value" in obj && (typeof obj.value === "string" || typeof obj.value === "number") || "$value" in obj && (typeof obj.$value === "string" || typeof obj.$value === "number");
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Normalize token to DesignToken interface.
|
|
1852
|
+
*/
|
|
1853
|
+
function normalizeToken(raw) {
|
|
1854
|
+
const token = {
|
|
1855
|
+
value: raw.value ?? raw.$value,
|
|
1856
|
+
type: raw.type ?? raw.$type,
|
|
1857
|
+
description: raw.description,
|
|
1858
|
+
attributes: raw.attributes
|
|
1859
|
+
};
|
|
1860
|
+
if (raw.$tier === "primitive" || raw.$tier === "semantic") token.$tier = raw.$tier;
|
|
1861
|
+
if (typeof raw.$reference === "string") token.$reference = raw.$reference;
|
|
1862
|
+
return token;
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Format category name for display.
|
|
1866
|
+
*/
|
|
1867
|
+
function formatCategoryName(name) {
|
|
1868
|
+
return name.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
//#endregion
|
|
1872
|
+
//#region src/tokens/resolver.ts
|
|
1873
|
+
const REFERENCE_PATTERN = /^\{(.+)\}$/;
|
|
1874
|
+
const MAX_RESOLVE_DEPTH = 10;
|
|
1875
|
+
/**
|
|
1876
|
+
* Flatten nested categories into a flat map keyed by dot-path.
|
|
1877
|
+
*/
|
|
1878
|
+
function buildTokenMap(categories, prefix = []) {
|
|
1879
|
+
const map = {};
|
|
1880
|
+
for (const cat of categories) {
|
|
1881
|
+
const catKey = cat.name.toLowerCase().replace(/\s+/g, "-");
|
|
1882
|
+
const catPath = [...prefix, catKey];
|
|
1883
|
+
for (const [name, token] of Object.entries(cat.tokens)) {
|
|
1884
|
+
const dotPath = [...catPath, name].join(".");
|
|
1885
|
+
map[dotPath] = token;
|
|
1886
|
+
}
|
|
1887
|
+
if (cat.subcategories) {
|
|
1888
|
+
const subMap = buildTokenMap(cat.subcategories, catPath);
|
|
1889
|
+
Object.assign(map, subMap);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return map;
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Resolve references in categories, setting $tier, $reference, and $resolvedValue.
|
|
1896
|
+
*/
|
|
1897
|
+
function resolveReferences(categories, tokenMap) {
|
|
1898
|
+
for (const cat of categories) {
|
|
1899
|
+
for (const token of Object.values(cat.tokens)) resolveTokenReference(token, tokenMap);
|
|
1900
|
+
if (cat.subcategories) resolveReferences(cat.subcategories, tokenMap);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
function resolveTokenReference(token, tokenMap) {
|
|
1904
|
+
if (typeof token.value === "string") {
|
|
1905
|
+
const match = token.value.match(REFERENCE_PATTERN);
|
|
1906
|
+
if (match) {
|
|
1907
|
+
token.$tier = token.$tier ?? "semantic";
|
|
1908
|
+
token.$reference = match[1];
|
|
1909
|
+
token.$resolvedValue = resolveValue(match[1], tokenMap, 0, new Set());
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
token.$tier = token.$tier ?? "primitive";
|
|
1914
|
+
}
|
|
1915
|
+
function resolveValue(ref, tokenMap, depth, visited) {
|
|
1916
|
+
if (depth >= MAX_RESOLVE_DEPTH || visited.has(ref)) return void 0;
|
|
1917
|
+
visited.add(ref);
|
|
1918
|
+
const target = tokenMap[ref];
|
|
1919
|
+
if (!target) return void 0;
|
|
1920
|
+
if (typeof target.value === "string") {
|
|
1921
|
+
const match = target.value.match(REFERENCE_PATTERN);
|
|
1922
|
+
if (match) return resolveValue(match[1], tokenMap, depth + 1, visited);
|
|
1923
|
+
}
|
|
1924
|
+
return target.value;
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Read raw JSON token file.
|
|
1928
|
+
*/
|
|
1929
|
+
async function readRawTokenFile(tokensPath) {
|
|
1930
|
+
const content = await fs.promises.readFile(tokensPath, "utf-8");
|
|
1931
|
+
return JSON.parse(content);
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Write raw JSON token file atomically (write tmp, rename).
|
|
1935
|
+
*/
|
|
1936
|
+
async function writeRawTokenFile(tokensPath, data) {
|
|
1937
|
+
const tmpPath = tokensPath + ".tmp";
|
|
1938
|
+
await fs.promises.writeFile(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1939
|
+
await fs.promises.rename(tmpPath, tokensPath);
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Set a token at a dot-separated path in the raw JSON structure.
|
|
1943
|
+
*/
|
|
1944
|
+
function setTokenAtPath(data, dotPath, token) {
|
|
1945
|
+
const parts = dotPath.split(".");
|
|
1946
|
+
let current = data;
|
|
1947
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1948
|
+
const key = parts[i];
|
|
1949
|
+
if (typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
1950
|
+
current = current[key];
|
|
1951
|
+
}
|
|
1952
|
+
const leafKey = parts[parts.length - 1];
|
|
1953
|
+
const raw = { value: token.value };
|
|
1954
|
+
if (token.type) raw.type = token.type;
|
|
1955
|
+
if (token.description) raw.description = token.description;
|
|
1956
|
+
if (token.$tier) raw.$tier = token.$tier;
|
|
1957
|
+
if (token.$reference) raw.$reference = token.$reference;
|
|
1958
|
+
if (token.attributes) raw.attributes = token.attributes;
|
|
1959
|
+
current[leafKey] = raw;
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Delete a token at a dot-separated path, cleaning empty parents.
|
|
1963
|
+
*/
|
|
1964
|
+
function deleteTokenAtPath(data, dotPath) {
|
|
1965
|
+
const parts = dotPath.split(".");
|
|
1966
|
+
const parents = [];
|
|
1967
|
+
let current = data;
|
|
1968
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1969
|
+
const key = parts[i];
|
|
1970
|
+
if (typeof current[key] !== "object" || current[key] === null) return false;
|
|
1971
|
+
parents.push({
|
|
1972
|
+
obj: current,
|
|
1973
|
+
key
|
|
1974
|
+
});
|
|
1975
|
+
current = current[key];
|
|
1976
|
+
}
|
|
1977
|
+
const leafKey = parts[parts.length - 1];
|
|
1978
|
+
if (!(leafKey in current)) return false;
|
|
1979
|
+
delete current[leafKey];
|
|
1980
|
+
for (let i = parents.length - 1; i >= 0; i--) {
|
|
1981
|
+
const { obj, key } = parents[i];
|
|
1982
|
+
const child = obj[key];
|
|
1983
|
+
if (Object.keys(child).length === 0) delete obj[key];
|
|
1984
|
+
else break;
|
|
1985
|
+
}
|
|
1986
|
+
return true;
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Validate that a semantic reference points to an existing token and has no cycles.
|
|
1990
|
+
*/
|
|
1991
|
+
function validateSemanticReference(tokenMap, reference, selfPath) {
|
|
1992
|
+
if (!tokenMap[reference]) return {
|
|
1993
|
+
valid: false,
|
|
1994
|
+
error: `Reference target "${reference}" does not exist`
|
|
1995
|
+
};
|
|
1996
|
+
const visited = new Set();
|
|
1997
|
+
if (selfPath) visited.add(selfPath);
|
|
1998
|
+
let current = reference;
|
|
1999
|
+
let depth = 0;
|
|
2000
|
+
while (depth < MAX_RESOLVE_DEPTH) {
|
|
2001
|
+
if (visited.has(current)) return {
|
|
2002
|
+
valid: false,
|
|
2003
|
+
error: `Circular reference detected at "${current}"`
|
|
2004
|
+
};
|
|
2005
|
+
visited.add(current);
|
|
2006
|
+
const target = tokenMap[current];
|
|
2007
|
+
if (!target) break;
|
|
2008
|
+
if (typeof target.value === "string") {
|
|
2009
|
+
const match = target.value.match(REFERENCE_PATTERN);
|
|
2010
|
+
if (match) {
|
|
2011
|
+
current = match[1];
|
|
2012
|
+
depth++;
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
break;
|
|
2017
|
+
}
|
|
2018
|
+
if (depth >= MAX_RESOLVE_DEPTH) return {
|
|
2019
|
+
valid: false,
|
|
2020
|
+
error: "Reference chain too deep (max 10)"
|
|
2021
|
+
};
|
|
2022
|
+
return { valid: true };
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Find all tokens that reference the given path.
|
|
2026
|
+
*/
|
|
2027
|
+
function findDependentTokens(tokenMap, targetPath) {
|
|
2028
|
+
const dependents = [];
|
|
2029
|
+
for (const [path$1, token] of Object.entries(tokenMap)) if (typeof token.value === "string") {
|
|
2030
|
+
const match = token.value.match(REFERENCE_PATTERN);
|
|
2031
|
+
if (match && match[1] === targetPath) dependents.push(path$1);
|
|
2032
|
+
}
|
|
2033
|
+
return dependents;
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Normalize a token value for comparison.
|
|
2037
|
+
* - Lowercase, trim
|
|
2038
|
+
* - Leading-zero: `.5rem` -> `0.5rem`
|
|
2039
|
+
* - Short hex: `#fff` -> `#ffffff`
|
|
2040
|
+
*/
|
|
2041
|
+
function normalizeTokenValue(value) {
|
|
2042
|
+
let v = String(value).trim().toLowerCase();
|
|
2043
|
+
const shortHex = v.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$/);
|
|
2044
|
+
if (shortHex) {
|
|
2045
|
+
const [, r, g, b, a] = shortHex;
|
|
2046
|
+
v = a ? `#${r}${r}${g}${g}${b}${b}${a}${a}` : `#${r}${r}${g}${g}${b}${b}`;
|
|
2047
|
+
}
|
|
2048
|
+
v = v.replace(/(?<![0-9])\.(\d)/g, "0.$1");
|
|
2049
|
+
return v;
|
|
2050
|
+
}
|
|
2051
|
+
const STYLE_BLOCK_RE = /<style[^>]*>([\s\S]*?)<\/style>/g;
|
|
2052
|
+
const CSS_PROPERTY_RE = /^\s*([\w-]+)\s*:\s*(.+?)\s*;?\s*$/;
|
|
2053
|
+
/**
|
|
2054
|
+
* Scan art file sources for token value matches in `<style>` blocks.
|
|
2055
|
+
*/
|
|
2056
|
+
function scanTokenUsage(artFiles, tokenMap) {
|
|
2057
|
+
const valueLookup = new Map();
|
|
2058
|
+
for (const [tokenPath, token] of Object.entries(tokenMap)) {
|
|
2059
|
+
const rawValue = token.$resolvedValue ?? token.value;
|
|
2060
|
+
const normalized = normalizeTokenValue(rawValue);
|
|
2061
|
+
if (!normalized) continue;
|
|
2062
|
+
const existing = valueLookup.get(normalized);
|
|
2063
|
+
if (existing) existing.push(tokenPath);
|
|
2064
|
+
else valueLookup.set(normalized, [tokenPath]);
|
|
2065
|
+
}
|
|
2066
|
+
const usageMap = {};
|
|
2067
|
+
for (const [artPath, artInfo] of artFiles) {
|
|
2068
|
+
let source;
|
|
2069
|
+
try {
|
|
2070
|
+
source = fs.readFileSync(artPath, "utf-8");
|
|
2071
|
+
} catch {
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
const allLines = source.split("\n");
|
|
2075
|
+
const styleRegions = [];
|
|
2076
|
+
let match;
|
|
2077
|
+
STYLE_BLOCK_RE.lastIndex = 0;
|
|
2078
|
+
while ((match = STYLE_BLOCK_RE.exec(source)) !== null) {
|
|
2079
|
+
const beforeMatch = source.slice(0, match.index);
|
|
2080
|
+
const startTag = source.slice(match.index, match.index + match[0].indexOf(match[1]));
|
|
2081
|
+
const startLine = beforeMatch.split("\n").length + startTag.split("\n").length - 1;
|
|
2082
|
+
styleRegions.push({
|
|
2083
|
+
startLine,
|
|
2084
|
+
content: match[1]
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
for (const region of styleRegions) {
|
|
2088
|
+
const lines = region.content.split("\n");
|
|
2089
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2090
|
+
const line = lines[i];
|
|
2091
|
+
const propMatch = line.match(CSS_PROPERTY_RE);
|
|
2092
|
+
if (!propMatch) continue;
|
|
2093
|
+
const property = propMatch[1];
|
|
2094
|
+
const valueStr = propMatch[2];
|
|
2095
|
+
const valueParts = valueStr.split(/\s+/);
|
|
2096
|
+
for (const part of valueParts) {
|
|
2097
|
+
const normalizedPart = normalizeTokenValue(part);
|
|
2098
|
+
const matchingTokens = valueLookup.get(normalizedPart);
|
|
2099
|
+
if (!matchingTokens) continue;
|
|
2100
|
+
const lineNumber = region.startLine + i;
|
|
2101
|
+
const lineContent = allLines[lineNumber - 1]?.trim() ?? line.trim();
|
|
2102
|
+
for (const tokenPath of matchingTokens) {
|
|
2103
|
+
if (!usageMap[tokenPath]) usageMap[tokenPath] = [];
|
|
2104
|
+
let entry = usageMap[tokenPath].find((e) => e.artPath === artPath);
|
|
2105
|
+
if (!entry) {
|
|
2106
|
+
entry = {
|
|
2107
|
+
artPath,
|
|
2108
|
+
artTitle: artInfo.metadata.title,
|
|
2109
|
+
artCategory: artInfo.metadata.category,
|
|
2110
|
+
matches: []
|
|
2111
|
+
};
|
|
2112
|
+
usageMap[tokenPath].push(entry);
|
|
2113
|
+
}
|
|
2114
|
+
if (!entry.matches.some((m) => m.line === lineNumber && m.property === property)) entry.matches.push({
|
|
2115
|
+
line: lineNumber,
|
|
2116
|
+
lineContent,
|
|
2117
|
+
property
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
return usageMap;
|
|
2560
2125
|
}
|
|
2561
|
-
`;
|
|
2562
|
-
function generatePreviewModule(art, variantComponentName, variantName, cssImports = [], previewSetup = null) {
|
|
2563
|
-
const artModuleId = `virtual:musea-art:${art.path}`;
|
|
2564
|
-
const escapedVariantName = escapeTemplate(variantName);
|
|
2565
|
-
const cssImportStatements = cssImports.map((cssPath) => `import '${cssPath}';`).join("\n");
|
|
2566
|
-
const setupImport = previewSetup ? `import __museaPreviewSetup from '${previewSetup}';` : "";
|
|
2567
|
-
const setupCall = previewSetup ? "await __museaPreviewSetup(app);" : "";
|
|
2568
|
-
return `
|
|
2569
|
-
${cssImportStatements}
|
|
2570
|
-
${setupImport}
|
|
2571
|
-
import { createApp, reactive, h } from 'vue';
|
|
2572
|
-
import * as artModule from '${artModuleId}';
|
|
2573
|
-
|
|
2574
|
-
const container = document.getElementById('app');
|
|
2575
|
-
|
|
2576
|
-
${MUSEA_ADDONS_INIT_CODE}
|
|
2577
|
-
|
|
2578
|
-
let currentApp = null;
|
|
2579
|
-
const propsOverride = reactive({});
|
|
2580
|
-
const slotsOverride = reactive({ default: '' });
|
|
2581
|
-
|
|
2582
|
-
window.__museaSetProps = (props) => {
|
|
2583
|
-
// Clear old keys
|
|
2584
|
-
for (const key of Object.keys(propsOverride)) {
|
|
2585
|
-
delete propsOverride[key];
|
|
2586
|
-
}
|
|
2587
|
-
Object.assign(propsOverride, props);
|
|
2588
|
-
};
|
|
2589
|
-
|
|
2590
|
-
window.__museaSetSlots = (slots) => {
|
|
2591
|
-
for (const key of Object.keys(slotsOverride)) {
|
|
2592
|
-
delete slotsOverride[key];
|
|
2593
|
-
}
|
|
2594
|
-
Object.assign(slotsOverride, slots);
|
|
2595
|
-
};
|
|
2596
|
-
|
|
2597
|
-
async function mount() {
|
|
2598
|
-
try {
|
|
2599
|
-
// Get the specific variant component
|
|
2600
|
-
const VariantComponent = artModule['${variantComponentName}'];
|
|
2601
|
-
const RawComponent = artModule.__component__;
|
|
2602
2126
|
|
|
2603
|
-
|
|
2604
|
-
|
|
2127
|
+
//#endregion
|
|
2128
|
+
//#region src/tokens/generator.ts
|
|
2129
|
+
/**
|
|
2130
|
+
* Generate HTML documentation for tokens.
|
|
2131
|
+
*/
|
|
2132
|
+
function generateTokensHtml(categories) {
|
|
2133
|
+
const renderToken = (name, token) => {
|
|
2134
|
+
const isColor = typeof token.value === "string" && (token.value.startsWith("#") || token.value.startsWith("rgb") || token.value.startsWith("hsl") || token.type === "color");
|
|
2135
|
+
return `
|
|
2136
|
+
<div class="token">
|
|
2137
|
+
<div class="token-preview">
|
|
2138
|
+
${isColor ? `<div class="color-swatch" style="background: ${token.value}"></div>` : ""}
|
|
2139
|
+
</div>
|
|
2140
|
+
<div class="token-info">
|
|
2141
|
+
<div class="token-name">${name}</div>
|
|
2142
|
+
<div class="token-value">${token.value}</div>
|
|
2143
|
+
${token.description ? `<div class="token-description">${token.description}</div>` : ""}
|
|
2144
|
+
</div>
|
|
2145
|
+
</div>
|
|
2146
|
+
`;
|
|
2147
|
+
};
|
|
2148
|
+
const renderCategory = (category, level = 2) => {
|
|
2149
|
+
const heading = `h${Math.min(level, 6)}`;
|
|
2150
|
+
let html = `<${heading}>${category.name}</${heading}>`;
|
|
2151
|
+
html += "<div class=\"tokens-grid\">";
|
|
2152
|
+
for (const [name, token] of Object.entries(category.tokens)) html += renderToken(name, token);
|
|
2153
|
+
html += "</div>";
|
|
2154
|
+
if (category.subcategories) for (const sub of category.subcategories) html += renderCategory(sub, level + 1);
|
|
2155
|
+
return html;
|
|
2156
|
+
};
|
|
2157
|
+
return `<!DOCTYPE html>
|
|
2158
|
+
<html lang="en">
|
|
2159
|
+
<head>
|
|
2160
|
+
<meta charset="UTF-8">
|
|
2161
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2162
|
+
<title>Design Tokens - Musea</title>
|
|
2163
|
+
<style>
|
|
2164
|
+
:root {
|
|
2165
|
+
--musea-bg: #0d0d0d;
|
|
2166
|
+
--musea-bg-secondary: #1a1815;
|
|
2167
|
+
--musea-text: #e6e9f0;
|
|
2168
|
+
--musea-text-muted: #7b8494;
|
|
2169
|
+
--musea-accent: #a34828;
|
|
2170
|
+
--musea-border: #3a3530;
|
|
2171
|
+
}
|
|
2172
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2173
|
+
body {
|
|
2174
|
+
font-family: 'Inter', -apple-system, sans-serif;
|
|
2175
|
+
background: var(--musea-bg);
|
|
2176
|
+
color: var(--musea-text);
|
|
2177
|
+
line-height: 1.6;
|
|
2178
|
+
padding: 2rem;
|
|
2179
|
+
}
|
|
2180
|
+
h1 { margin-bottom: 2rem; color: var(--musea-accent); }
|
|
2181
|
+
h2 { margin: 2rem 0 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--musea-border); }
|
|
2182
|
+
h3, h4, h5, h6 { margin: 1.5rem 0 0.75rem; }
|
|
2183
|
+
.tokens-grid {
|
|
2184
|
+
display: grid;
|
|
2185
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
2186
|
+
gap: 1rem;
|
|
2187
|
+
margin-bottom: 1.5rem;
|
|
2188
|
+
}
|
|
2189
|
+
.token {
|
|
2190
|
+
background: var(--musea-bg-secondary);
|
|
2191
|
+
border: 1px solid var(--musea-border);
|
|
2192
|
+
border-radius: 8px;
|
|
2193
|
+
padding: 1rem;
|
|
2194
|
+
display: flex;
|
|
2195
|
+
gap: 1rem;
|
|
2196
|
+
align-items: center;
|
|
2197
|
+
}
|
|
2198
|
+
.token-preview {
|
|
2199
|
+
flex-shrink: 0;
|
|
2200
|
+
width: 48px;
|
|
2201
|
+
height: 48px;
|
|
2202
|
+
display: flex;
|
|
2203
|
+
align-items: center;
|
|
2204
|
+
justify-content: center;
|
|
2205
|
+
}
|
|
2206
|
+
.color-swatch {
|
|
2207
|
+
width: 48px;
|
|
2208
|
+
height: 48px;
|
|
2209
|
+
border-radius: 8px;
|
|
2210
|
+
border: 1px solid var(--musea-border);
|
|
2211
|
+
}
|
|
2212
|
+
.token-info {
|
|
2213
|
+
flex: 1;
|
|
2214
|
+
min-width: 0;
|
|
2215
|
+
}
|
|
2216
|
+
.token-name {
|
|
2217
|
+
font-weight: 600;
|
|
2218
|
+
font-family: 'JetBrains Mono', monospace;
|
|
2219
|
+
font-size: 0.875rem;
|
|
2220
|
+
}
|
|
2221
|
+
.token-value {
|
|
2222
|
+
color: var(--musea-text-muted);
|
|
2223
|
+
font-family: 'JetBrains Mono', monospace;
|
|
2224
|
+
font-size: 0.75rem;
|
|
2225
|
+
word-break: break-all;
|
|
2226
|
+
}
|
|
2227
|
+
.token-description {
|
|
2228
|
+
color: var(--musea-text-muted);
|
|
2229
|
+
font-size: 0.75rem;
|
|
2230
|
+
margin-top: 0.25rem;
|
|
2605
2231
|
}
|
|
2232
|
+
</style>
|
|
2233
|
+
</head>
|
|
2234
|
+
<body>
|
|
2235
|
+
<h1>Design Tokens</h1>
|
|
2236
|
+
${categories.map((cat) => renderCategory(cat)).join("")}
|
|
2237
|
+
</body>
|
|
2238
|
+
</html>`;
|
|
2239
|
+
}
|
|
2240
|
+
/**
|
|
2241
|
+
* Generate Markdown documentation for tokens.
|
|
2242
|
+
*/
|
|
2243
|
+
function generateTokensMarkdown(categories) {
|
|
2244
|
+
const renderCategory = (category, level = 2) => {
|
|
2245
|
+
const heading = "#".repeat(level);
|
|
2246
|
+
let md = `\n${heading} ${category.name}\n\n`;
|
|
2247
|
+
if (Object.keys(category.tokens).length > 0) {
|
|
2248
|
+
md += "| Token | Value | Description |\n";
|
|
2249
|
+
md += "|-------|-------|-------------|\n";
|
|
2250
|
+
for (const [name, token] of Object.entries(category.tokens)) {
|
|
2251
|
+
const desc = token.description || "-";
|
|
2252
|
+
md += `| \`${name}\` | \`${token.value}\` | ${desc} |\n`;
|
|
2253
|
+
}
|
|
2254
|
+
md += "\n";
|
|
2255
|
+
}
|
|
2256
|
+
if (category.subcategories) for (const sub of category.subcategories) md += renderCategory(sub, level + 1);
|
|
2257
|
+
return md;
|
|
2258
|
+
};
|
|
2259
|
+
let markdown = "# Design Tokens\n\n";
|
|
2260
|
+
markdown += `> Generated by Musea on ${new Date().toISOString()}\n`;
|
|
2261
|
+
for (const category of categories) markdown += renderCategory(category);
|
|
2262
|
+
return markdown;
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Style Dictionary plugin for Musea.
|
|
2266
|
+
* Parses tokens and generates documentation in the specified format.
|
|
2267
|
+
*/
|
|
2268
|
+
async function processStyleDictionary(config) {
|
|
2269
|
+
const categories = await parseTokens(config.tokensPath);
|
|
2270
|
+
const outputDir = config.outputDir ?? ".vize/tokens";
|
|
2271
|
+
const outputFormat = config.outputFormat ?? "html";
|
|
2272
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
2273
|
+
let content;
|
|
2274
|
+
let filename;
|
|
2275
|
+
switch (outputFormat) {
|
|
2276
|
+
case "html":
|
|
2277
|
+
content = generateTokensHtml(categories);
|
|
2278
|
+
filename = "tokens.html";
|
|
2279
|
+
break;
|
|
2280
|
+
case "markdown":
|
|
2281
|
+
content = generateTokensMarkdown(categories);
|
|
2282
|
+
filename = "tokens.md";
|
|
2283
|
+
break;
|
|
2284
|
+
case "json":
|
|
2285
|
+
default:
|
|
2286
|
+
content = JSON.stringify({ categories }, null, 2);
|
|
2287
|
+
filename = "tokens.json";
|
|
2288
|
+
}
|
|
2289
|
+
const outputPath = path.join(outputDir, filename);
|
|
2290
|
+
await fs.promises.writeFile(outputPath, content, "utf-8");
|
|
2291
|
+
console.log(`[musea] Generated token documentation: ${outputPath}`);
|
|
2292
|
+
return {
|
|
2293
|
+
categories,
|
|
2294
|
+
metadata: {
|
|
2295
|
+
name: path.basename(config.tokensPath),
|
|
2296
|
+
generatedAt: new Date().toISOString()
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2606
2300
|
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2301
|
+
//#endregion
|
|
2302
|
+
//#region src/api-tokens.ts
|
|
2303
|
+
/** GET /api/tokens/usage */
|
|
2304
|
+
async function handleTokensUsage(ctx, sendJson) {
|
|
2305
|
+
if (!ctx.tokensPath) {
|
|
2306
|
+
sendJson({});
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
try {
|
|
2310
|
+
const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
|
|
2311
|
+
const categories = await parseTokens(absoluteTokensPath);
|
|
2312
|
+
const tokenMap = buildTokenMap(categories);
|
|
2313
|
+
resolveReferences(categories, tokenMap);
|
|
2314
|
+
const resolvedTokenMap = buildTokenMap(categories);
|
|
2315
|
+
const usage = scanTokenUsage(ctx.artFiles, resolvedTokenMap);
|
|
2316
|
+
sendJson(usage);
|
|
2317
|
+
} catch (e) {
|
|
2318
|
+
console.error("[musea] Failed to scan token usage:", e);
|
|
2319
|
+
sendJson({});
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
/** GET /api/tokens */
|
|
2323
|
+
async function handleTokensGet(ctx, sendJson) {
|
|
2324
|
+
if (!ctx.tokensPath) {
|
|
2325
|
+
sendJson({
|
|
2326
|
+
categories: [],
|
|
2327
|
+
tokenMap: {},
|
|
2328
|
+
meta: {
|
|
2329
|
+
filePath: "",
|
|
2330
|
+
tokenCount: 0,
|
|
2331
|
+
primitiveCount: 0,
|
|
2332
|
+
semanticCount: 0
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
try {
|
|
2338
|
+
const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
|
|
2339
|
+
const categories = await parseTokens(absoluteTokensPath);
|
|
2340
|
+
const tokenMap = buildTokenMap(categories);
|
|
2341
|
+
resolveReferences(categories, tokenMap);
|
|
2342
|
+
const resolvedTokenMap = buildTokenMap(categories);
|
|
2343
|
+
let primitiveCount = 0;
|
|
2344
|
+
let semanticCount = 0;
|
|
2345
|
+
for (const token of Object.values(resolvedTokenMap)) if (token.$tier === "semantic") semanticCount++;
|
|
2346
|
+
else primitiveCount++;
|
|
2347
|
+
sendJson({
|
|
2348
|
+
categories,
|
|
2349
|
+
tokenMap: resolvedTokenMap,
|
|
2350
|
+
meta: {
|
|
2351
|
+
filePath: absoluteTokensPath,
|
|
2352
|
+
tokenCount: Object.keys(resolvedTokenMap).length,
|
|
2353
|
+
primitiveCount,
|
|
2354
|
+
semanticCount
|
|
2355
|
+
}
|
|
2356
|
+
});
|
|
2357
|
+
} catch (e) {
|
|
2358
|
+
console.error("[musea] Failed to load tokens:", e);
|
|
2359
|
+
sendJson({
|
|
2360
|
+
categories: [],
|
|
2361
|
+
tokenMap: {},
|
|
2362
|
+
error: String(e)
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
/** POST /api/tokens (create) */
|
|
2367
|
+
async function handleTokensCreate(ctx, readBody, sendJson, sendError) {
|
|
2368
|
+
if (!ctx.tokensPath) {
|
|
2369
|
+
sendError("No tokens path configured", 400);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
const body = await readBody();
|
|
2373
|
+
try {
|
|
2374
|
+
const { path: dotPath, token } = JSON.parse(body);
|
|
2375
|
+
if (!dotPath || !token || token.value === void 0) {
|
|
2376
|
+
sendError("Missing required fields: path, token.value", 400);
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
|
|
2380
|
+
const rawData = await readRawTokenFile(absoluteTokensPath);
|
|
2381
|
+
const currentCategories = await parseTokens(absoluteTokensPath);
|
|
2382
|
+
const currentMap = buildTokenMap(currentCategories);
|
|
2383
|
+
if (currentMap[dotPath]) {
|
|
2384
|
+
sendError(`Token already exists at path "${dotPath}"`, 409);
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
if (token.$reference) {
|
|
2388
|
+
const validation = validateSemanticReference(currentMap, token.$reference, dotPath);
|
|
2389
|
+
if (!validation.valid) {
|
|
2390
|
+
sendError(validation.error, 400);
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
token.value = `{${token.$reference}}`;
|
|
2394
|
+
token.$tier = "semantic";
|
|
2395
|
+
}
|
|
2396
|
+
setTokenAtPath(rawData, dotPath, token);
|
|
2397
|
+
await writeRawTokenFile(absoluteTokensPath, rawData);
|
|
2398
|
+
const categories = await parseTokens(absoluteTokensPath);
|
|
2399
|
+
const tokenMap = buildTokenMap(categories);
|
|
2400
|
+
resolveReferences(categories, tokenMap);
|
|
2401
|
+
const resolvedTokenMap = buildTokenMap(categories);
|
|
2402
|
+
sendJson({
|
|
2403
|
+
categories,
|
|
2404
|
+
tokenMap: resolvedTokenMap
|
|
2405
|
+
}, 201);
|
|
2406
|
+
} catch (e) {
|
|
2407
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
/** PUT /api/tokens (update) */
|
|
2411
|
+
async function handleTokensUpdate(ctx, readBody, sendJson, sendError) {
|
|
2412
|
+
if (!ctx.tokensPath) {
|
|
2413
|
+
sendError("No tokens path configured", 400);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
const body = await readBody();
|
|
2417
|
+
try {
|
|
2418
|
+
const { path: dotPath, token } = JSON.parse(body);
|
|
2419
|
+
if (!dotPath || !token || token.value === void 0) {
|
|
2420
|
+
sendError("Missing required fields: path, token.value", 400);
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
|
|
2424
|
+
if (token.$reference) {
|
|
2425
|
+
const currentCategories = await parseTokens(absoluteTokensPath);
|
|
2426
|
+
const currentMap = buildTokenMap(currentCategories);
|
|
2427
|
+
const validation = validateSemanticReference(currentMap, token.$reference, dotPath);
|
|
2428
|
+
if (!validation.valid) {
|
|
2429
|
+
sendError(validation.error, 400);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
token.value = `{${token.$reference}}`;
|
|
2433
|
+
token.$tier = "semantic";
|
|
2434
|
+
}
|
|
2435
|
+
const rawData = await readRawTokenFile(absoluteTokensPath);
|
|
2436
|
+
setTokenAtPath(rawData, dotPath, token);
|
|
2437
|
+
await writeRawTokenFile(absoluteTokensPath, rawData);
|
|
2438
|
+
const categories = await parseTokens(absoluteTokensPath);
|
|
2439
|
+
const tokenMap = buildTokenMap(categories);
|
|
2440
|
+
resolveReferences(categories, tokenMap);
|
|
2441
|
+
const resolvedTokenMap = buildTokenMap(categories);
|
|
2442
|
+
sendJson({
|
|
2443
|
+
categories,
|
|
2444
|
+
tokenMap: resolvedTokenMap
|
|
2445
|
+
});
|
|
2446
|
+
} catch (e) {
|
|
2447
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
/** DELETE /api/tokens */
|
|
2451
|
+
async function handleTokensDelete(ctx, readBody, sendJson, sendError) {
|
|
2452
|
+
if (!ctx.tokensPath) {
|
|
2453
|
+
sendError("No tokens path configured", 400);
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
const body = await readBody();
|
|
2457
|
+
try {
|
|
2458
|
+
const { path: dotPath } = JSON.parse(body);
|
|
2459
|
+
if (!dotPath) {
|
|
2460
|
+
sendError("Missing required field: path", 400);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
|
|
2464
|
+
const currentCategories = await parseTokens(absoluteTokensPath);
|
|
2465
|
+
const currentMap = buildTokenMap(currentCategories);
|
|
2466
|
+
const dependents = findDependentTokens(currentMap, dotPath);
|
|
2467
|
+
const rawData = await readRawTokenFile(absoluteTokensPath);
|
|
2468
|
+
const deleted = deleteTokenAtPath(rawData, dotPath);
|
|
2469
|
+
if (!deleted) {
|
|
2470
|
+
sendError(`Token not found at path "${dotPath}"`, 404);
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
await writeRawTokenFile(absoluteTokensPath, rawData);
|
|
2474
|
+
const categories = await parseTokens(absoluteTokensPath);
|
|
2475
|
+
const tokenMap = buildTokenMap(categories);
|
|
2476
|
+
resolveReferences(categories, tokenMap);
|
|
2477
|
+
const resolvedTokenMap = buildTokenMap(categories);
|
|
2478
|
+
sendJson({
|
|
2479
|
+
categories,
|
|
2480
|
+
tokenMap: resolvedTokenMap,
|
|
2481
|
+
dependentsWarning: dependents.length > 0 ? dependents : void 0
|
|
2482
|
+
});
|
|
2483
|
+
} catch (e) {
|
|
2484
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2485
|
+
}
|
|
2644
2486
|
}
|
|
2645
2487
|
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2488
|
+
//#endregion
|
|
2489
|
+
//#region src/api-routes.ts
|
|
2490
|
+
/** Helper to read the full request body as a string. */
|
|
2491
|
+
function collectBody(req) {
|
|
2492
|
+
return new Promise((resolve) => {
|
|
2493
|
+
let body = "";
|
|
2494
|
+
req.on("data", (chunk) => {
|
|
2495
|
+
body += chunk;
|
|
2496
|
+
});
|
|
2497
|
+
req.on("end", () => resolve(body));
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Create the API middleware handler for the Musea gallery.
|
|
2502
|
+
*
|
|
2503
|
+
* Returns a Connect-compatible middleware function that handles all
|
|
2504
|
+
* `/api/...` sub-routes under the configured basePath.
|
|
2505
|
+
*/
|
|
2506
|
+
function createApiMiddleware(ctx) {
|
|
2507
|
+
return async (req, res, next) => {
|
|
2508
|
+
const sendJson = (data, status = 200) => {
|
|
2509
|
+
res.statusCode = status;
|
|
2510
|
+
res.setHeader("Content-Type", "application/json");
|
|
2511
|
+
res.end(JSON.stringify(data));
|
|
2512
|
+
};
|
|
2513
|
+
const sendError = (message, status = 500) => {
|
|
2514
|
+
sendJson({ error: message }, status);
|
|
2515
|
+
};
|
|
2516
|
+
const readBody = () => collectBody(req);
|
|
2517
|
+
const url = req.url || "/";
|
|
2518
|
+
if (url === "/arts" && req.method === "GET") {
|
|
2519
|
+
sendJson(Array.from(ctx.artFiles.values()));
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
if (url === "/tokens/usage" && req.method === "GET") {
|
|
2523
|
+
await handleTokensUsage(ctx, sendJson);
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (url === "/tokens" && req.method === "GET") {
|
|
2527
|
+
await handleTokensGet(ctx, sendJson);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
if (url === "/tokens" && req.method === "POST") {
|
|
2531
|
+
await handleTokensCreate(ctx, readBody, sendJson, sendError);
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
if (url === "/tokens" && req.method === "PUT") {
|
|
2535
|
+
await handleTokensUpdate(ctx, readBody, sendJson, sendError);
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
if (url === "/tokens" && req.method === "DELETE") {
|
|
2539
|
+
await handleTokensDelete(ctx, readBody, sendJson, sendError);
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
if (url?.startsWith("/arts/") && req.method === "PUT") {
|
|
2543
|
+
const rest = url.slice(6);
|
|
2544
|
+
const sourceMatch = rest.match(/^(.+)\/source$/);
|
|
2545
|
+
if (sourceMatch) {
|
|
2546
|
+
const artPath = decodeURIComponent(sourceMatch[1]);
|
|
2547
|
+
const art = ctx.artFiles.get(artPath);
|
|
2548
|
+
if (!art) {
|
|
2549
|
+
sendError("Art not found", 404);
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
let body = "";
|
|
2553
|
+
req.on("data", (chunk) => {
|
|
2554
|
+
body += chunk;
|
|
2555
|
+
});
|
|
2556
|
+
req.on("end", async () => {
|
|
2557
|
+
try {
|
|
2558
|
+
const { source } = JSON.parse(body);
|
|
2559
|
+
if (typeof source !== "string") {
|
|
2560
|
+
sendError("Missing required field: source", 400);
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
await fs.promises.writeFile(artPath, source, "utf-8");
|
|
2564
|
+
await ctx.processArtFile(artPath);
|
|
2565
|
+
sendJson({ success: true });
|
|
2566
|
+
} catch (e) {
|
|
2567
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
next();
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (url?.startsWith("/arts/") && req.method === "GET") {
|
|
2576
|
+
const rest = url.slice(6);
|
|
2577
|
+
const sourceMatch = rest.match(/^(.+)\/source$/);
|
|
2578
|
+
const paletteMatch = rest.match(/^(.+)\/palette$/);
|
|
2579
|
+
const analysisMatch = rest.match(/^(.+)\/analysis$/);
|
|
2580
|
+
const docsMatch = rest.match(/^(.+)\/docs$/);
|
|
2581
|
+
const a11yMatch = rest.match(/^(.+)\/variants\/([^/]+)\/a11y$/);
|
|
2582
|
+
if (sourceMatch) {
|
|
2583
|
+
await handleArtSource(ctx, sourceMatch, sendJson, sendError);
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
if (paletteMatch) {
|
|
2587
|
+
await handleArtPalette(ctx, paletteMatch, sendJson, sendError);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
if (analysisMatch) {
|
|
2591
|
+
await handleArtAnalysis(ctx, analysisMatch, sendJson, sendError);
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
if (docsMatch) {
|
|
2595
|
+
await handleArtDocs(ctx, docsMatch, sendJson, sendError);
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
if (a11yMatch) {
|
|
2599
|
+
handleArtA11y(ctx, a11yMatch, sendJson, sendError);
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const artPath = decodeURIComponent(rest);
|
|
2603
|
+
const art = ctx.artFiles.get(artPath);
|
|
2604
|
+
if (art) sendJson(art);
|
|
2605
|
+
else sendError("Art not found", 404);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
if (url === "/preview-with-props" && req.method === "POST") {
|
|
2609
|
+
let body = "";
|
|
2610
|
+
req.on("data", (chunk) => {
|
|
2611
|
+
body += chunk;
|
|
2612
|
+
});
|
|
2613
|
+
req.on("end", () => {
|
|
2614
|
+
try {
|
|
2615
|
+
const { artPath: reqArtPath, variantName, props: propsOverride } = JSON.parse(body);
|
|
2616
|
+
const art = ctx.artFiles.get(reqArtPath);
|
|
2617
|
+
if (!art) {
|
|
2618
|
+
sendError("Art not found", 404);
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
const variant = art.variants.find((v) => v.name === variantName);
|
|
2622
|
+
if (!variant) {
|
|
2623
|
+
sendError("Variant not found", 404);
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
const variantComponentName = toPascalCase(variant.name);
|
|
2627
|
+
const moduleCode = generatePreviewModuleWithProps(art, variantComponentName, variant.name, propsOverride, ctx.resolvedPreviewCss, ctx.resolvedPreviewSetup);
|
|
2628
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
2629
|
+
res.end(moduleCode);
|
|
2630
|
+
} catch (e) {
|
|
2631
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
if (url === "/generate" && req.method === "POST") {
|
|
2637
|
+
let body = "";
|
|
2638
|
+
req.on("data", (chunk) => {
|
|
2639
|
+
body += chunk;
|
|
2640
|
+
});
|
|
2641
|
+
req.on("end", async () => {
|
|
2642
|
+
try {
|
|
2643
|
+
const { componentPath: reqComponentPath, options: autogenOptions } = JSON.parse(body);
|
|
2644
|
+
const { generateArtFile: genArt } = await import("./autogen.js");
|
|
2645
|
+
const result = await genArt(reqComponentPath, autogenOptions);
|
|
2646
|
+
sendJson({
|
|
2647
|
+
generated: true,
|
|
2648
|
+
componentName: result.componentName,
|
|
2649
|
+
variants: result.variants,
|
|
2650
|
+
artFileContent: result.artFileContent
|
|
2651
|
+
});
|
|
2652
|
+
} catch (e) {
|
|
2653
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2654
|
+
}
|
|
2655
|
+
});
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
if (url === "/run-vrt" && req.method === "POST") {
|
|
2659
|
+
let body = "";
|
|
2660
|
+
req.on("data", (chunk) => {
|
|
2661
|
+
body += chunk;
|
|
2662
|
+
});
|
|
2663
|
+
req.on("end", async () => {
|
|
2664
|
+
try {
|
|
2665
|
+
const { artPath, updateSnapshots } = JSON.parse(body);
|
|
2666
|
+
const { MuseaVrtRunner: MuseaVrtRunner$1 } = await import("./vrt.js");
|
|
2667
|
+
const runner = new MuseaVrtRunner$1({ snapshotDir: path.resolve(ctx.config.root, ".vize/snapshots") });
|
|
2668
|
+
const port = ctx.getDevServerPort();
|
|
2669
|
+
const baseUrl = `http://localhost:${port}`;
|
|
2670
|
+
let artsToTest = Array.from(ctx.artFiles.values());
|
|
2671
|
+
if (artPath) artsToTest = artsToTest.filter((a) => a.path === artPath);
|
|
2672
|
+
await runner.start();
|
|
2673
|
+
const results = await runner.runTests(artsToTest, baseUrl, { updateSnapshots });
|
|
2674
|
+
const summary = runner.getSummary(results);
|
|
2675
|
+
await runner.stop();
|
|
2676
|
+
sendJson({
|
|
2677
|
+
success: true,
|
|
2678
|
+
summary,
|
|
2679
|
+
results: results.map((r) => ({
|
|
2680
|
+
artPath: r.artPath,
|
|
2681
|
+
variantName: r.variantName,
|
|
2682
|
+
viewport: r.viewport.name,
|
|
2683
|
+
passed: r.passed,
|
|
2684
|
+
isNew: r.isNew,
|
|
2685
|
+
diffPercentage: r.diffPercentage,
|
|
2686
|
+
error: r.error
|
|
2687
|
+
}))
|
|
2688
|
+
});
|
|
2689
|
+
} catch (e) {
|
|
2690
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
next();
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
/** GET /api/arts/:path/source */
|
|
2699
|
+
async function handleArtSource(ctx, match, sendJson, sendError) {
|
|
2700
|
+
const artPath = decodeURIComponent(match[1]);
|
|
2701
|
+
const art = ctx.artFiles.get(artPath);
|
|
2702
|
+
if (!art) {
|
|
2703
|
+
sendError("Art not found", 404);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
try {
|
|
2707
|
+
const source = await fs.promises.readFile(artPath, "utf-8");
|
|
2708
|
+
sendJson({
|
|
2709
|
+
source,
|
|
2710
|
+
path: artPath
|
|
2711
|
+
});
|
|
2712
|
+
} catch (e) {
|
|
2713
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
/** GET /api/arts/:path/palette */
|
|
2717
|
+
async function handleArtPalette(ctx, match, sendJson, sendError) {
|
|
2718
|
+
const artPath = decodeURIComponent(match[1]);
|
|
2719
|
+
const art = ctx.artFiles.get(artPath);
|
|
2720
|
+
if (!art) {
|
|
2721
|
+
sendError("Art not found", 404);
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
try {
|
|
2725
|
+
const source = await fs.promises.readFile(artPath, "utf-8");
|
|
2726
|
+
const binding = loadNative();
|
|
2727
|
+
let palette;
|
|
2728
|
+
if (binding.generateArtPalette) palette = binding.generateArtPalette(source, { filename: artPath });
|
|
2729
|
+
else palette = {
|
|
2730
|
+
title: art.metadata.title,
|
|
2731
|
+
controls: [],
|
|
2732
|
+
groups: [],
|
|
2733
|
+
json: "{}",
|
|
2734
|
+
typescript: ""
|
|
2735
|
+
};
|
|
2736
|
+
if (palette.controls.length === 0 && art.metadata.component) {
|
|
2737
|
+
const resolvedComponentPath = path.isAbsolute(art.metadata.component) ? art.metadata.component : path.resolve(path.dirname(artPath), art.metadata.component);
|
|
2738
|
+
try {
|
|
2739
|
+
const componentSource = await fs.promises.readFile(resolvedComponentPath, "utf-8");
|
|
2740
|
+
const analysis = binding.analyzeSfc ? binding.analyzeSfc(componentSource, { filename: resolvedComponentPath }) : analyzeSfcFallback(componentSource, { filename: resolvedComponentPath });
|
|
2741
|
+
if (analysis.props.length > 0) {
|
|
2742
|
+
palette.controls = analysis.props.map((prop) => {
|
|
2743
|
+
let control = "text";
|
|
2744
|
+
if (prop.type === "boolean") control = "boolean";
|
|
2745
|
+
else if (prop.type === "number") control = "number";
|
|
2746
|
+
else if (prop.type.includes("|") && !prop.type.includes("=>")) control = "select";
|
|
2747
|
+
const options = [];
|
|
2748
|
+
if (control === "select") {
|
|
2749
|
+
const optionMatches = prop.type.match(/"([^"]+)"/g);
|
|
2750
|
+
if (optionMatches) for (const opt of optionMatches) {
|
|
2751
|
+
const val = opt.replace(/"/g, "");
|
|
2752
|
+
options.push({
|
|
2753
|
+
label: val,
|
|
2754
|
+
value: val
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
return {
|
|
2759
|
+
name: prop.name,
|
|
2760
|
+
control,
|
|
2761
|
+
default_value: prop.default_value !== void 0 ? prop.default_value === "true" ? true : prop.default_value === "false" ? false : typeof prop.default_value === "string" && prop.default_value.startsWith("\"") ? prop.default_value.replace(/^"|"$/g, "") : prop.default_value : void 0,
|
|
2762
|
+
description: void 0,
|
|
2763
|
+
required: prop.required,
|
|
2764
|
+
options,
|
|
2765
|
+
range: void 0,
|
|
2766
|
+
group: void 0
|
|
2767
|
+
};
|
|
2768
|
+
});
|
|
2769
|
+
palette.json = JSON.stringify({
|
|
2770
|
+
title: palette.title,
|
|
2771
|
+
controls: palette.controls
|
|
2772
|
+
}, null, 2);
|
|
2773
|
+
palette.typescript = `export interface ${palette.title}Props {\n${palette.controls.map((c) => ` ${c.name}${c.required ? "" : "?"}: ${c.control === "boolean" ? "boolean" : c.control === "number" ? "number" : c.control === "select" ? c.options.map((o) => `"${String(o.value)}"`).join(" | ") : "string"};`).join("\n")}\n}\n`;
|
|
2774
|
+
}
|
|
2775
|
+
} catch {}
|
|
2776
|
+
}
|
|
2777
|
+
sendJson(palette);
|
|
2778
|
+
} catch (e) {
|
|
2779
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2780
|
+
}
|
|
2665
2781
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2782
|
+
/** GET /api/arts/:path/analysis */
|
|
2783
|
+
async function handleArtAnalysis(ctx, match, sendJson, sendError) {
|
|
2784
|
+
const artPath = decodeURIComponent(match[1]);
|
|
2785
|
+
const art = ctx.artFiles.get(artPath);
|
|
2786
|
+
if (!art) {
|
|
2787
|
+
sendError("Art not found", 404);
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
try {
|
|
2791
|
+
const resolvedComponentPath = art.isInline && art.componentPath ? art.componentPath : art.metadata.component ? path.isAbsolute(art.metadata.component) ? art.metadata.component : path.resolve(path.dirname(artPath), art.metadata.component) : null;
|
|
2792
|
+
if (resolvedComponentPath) {
|
|
2793
|
+
const source = await fs.promises.readFile(resolvedComponentPath, "utf-8");
|
|
2794
|
+
const binding = loadNative();
|
|
2795
|
+
if (binding.analyzeSfc) {
|
|
2796
|
+
const analysis = binding.analyzeSfc(source, { filename: resolvedComponentPath });
|
|
2797
|
+
sendJson(analysis);
|
|
2798
|
+
} else {
|
|
2799
|
+
const analysis = analyzeSfcFallback(source, { filename: resolvedComponentPath });
|
|
2800
|
+
sendJson(analysis);
|
|
2801
|
+
}
|
|
2802
|
+
} else sendJson({
|
|
2803
|
+
props: [],
|
|
2804
|
+
emits: []
|
|
2805
|
+
});
|
|
2806
|
+
} catch (e) {
|
|
2807
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2808
|
+
}
|
|
2669
2809
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2810
|
+
/** GET /api/arts/:path/docs */
|
|
2811
|
+
async function handleArtDocs(ctx, match, sendJson, sendError) {
|
|
2812
|
+
const artPath = decodeURIComponent(match[1]);
|
|
2813
|
+
const art = ctx.artFiles.get(artPath);
|
|
2814
|
+
if (!art) {
|
|
2815
|
+
sendError("Art not found", 404);
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
try {
|
|
2819
|
+
const source = await fs.promises.readFile(artPath, "utf-8");
|
|
2820
|
+
const binding = loadNative();
|
|
2821
|
+
if (binding.generateArtDoc) {
|
|
2822
|
+
const doc = binding.generateArtDoc(source, { filename: artPath });
|
|
2823
|
+
let markdown = doc.markdown || "";
|
|
2824
|
+
const componentName = art.metadata.title || "Component";
|
|
2825
|
+
markdown = markdown.replace(/<Self(\s|>|\/)/g, `<${componentName}$1`).replace(/<\/Self>/g, `</${componentName}>`);
|
|
2826
|
+
markdown = markdown.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
|
|
2827
|
+
const lines = code.split("\n");
|
|
2828
|
+
let minIndent = Infinity;
|
|
2829
|
+
for (const line of lines) if (line.trim()) {
|
|
2830
|
+
const indent = line.match(/^(\s*)/)?.[1].length || 0;
|
|
2831
|
+
minIndent = Math.min(minIndent, indent);
|
|
2832
|
+
}
|
|
2833
|
+
if (minIndent === Infinity) minIndent = 0;
|
|
2834
|
+
let formatted;
|
|
2835
|
+
if (minIndent > 0) formatted = lines.map((line) => line.slice(minIndent)).join("\n");
|
|
2836
|
+
else {
|
|
2837
|
+
let restIndent = Infinity;
|
|
2838
|
+
for (let i = 1; i < lines.length; i++) if (lines[i].trim()) {
|
|
2839
|
+
const indent = lines[i].match(/^(\s*)/)?.[1].length || 0;
|
|
2840
|
+
restIndent = Math.min(restIndent, indent);
|
|
2841
|
+
}
|
|
2842
|
+
if (restIndent === Infinity || restIndent === 0) formatted = lines.join("\n");
|
|
2843
|
+
else formatted = lines.map((line, i) => i === 0 ? line : line.slice(restIndent)).join("\n");
|
|
2844
|
+
}
|
|
2845
|
+
return "```" + lang + "\n" + formatted + "```";
|
|
2846
|
+
});
|
|
2847
|
+
sendJson({
|
|
2848
|
+
...doc,
|
|
2849
|
+
markdown
|
|
2850
|
+
});
|
|
2851
|
+
} else sendJson({
|
|
2852
|
+
markdown: "",
|
|
2853
|
+
title: art.metadata.title,
|
|
2854
|
+
variant_count: art.variants.length
|
|
2855
|
+
});
|
|
2856
|
+
} catch (e) {
|
|
2857
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
2858
|
+
}
|
|
2673
2859
|
}
|
|
2674
|
-
/**
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
const
|
|
2679
|
-
|
|
2860
|
+
/** GET /api/arts/:path/variants/:name/a11y */
|
|
2861
|
+
function handleArtA11y(ctx, match, sendJson, sendError) {
|
|
2862
|
+
const artPath = decodeURIComponent(match[1]);
|
|
2863
|
+
const _variantName = decodeURIComponent(match[2]);
|
|
2864
|
+
const art = ctx.artFiles.get(artPath);
|
|
2865
|
+
if (!art) {
|
|
2866
|
+
sendError("Art not found", 404);
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
sendJson({
|
|
2870
|
+
violations: [],
|
|
2871
|
+
passes: 0,
|
|
2872
|
+
incomplete: 0
|
|
2873
|
+
});
|
|
2680
2874
|
}
|
|
2875
|
+
|
|
2876
|
+
//#endregion
|
|
2877
|
+
//#region src/plugin.ts
|
|
2878
|
+
const VIRTUAL_MUSEA_PREFIX = "\0musea:";
|
|
2879
|
+
const VIRTUAL_GALLERY = "\0musea-gallery";
|
|
2880
|
+
const VIRTUAL_MANIFEST = "\0musea-manifest";
|
|
2681
2881
|
/**
|
|
2682
|
-
*
|
|
2683
|
-
* Returns the import lines, setup body lines, and all identifiers to expose.
|
|
2882
|
+
* Create Musea Vite plugin.
|
|
2684
2883
|
*/
|
|
2685
|
-
function
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2884
|
+
function musea(options = {}) {
|
|
2885
|
+
let include = options.include ?? ["**/*.art.vue"];
|
|
2886
|
+
let exclude = options.exclude ?? ["node_modules/**", "dist/**"];
|
|
2887
|
+
let basePath = options.basePath ?? "/__musea__";
|
|
2888
|
+
let storybookCompat = options.storybookCompat ?? false;
|
|
2889
|
+
const storybookOutDir = options.storybookOutDir ?? ".storybook/stories";
|
|
2890
|
+
let inlineArt = options.inlineArt ?? false;
|
|
2891
|
+
const tokensPath = options.tokensPath;
|
|
2892
|
+
const themeConfig = buildThemeConfig(options.theme);
|
|
2893
|
+
const previewCss = options.previewCss ?? [];
|
|
2894
|
+
const previewSetup = options.previewSetup;
|
|
2895
|
+
let config;
|
|
2896
|
+
let server = null;
|
|
2897
|
+
const artFiles = new Map();
|
|
2898
|
+
let resolvedPreviewCss = [];
|
|
2899
|
+
let resolvedPreviewSetup = null;
|
|
2900
|
+
const mainPlugin = {
|
|
2901
|
+
name: "vite-plugin-musea",
|
|
2902
|
+
enforce: "pre",
|
|
2903
|
+
config() {
|
|
2904
|
+
return { resolve: { alias: { vue: "vue/dist/vue.esm-bundler.js" } } };
|
|
2905
|
+
},
|
|
2906
|
+
configResolved(resolvedConfig) {
|
|
2907
|
+
config = resolvedConfig;
|
|
2908
|
+
const vizeConfig = vizeConfigStore.get(resolvedConfig.root);
|
|
2909
|
+
if (vizeConfig?.musea) {
|
|
2910
|
+
const mc = vizeConfig.musea;
|
|
2911
|
+
if (!options.include && mc.include) include = mc.include;
|
|
2912
|
+
if (!options.exclude && mc.exclude) exclude = mc.exclude;
|
|
2913
|
+
if (!options.basePath && mc.basePath) basePath = mc.basePath;
|
|
2914
|
+
if (options.storybookCompat === void 0 && mc.storybookCompat !== void 0) storybookCompat = mc.storybookCompat;
|
|
2915
|
+
if (options.inlineArt === void 0 && mc.inlineArt !== void 0) inlineArt = mc.inlineArt;
|
|
2701
2916
|
}
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2917
|
+
resolvedPreviewCss = previewCss.map((cssPath) => path.isAbsolute(cssPath) ? cssPath : path.resolve(resolvedConfig.root, cssPath));
|
|
2918
|
+
if (previewSetup) resolvedPreviewSetup = path.isAbsolute(previewSetup) ? previewSetup : path.resolve(resolvedConfig.root, previewSetup);
|
|
2919
|
+
},
|
|
2920
|
+
configureServer(devServer) {
|
|
2921
|
+
server = devServer;
|
|
2922
|
+
registerMiddleware(devServer, {
|
|
2923
|
+
basePath,
|
|
2924
|
+
themeConfig,
|
|
2925
|
+
artFiles,
|
|
2926
|
+
resolvedPreviewCss,
|
|
2927
|
+
resolvedPreviewSetup
|
|
2928
|
+
});
|
|
2929
|
+
devServer.middlewares.use(`${basePath}/api`, createApiMiddleware({
|
|
2930
|
+
config,
|
|
2931
|
+
artFiles,
|
|
2932
|
+
tokensPath,
|
|
2933
|
+
basePath,
|
|
2934
|
+
resolvedPreviewCss,
|
|
2935
|
+
resolvedPreviewSetup,
|
|
2936
|
+
processArtFile,
|
|
2937
|
+
getDevServerPort: () => devServer.config.server.port || 5173
|
|
2938
|
+
}));
|
|
2939
|
+
devServer.watcher.on("change", async (file) => {
|
|
2940
|
+
if (file.endsWith(".art.vue") && shouldProcess(file, include, exclude, config.root)) {
|
|
2941
|
+
await processArtFile(file);
|
|
2942
|
+
console.log(`[musea] Reloaded: ${path.relative(config.root, file)}`);
|
|
2943
|
+
}
|
|
2944
|
+
if (inlineArt && file.endsWith(".vue") && !file.endsWith(".art.vue")) {
|
|
2945
|
+
const hadArt = artFiles.has(file);
|
|
2946
|
+
const source = await fs.promises.readFile(file, "utf-8");
|
|
2947
|
+
if (source.includes("<art")) {
|
|
2948
|
+
await processArtFile(file);
|
|
2949
|
+
console.log(`[musea] Reloaded inline art: ${path.relative(config.root, file)}`);
|
|
2950
|
+
} else if (hadArt) {
|
|
2951
|
+
artFiles.delete(file);
|
|
2952
|
+
console.log(`[musea] Removed inline art: ${path.relative(config.root, file)}`);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
devServer.watcher.on("add", async (file) => {
|
|
2957
|
+
if (file.endsWith(".art.vue") && shouldProcess(file, include, exclude, config.root)) {
|
|
2958
|
+
await processArtFile(file);
|
|
2959
|
+
console.log(`[musea] Added: ${path.relative(config.root, file)}`);
|
|
2960
|
+
}
|
|
2961
|
+
if (inlineArt && file.endsWith(".vue") && !file.endsWith(".art.vue")) {
|
|
2962
|
+
const source = await fs.promises.readFile(file, "utf-8");
|
|
2963
|
+
if (source.includes("<art")) {
|
|
2964
|
+
await processArtFile(file);
|
|
2965
|
+
console.log(`[musea] Added inline art: ${path.relative(config.root, file)}`);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
});
|
|
2969
|
+
devServer.watcher.on("unlink", (file) => {
|
|
2970
|
+
if (artFiles.has(file)) {
|
|
2971
|
+
artFiles.delete(file);
|
|
2972
|
+
console.log(`[musea] Removed: ${path.relative(config.root, file)}`);
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
return () => {
|
|
2976
|
+
devServer.httpServer?.once("listening", () => {
|
|
2977
|
+
const address = devServer.httpServer?.address();
|
|
2978
|
+
if (address && typeof address === "object") {
|
|
2979
|
+
const protocol = devServer.config.server.https ? "https" : "http";
|
|
2980
|
+
const rawHost = address.address;
|
|
2981
|
+
const host = rawHost === "::" || rawHost === "::1" || rawHost === "0.0.0.0" || rawHost === "127.0.0.1" ? "localhost" : rawHost;
|
|
2982
|
+
const port = address.port;
|
|
2983
|
+
const url = `${protocol}://${host}:${port}${basePath}`;
|
|
2984
|
+
console.log();
|
|
2985
|
+
console.log(` \x1b[36m➜\x1b[0m \x1b[1mMusea Gallery:\x1b[0m \x1b[36m${url}\x1b[0m`);
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
2988
|
+
};
|
|
2989
|
+
},
|
|
2990
|
+
async buildStart() {
|
|
2991
|
+
console.log(`[musea] config.root: ${config.root}, include: ${JSON.stringify(include)}`);
|
|
2992
|
+
const files = await scanArtFiles(config.root, include, exclude, inlineArt);
|
|
2993
|
+
console.log(`[musea] Found ${files.length} art files`);
|
|
2994
|
+
for (const file of files) await processArtFile(file);
|
|
2995
|
+
if (storybookCompat) await generateStorybookFiles(artFiles, config.root, storybookOutDir);
|
|
2996
|
+
},
|
|
2997
|
+
resolveId(id) {
|
|
2998
|
+
if (id === VIRTUAL_GALLERY) return VIRTUAL_GALLERY;
|
|
2999
|
+
if (id === VIRTUAL_MANIFEST) return VIRTUAL_MANIFEST;
|
|
3000
|
+
if (id.startsWith("virtual:musea-preview:")) return "\0musea-preview:" + id.slice(22);
|
|
3001
|
+
if (id.startsWith("virtual:musea-art:")) {
|
|
3002
|
+
const artPath = id.slice(18);
|
|
3003
|
+
if (artFiles.has(artPath)) return "\0musea-art:" + artPath + "?musea-virtual";
|
|
2710
3004
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
if (name && name !== "...") returnNames.add(name);
|
|
3005
|
+
if (id.endsWith(".art.vue")) {
|
|
3006
|
+
const resolved = path.resolve(config.root, id);
|
|
3007
|
+
if (artFiles.has(resolved)) return VIRTUAL_MUSEA_PREFIX + resolved + "?musea-virtual";
|
|
2715
3008
|
}
|
|
3009
|
+
if (inlineArt && id.endsWith(".vue") && !id.endsWith(".art.vue")) {
|
|
3010
|
+
const resolved = path.resolve(config.root, id);
|
|
3011
|
+
if (artFiles.has(resolved)) return VIRTUAL_MUSEA_PREFIX + resolved + "?musea-virtual";
|
|
3012
|
+
}
|
|
3013
|
+
return null;
|
|
3014
|
+
},
|
|
3015
|
+
load(id) {
|
|
3016
|
+
if (id === VIRTUAL_GALLERY) return generateGalleryModule(basePath);
|
|
3017
|
+
if (id === VIRTUAL_MANIFEST) return generateManifestModule(artFiles);
|
|
3018
|
+
if (id.startsWith("\0musea-preview:")) {
|
|
3019
|
+
const rest = id.slice(15);
|
|
3020
|
+
const lastColonIndex = rest.lastIndexOf(":");
|
|
3021
|
+
if (lastColonIndex !== -1) {
|
|
3022
|
+
const artPath = rest.slice(0, lastColonIndex);
|
|
3023
|
+
const variantName = rest.slice(lastColonIndex + 1);
|
|
3024
|
+
const art = artFiles.get(artPath);
|
|
3025
|
+
if (art) {
|
|
3026
|
+
const variantComponentName = toPascalCase(variantName);
|
|
3027
|
+
return generatePreviewModule(art, variantComponentName, variantName, resolvedPreviewCss, resolvedPreviewSetup);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
if (id.startsWith("\0musea-art:")) {
|
|
3032
|
+
const artPath = id.slice(11).replace(/\?musea-virtual$/, "");
|
|
3033
|
+
const art = artFiles.get(artPath);
|
|
3034
|
+
if (art) return generateArtModule(art, artPath);
|
|
3035
|
+
}
|
|
3036
|
+
if (id.startsWith(VIRTUAL_MUSEA_PREFIX)) {
|
|
3037
|
+
const realPath = id.slice(VIRTUAL_MUSEA_PREFIX.length).replace(/\?musea-virtual$/, "");
|
|
3038
|
+
const art = artFiles.get(realPath);
|
|
3039
|
+
if (art) return generateArtModule(art, realPath);
|
|
3040
|
+
}
|
|
3041
|
+
return null;
|
|
3042
|
+
},
|
|
3043
|
+
async handleHotUpdate(ctx) {
|
|
3044
|
+
const { file } = ctx;
|
|
3045
|
+
if (file.endsWith(".art.vue") && artFiles.has(file)) {
|
|
3046
|
+
await processArtFile(file);
|
|
3047
|
+
const virtualId = VIRTUAL_MUSEA_PREFIX + file + "?musea-virtual";
|
|
3048
|
+
const modules = server?.moduleGraph.getModulesByFile(virtualId);
|
|
3049
|
+
if (modules) return [...modules];
|
|
3050
|
+
}
|
|
3051
|
+
if (inlineArt && file.endsWith(".vue") && !file.endsWith(".art.vue") && artFiles.has(file)) {
|
|
3052
|
+
await processArtFile(file);
|
|
3053
|
+
const virtualId = VIRTUAL_MUSEA_PREFIX + file;
|
|
3054
|
+
const modules = server?.moduleGraph.getModulesByFile(virtualId);
|
|
3055
|
+
if (modules) return [...modules];
|
|
3056
|
+
}
|
|
3057
|
+
return void 0;
|
|
2716
3058
|
}
|
|
2717
|
-
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
const alreadyImported = scriptSetup?.imports.some((imp) => {
|
|
2753
|
-
if (imp.includes(`from '${componentImportPath}'`) || imp.includes(`from "${componentImportPath}"`)) return true;
|
|
2754
|
-
return new RegExp(`^import\\s+${componentName}[\\s,]`).test(imp.trim());
|
|
2755
|
-
});
|
|
2756
|
-
if (!alreadyImported) code += `import ${componentName} from '${componentImportPath}';\n`;
|
|
2757
|
-
code += `export const __component__ = ${componentName};\n`;
|
|
2758
|
-
}
|
|
2759
|
-
code += `
|
|
2760
|
-
export const metadata = ${JSON.stringify(art.metadata)};
|
|
2761
|
-
export const variants = ${JSON.stringify(art.variants)};
|
|
2762
|
-
`;
|
|
2763
|
-
for (const variant of art.variants) {
|
|
2764
|
-
const variantComponentName = toPascalCase(variant.name);
|
|
2765
|
-
let template = variant.template;
|
|
2766
|
-
if (componentName) template = template.replace(/<Self/g, `<${componentName}`).replace(/<\/Self>/g, `</${componentName}>`);
|
|
2767
|
-
const escapedTemplate = template.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
2768
|
-
const fullTemplate = `<div data-variant="${variant.name}">${escapedTemplate}</div>`;
|
|
2769
|
-
const componentNames = new Set();
|
|
2770
|
-
if (componentName) componentNames.add(componentName);
|
|
2771
|
-
if (scriptSetup) {
|
|
2772
|
-
for (const name of scriptSetup.returnNames) if (/^[A-Z]/.test(name)) componentNames.add(name);
|
|
3059
|
+
};
|
|
3060
|
+
async function processArtFile(filePath) {
|
|
3061
|
+
try {
|
|
3062
|
+
const source = await fs.promises.readFile(filePath, "utf-8");
|
|
3063
|
+
const binding = loadNative();
|
|
3064
|
+
const parsed = binding.parseArt(source, { filename: filePath });
|
|
3065
|
+
if (!parsed.variants || parsed.variants.length === 0) return;
|
|
3066
|
+
const isInline = !filePath.endsWith(".art.vue");
|
|
3067
|
+
const info = {
|
|
3068
|
+
path: filePath,
|
|
3069
|
+
metadata: {
|
|
3070
|
+
title: parsed.metadata.title || (isInline ? path.basename(filePath, ".vue") : ""),
|
|
3071
|
+
description: parsed.metadata.description,
|
|
3072
|
+
component: isInline ? void 0 : parsed.metadata.component,
|
|
3073
|
+
category: parsed.metadata.category,
|
|
3074
|
+
tags: parsed.metadata.tags,
|
|
3075
|
+
status: parsed.metadata.status,
|
|
3076
|
+
order: parsed.metadata.order
|
|
3077
|
+
},
|
|
3078
|
+
variants: parsed.variants.map((v) => ({
|
|
3079
|
+
name: v.name,
|
|
3080
|
+
template: v.template,
|
|
3081
|
+
isDefault: v.isDefault,
|
|
3082
|
+
skipVrt: v.skipVrt
|
|
3083
|
+
})),
|
|
3084
|
+
hasScriptSetup: isInline ? false : parsed.hasScriptSetup,
|
|
3085
|
+
scriptSetupContent: !isInline && parsed.hasScriptSetup ? extractScriptSetupContent(source) : void 0,
|
|
3086
|
+
hasScript: parsed.hasScript,
|
|
3087
|
+
styleCount: parsed.styleCount,
|
|
3088
|
+
isInline,
|
|
3089
|
+
componentPath: isInline ? filePath : void 0
|
|
3090
|
+
};
|
|
3091
|
+
artFiles.set(filePath, info);
|
|
3092
|
+
} catch (e) {
|
|
3093
|
+
console.error(`[musea] Failed to process ${filePath}:`, e);
|
|
2773
3094
|
}
|
|
2774
|
-
const components = componentNames.size > 0 ? ` components: { ${[...componentNames].join(", ")} },\n` : "";
|
|
2775
|
-
if (scriptSetup && scriptSetup.setupBody.length > 0) code += `
|
|
2776
|
-
export const ${variantComponentName} = defineComponent({
|
|
2777
|
-
name: '${variantComponentName}',
|
|
2778
|
-
${components} setup() {
|
|
2779
|
-
${scriptSetup.setupBody.map((l) => ` ${l}`).join("\n")}
|
|
2780
|
-
return { ${scriptSetup.returnNames.join(", ")} };
|
|
2781
|
-
},
|
|
2782
|
-
template: \`${fullTemplate}\`,
|
|
2783
|
-
});
|
|
2784
|
-
`;
|
|
2785
|
-
else if (componentName) code += `
|
|
2786
|
-
export const ${variantComponentName} = {
|
|
2787
|
-
name: '${variantComponentName}',
|
|
2788
|
-
${components} template: \`${fullTemplate}\`,
|
|
2789
|
-
};
|
|
2790
|
-
`;
|
|
2791
|
-
else code += `
|
|
2792
|
-
export const ${variantComponentName} = {
|
|
2793
|
-
name: '${variantComponentName}',
|
|
2794
|
-
template: \`${fullTemplate}\`,
|
|
2795
|
-
};
|
|
2796
|
-
`;
|
|
2797
|
-
}
|
|
2798
|
-
const defaultVariant = art.variants.find((v) => v.isDefault) || art.variants[0];
|
|
2799
|
-
if (defaultVariant) code += `
|
|
2800
|
-
export default ${toPascalCase(defaultVariant.name)};
|
|
2801
|
-
`;
|
|
2802
|
-
return code;
|
|
2803
|
-
}
|
|
2804
|
-
async function generateStorybookFiles(artFiles, root, outDir) {
|
|
2805
|
-
const binding = loadNative();
|
|
2806
|
-
const outputDir = path.resolve(root, outDir);
|
|
2807
|
-
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
2808
|
-
for (const [filePath, _art] of artFiles) try {
|
|
2809
|
-
const source = await fs.promises.readFile(filePath, "utf-8");
|
|
2810
|
-
const csf = binding.artToCsf(source, { filename: filePath });
|
|
2811
|
-
const outputPath = path.join(outputDir, csf.filename);
|
|
2812
|
-
await fs.promises.writeFile(outputPath, csf.code, "utf-8");
|
|
2813
|
-
console.log(`[musea] Generated: ${path.relative(root, outputPath)}`);
|
|
2814
|
-
} catch (e) {
|
|
2815
|
-
console.error(`[musea] Failed to generate CSF for ${filePath}:`, e);
|
|
2816
3095
|
}
|
|
3096
|
+
return [mainPlugin];
|
|
2817
3097
|
}
|
|
2818
|
-
function toPascalCase(str) {
|
|
2819
|
-
return str.split(/[\s\-_]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
2820
|
-
}
|
|
2821
|
-
function escapeTemplate(str) {
|
|
2822
|
-
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
2823
|
-
}
|
|
2824
|
-
function generatePreviewModuleWithProps(art, variantComponentName, variantName, propsOverride, cssImports = [], previewSetup = null) {
|
|
2825
|
-
const artModuleId = `virtual:musea-art:${art.path}`;
|
|
2826
|
-
const escapedVariantName = escapeTemplate(variantName);
|
|
2827
|
-
const propsJson = JSON.stringify(propsOverride);
|
|
2828
|
-
const cssImportStatements = cssImports.map((cssPath) => `import '${cssPath}';`).join("\n");
|
|
2829
|
-
const setupImport = previewSetup ? `import __museaPreviewSetup from '${previewSetup}';` : "";
|
|
2830
|
-
const setupCall = previewSetup ? "await __museaPreviewSetup(app);" : "";
|
|
2831
|
-
return `
|
|
2832
|
-
${cssImportStatements}
|
|
2833
|
-
${setupImport}
|
|
2834
|
-
import { createApp, h } from 'vue';
|
|
2835
|
-
import * as artModule from '${artModuleId}';
|
|
2836
|
-
|
|
2837
|
-
const container = document.getElementById('app');
|
|
2838
|
-
const propsOverride = ${propsJson};
|
|
2839
|
-
|
|
2840
|
-
${MUSEA_ADDONS_INIT_CODE}
|
|
2841
|
-
|
|
2842
|
-
async function mount() {
|
|
2843
|
-
try {
|
|
2844
|
-
const VariantComponent = artModule['${variantComponentName}'];
|
|
2845
|
-
if (!VariantComponent) {
|
|
2846
|
-
throw new Error('Variant component "${variantComponentName}" not found');
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
|
-
const WrappedComponent = {
|
|
2850
|
-
render() {
|
|
2851
|
-
return h(VariantComponent, propsOverride);
|
|
2852
|
-
}
|
|
2853
|
-
};
|
|
2854
|
-
|
|
2855
|
-
const app = createApp(WrappedComponent);
|
|
2856
|
-
${setupCall}
|
|
2857
|
-
container.innerHTML = '';
|
|
2858
|
-
container.className = 'musea-variant';
|
|
2859
|
-
app.mount(container);
|
|
2860
|
-
console.log('[musea-preview] Mounted variant: ${escapedVariantName} with props override');
|
|
2861
|
-
__museaInitAddons(container, '${escapedVariantName}');
|
|
2862
|
-
} catch (error) {
|
|
2863
|
-
console.error('[musea-preview] Failed to mount:', error);
|
|
2864
|
-
container.innerHTML = '<div class="musea-error"><div class="musea-error-title">Failed to render</div><div>' + error.message + '</div></div>';
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
|
-
mount();
|
|
2869
|
-
`;
|
|
2870
|
-
}
|
|
2871
|
-
function generatePreviewHtml(art, variant, _basePath, viteBase) {
|
|
2872
|
-
const previewModuleUrl = `${_basePath}/preview-module?art=${encodeURIComponent(art.path)}&variant=${encodeURIComponent(variant.name)}`;
|
|
2873
|
-
const base = (viteBase || "/").replace(/\/$/, "");
|
|
2874
|
-
return `<!DOCTYPE html>
|
|
2875
|
-
<html lang="en">
|
|
2876
|
-
<head>
|
|
2877
|
-
<meta charset="UTF-8">
|
|
2878
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2879
|
-
<title>${escapeHtml(art.metadata.title)} - ${escapeHtml(variant.name)}</title>
|
|
2880
|
-
<script type="module" src="${base}/@vite/client"></script>
|
|
2881
|
-
<style>
|
|
2882
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2883
|
-
html, body {
|
|
2884
|
-
width: 100%;
|
|
2885
|
-
height: 100%;
|
|
2886
|
-
}
|
|
2887
|
-
body {
|
|
2888
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2889
|
-
background: #ffffff;
|
|
2890
|
-
}
|
|
2891
|
-
.musea-variant {
|
|
2892
|
-
min-height: 100vh;
|
|
2893
|
-
}
|
|
2894
|
-
.musea-error {
|
|
2895
|
-
color: #dc2626;
|
|
2896
|
-
background: #fef2f2;
|
|
2897
|
-
border: 1px solid #fecaca;
|
|
2898
|
-
border-radius: 8px;
|
|
2899
|
-
padding: 1rem;
|
|
2900
|
-
font-size: 0.875rem;
|
|
2901
|
-
max-width: 400px;
|
|
2902
|
-
}
|
|
2903
|
-
.musea-error-title {
|
|
2904
|
-
font-weight: 600;
|
|
2905
|
-
margin-bottom: 0.5rem;
|
|
2906
|
-
}
|
|
2907
|
-
.musea-error pre {
|
|
2908
|
-
font-family: monospace;
|
|
2909
|
-
font-size: 0.75rem;
|
|
2910
|
-
white-space: pre-wrap;
|
|
2911
|
-
word-break: break-all;
|
|
2912
|
-
margin-top: 0.5rem;
|
|
2913
|
-
padding: 0.5rem;
|
|
2914
|
-
background: #fff;
|
|
2915
|
-
border-radius: 4px;
|
|
2916
|
-
}
|
|
2917
|
-
.musea-loading {
|
|
2918
|
-
display: flex;
|
|
2919
|
-
align-items: center;
|
|
2920
|
-
gap: 0.75rem;
|
|
2921
|
-
color: #6b7280;
|
|
2922
|
-
font-size: 0.875rem;
|
|
2923
|
-
}
|
|
2924
|
-
.musea-spinner {
|
|
2925
|
-
width: 20px;
|
|
2926
|
-
height: 20px;
|
|
2927
|
-
border: 2px solid #e5e7eb;
|
|
2928
|
-
border-top-color: #3b82f6;
|
|
2929
|
-
border-radius: 50%;
|
|
2930
|
-
animation: spin 0.8s linear infinite;
|
|
2931
|
-
}
|
|
2932
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
2933
|
-
|
|
2934
|
-
/* Musea Addons: Checkerboard background for transparent mode */
|
|
2935
|
-
.musea-bg-checkerboard {
|
|
2936
|
-
background-image:
|
|
2937
|
-
linear-gradient(45deg, #ccc 25%, transparent 25%),
|
|
2938
|
-
linear-gradient(-45deg, #ccc 25%, transparent 25%),
|
|
2939
|
-
linear-gradient(45deg, transparent 75%, #ccc 75%),
|
|
2940
|
-
linear-gradient(-45deg, transparent 75%, #ccc 75%) !important;
|
|
2941
|
-
background-size: 20px 20px !important;
|
|
2942
|
-
background-position: 0 0, 0 10px, 10px -10px, -10px 0 !important;
|
|
2943
|
-
}
|
|
2944
3098
|
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
position: fixed;
|
|
2948
|
-
background: #333;
|
|
2949
|
-
color: #fff;
|
|
2950
|
-
font-size: 11px;
|
|
2951
|
-
padding: 2px 6px;
|
|
2952
|
-
border-radius: 3px;
|
|
2953
|
-
pointer-events: none;
|
|
2954
|
-
z-index: 100000;
|
|
2955
|
-
}
|
|
2956
|
-
</style>
|
|
2957
|
-
</head>
|
|
2958
|
-
<body>
|
|
2959
|
-
<div id="app" class="musea-variant" data-art="${escapeHtml(art.path)}" data-variant="${escapeHtml(variant.name)}">
|
|
2960
|
-
<div class="musea-loading">
|
|
2961
|
-
<div class="musea-spinner"></div>
|
|
2962
|
-
Loading component...
|
|
2963
|
-
</div>
|
|
2964
|
-
</div>
|
|
2965
|
-
<script type="module" src="${previewModuleUrl}"></script>
|
|
2966
|
-
</body>
|
|
2967
|
-
</html>`;
|
|
2968
|
-
}
|
|
2969
|
-
function escapeHtml(str) {
|
|
2970
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2971
|
-
}
|
|
3099
|
+
//#endregion
|
|
3100
|
+
//#region src/index.ts
|
|
2972
3101
|
var src_default = musea;
|
|
2973
3102
|
|
|
2974
3103
|
//#endregion
|