@vizejs/vite-plugin-musea 0.429.1 → 0.430.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/a11y/index.d.mts +1 -1
  2. package/dist/a11y/index.mjs +1 -1
  3. package/dist/{a11y-Ci9EEv6F.mjs → a11y-BbFK942j.mjs} +66 -15
  4. package/dist/a11y-BbFK942j.mjs.map +1 -0
  5. package/dist/api-gMwahZKm.d.mts +112 -0
  6. package/dist/api-gMwahZKm.d.mts.map +1 -0
  7. package/dist/cli/index.d.mts +1 -1
  8. package/dist/cli/index.d.mts.map +1 -1
  9. package/dist/cli/index.mjs +79 -33
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/gallery/assets/{MonacoEditor-BIjYdDrA.js → MonacoEditor-CnzIyKMy.js} +2 -2
  12. package/dist/gallery/assets/{cssMode-C5PB9d_X.js → cssMode-DZ_me5CG.js} +1 -1
  13. package/dist/gallery/assets/{editor.api-tyf2wJeZ.js → editor.api-CjD4Hpms.js} +1 -1
  14. package/dist/gallery/assets/{freemarker2-Df2SP4C4.js → freemarker2-F7z49Hfe.js} +1 -1
  15. package/dist/gallery/assets/{handlebars-CbFr-5KQ.js → handlebars-Dt4eGgf-.js} +1 -1
  16. package/dist/gallery/assets/{html-C1ubjnt5.js → html-B1zmbcur.js} +1 -1
  17. package/dist/gallery/assets/{htmlMode-9FYaGqmo.js → htmlMode-DmDwpA4v.js} +1 -1
  18. package/dist/gallery/assets/{index-D81pMC9f.css → index-C4vm-yqv.css} +1 -1
  19. package/dist/gallery/assets/{index-BjW1cVGf.js → index-vJ3ftuqF.js} +3 -3
  20. package/dist/gallery/assets/{javascript-DjzYR9ls.js → javascript-CLKbyvz8.js} +1 -1
  21. package/dist/gallery/assets/{jsonMode-BRxSdIjW.js → jsonMode-sms9-L2I.js} +1 -1
  22. package/dist/gallery/assets/{liquid-DD0LqlcF.js → liquid-CesACmVT.js} +1 -1
  23. package/dist/gallery/assets/{lspLanguageFeatures-C0f4qlAU.js → lspLanguageFeatures-C8BR-41u.js} +1 -1
  24. package/dist/gallery/assets/{mdx-DtcWR2Lx.js → mdx-Cd93KUo_.js} +1 -1
  25. package/dist/gallery/assets/{python-C1OlrT9L.js → python-8gi7tgMv.js} +1 -1
  26. package/dist/gallery/assets/{razor-BN2Zj1B9.js → razor-sOyhBXSy.js} +1 -1
  27. package/dist/gallery/assets/{register-CTEjv03I.js → register-C-_4TcWq.js} +2 -2
  28. package/dist/gallery/assets/{toggleHighContrast-BELgo5Kd.js → toggleHighContrast-BCHobRx5.js} +1 -1
  29. package/dist/gallery/assets/{tsMode-Bd8a8shj.js → tsMode-BkQQD7P8.js} +1 -1
  30. package/dist/gallery/assets/{typescript-DLlFIuFu.js → typescript-XiwOdN8G.js} +1 -1
  31. package/dist/gallery/assets/{vs-YCtNwrNl.js → vs-Coo3fK7w.js} +3 -3
  32. package/dist/gallery/assets/{workers-CBv4Qjp-.js → workers-Cypb28_F.js} +1 -1
  33. package/dist/gallery/assets/{xml-wUPbJH_s.js → xml-CN1ixSLs.js} +1 -1
  34. package/dist/gallery/assets/{yaml-B52zgDg-.js → yaml-l30MdsZA.js} +1 -1
  35. package/dist/gallery/index.html +2 -2
  36. package/dist/{gallery-qE5deHL0.mjs → gallery-CIcrB07g.mjs} +2 -2
  37. package/dist/{gallery-qE5deHL0.mjs.map → gallery-CIcrB07g.mjs.map} +1 -1
  38. package/dist/index-C5evpfmF.d.mts +81 -0
  39. package/dist/index-C5evpfmF.d.mts.map +1 -0
  40. package/dist/index.d.mts +5 -4
  41. package/dist/index.d.mts.map +1 -1
  42. package/dist/index.mjs +33 -9
  43. package/dist/index.mjs.map +1 -1
  44. package/dist/options-CZYdDtTW.mjs +581 -0
  45. package/dist/options-CZYdDtTW.mjs.map +1 -0
  46. package/dist/{plugin-CIrKPW8K.d.mts → plugin-DkFXWcCD.d.mts} +11 -2
  47. package/dist/plugin-DkFXWcCD.d.mts.map +1 -0
  48. package/dist/{template-DNhEgY8-.mjs → template-DDl7rsgo.mjs} +2 -2
  49. package/dist/{template-DNhEgY8-.mjs.map → template-DDl7rsgo.mjs.map} +1 -1
  50. package/dist/utils-DeMLVlNS.mjs +628 -0
  51. package/dist/utils-DeMLVlNS.mjs.map +1 -0
  52. package/dist/{vrt-DVub2B6r.d.mts → vrt-3T_e4bJ3.d.mts} +7 -1
  53. package/dist/vrt-3T_e4bJ3.d.mts.map +1 -0
  54. package/dist/{vrt-BPwRc95_.d.mts → vrt-6iDBfqNg.d.mts} +2 -2
  55. package/dist/{vrt-BPwRc95_.d.mts.map → vrt-6iDBfqNg.d.mts.map} +1 -1
  56. package/dist/{vrt-UxzWFygI.mjs → vrt-VuGK9wTX.mjs} +11 -4
  57. package/dist/vrt-VuGK9wTX.mjs.map +1 -0
  58. package/dist/vrt.d.mts +1 -1
  59. package/dist/vrt.mjs +1 -1
  60. package/package.json +4 -4
  61. package/dist/a11y-Ci9EEv6F.mjs.map +0 -1
  62. package/dist/index-HVtO9FK5.d.mts +0 -170
  63. package/dist/index-HVtO9FK5.d.mts.map +0 -1
  64. package/dist/options-Cd7bzLGr.mjs +0 -1142
  65. package/dist/options-Cd7bzLGr.mjs.map +0 -1
  66. package/dist/plugin-CIrKPW8K.d.mts.map +0 -1
  67. package/dist/vrt-DVub2B6r.d.mts.map +0 -1
  68. package/dist/vrt-UxzWFygI.mjs.map +0 -1
@@ -0,0 +1,628 @@
1
+ import { createRequire } from "node:module";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { randomBytes, timingSafeEqual } from "node:crypto";
5
+ //#region src/with-defaults.ts
6
+ /**
7
+ * Parsing helpers for extracting prop defaults from a Vue SFC `<script setup>`
8
+ * block. Defaults are read only from `withDefaults(defineProps(...), { ... })`
9
+ * so unrelated local identifiers are never treated as prop defaults.
10
+ */
11
+ function extractWithDefaults(scriptContent) {
12
+ const defaults = /* @__PURE__ */ new Map();
13
+ let searchIndex = 0;
14
+ while (searchIndex < scriptContent.length) {
15
+ const calleeIndex = scriptContent.indexOf("withDefaults", searchIndex);
16
+ if (calleeIndex === -1) break;
17
+ searchIndex = calleeIndex + 12;
18
+ if (!isIdentifierBoundary(scriptContent[calleeIndex - 1])) continue;
19
+ if (!isIdentifierBoundary(scriptContent[searchIndex])) continue;
20
+ const parenIndex = skipWhitespace(scriptContent, searchIndex);
21
+ if (scriptContent[parenIndex] !== "(") continue;
22
+ const endParen = findMatching(scriptContent, parenIndex, "(", ")");
23
+ if (endParen === -1) continue;
24
+ const defaultsArg = splitTopLevel(scriptContent.slice(parenIndex + 1, endParen))[1]?.trim();
25
+ if (!defaultsArg?.startsWith("{")) continue;
26
+ const objectStart = scriptContent.indexOf(defaultsArg, parenIndex + 1);
27
+ const objectEnd = findMatching(scriptContent, objectStart, "{", "}");
28
+ if (objectEnd === -1) continue;
29
+ for (const [name, value] of objectProperties(scriptContent.slice(objectStart + 1, objectEnd))) defaults.set(name, value);
30
+ }
31
+ return defaults;
32
+ }
33
+ function objectProperties(objectBody) {
34
+ const properties = [];
35
+ for (const item of splitTopLevel(objectBody)) {
36
+ const colonIndex = topLevelColon(item);
37
+ if (colonIndex === -1) continue;
38
+ const rawKey = item.slice(0, colonIndex).trim();
39
+ const key = rawKey.match(/^[$A-Z_a-z][$\w]*$/)?.[0] ?? rawKey.match(/^["']([^"']+)["']$/)?.[1];
40
+ if (!key) continue;
41
+ const value = item.slice(colonIndex + 1).trim();
42
+ if (value) properties.push([key, value]);
43
+ }
44
+ return properties;
45
+ }
46
+ function splitTopLevel(source) {
47
+ const parts = [];
48
+ let start = 0;
49
+ let round = 0;
50
+ let curly = 0;
51
+ let square = 0;
52
+ let quote = null;
53
+ let escaped = false;
54
+ for (let index = 0; index < source.length; index++) {
55
+ const char = source[index];
56
+ if (quote) {
57
+ if (escaped) escaped = false;
58
+ else if (char === "\\") escaped = true;
59
+ else if (char === quote) quote = null;
60
+ continue;
61
+ }
62
+ if (char === "\"" || char === "'" || char === "`") {
63
+ quote = char;
64
+ continue;
65
+ }
66
+ if (char === "(") round++;
67
+ else if (char === ")") round--;
68
+ else if (char === "{") curly++;
69
+ else if (char === "}") curly--;
70
+ else if (char === "[") square++;
71
+ else if (char === "]") square--;
72
+ else if (char === "," && round === 0 && curly === 0 && square === 0) {
73
+ parts.push(source.slice(start, index).trim());
74
+ start = index + 1;
75
+ }
76
+ }
77
+ const tail = source.slice(start).trim();
78
+ if (tail) parts.push(tail);
79
+ return parts;
80
+ }
81
+ function topLevelColon(source) {
82
+ let round = 0;
83
+ let curly = 0;
84
+ let square = 0;
85
+ let quote = null;
86
+ let escaped = false;
87
+ for (let index = 0; index < source.length; index++) {
88
+ const char = source[index];
89
+ if (quote) {
90
+ if (escaped) escaped = false;
91
+ else if (char === "\\") escaped = true;
92
+ else if (char === quote) quote = null;
93
+ continue;
94
+ }
95
+ if (char === "\"" || char === "'" || char === "`") {
96
+ quote = char;
97
+ continue;
98
+ }
99
+ if (char === "(") round++;
100
+ else if (char === ")") round--;
101
+ else if (char === "{") curly++;
102
+ else if (char === "}") curly--;
103
+ else if (char === "[") square++;
104
+ else if (char === "]") square--;
105
+ else if (char === ":" && round === 0 && curly === 0 && square === 0) return index;
106
+ }
107
+ return -1;
108
+ }
109
+ function findMatching(source, start, open, close) {
110
+ let depth = 0;
111
+ let quote = null;
112
+ let escaped = false;
113
+ for (let index = start; index < source.length; index++) {
114
+ const char = source[index];
115
+ if (quote) {
116
+ if (escaped) escaped = false;
117
+ else if (char === "\\") escaped = true;
118
+ else if (char === quote) quote = null;
119
+ continue;
120
+ }
121
+ if (char === "\"" || char === "'" || char === "`") {
122
+ quote = char;
123
+ continue;
124
+ }
125
+ if (char === open) depth++;
126
+ if (char === close) {
127
+ depth--;
128
+ if (depth === 0) return index;
129
+ }
130
+ }
131
+ return -1;
132
+ }
133
+ function skipWhitespace(source, index) {
134
+ while (index < source.length && /\s/.test(source[index])) index++;
135
+ return index;
136
+ }
137
+ function isIdentifierBoundary(char) {
138
+ return char === void 0 || !/[$\w]/.test(char);
139
+ }
140
+ //#endregion
141
+ //#region src/native-loader.ts
142
+ /**
143
+ * Native binding loader for @vizejs/native.
144
+ *
145
+ * Provides lazy-loading of the native Rust-based parser and a JS fallback
146
+ * for SFC analysis when the native `analyzeSfc` function is unavailable.
147
+ */
148
+ let native = null;
149
+ function loadNative() {
150
+ if (native) return native;
151
+ const require = createRequire(import.meta.url);
152
+ try {
153
+ native = require("@vizejs/native");
154
+ return native;
155
+ } catch (e) {
156
+ throw new Error(`Failed to load @vizejs/native. Make sure it's installed and built:\n${String(e)}`);
157
+ }
158
+ }
159
+ /**
160
+ * JS-based fallback for SFC analysis when native `analyzeSfc` is not available.
161
+ * Uses regex parsing to extract props and emits from Vue SFC source.
162
+ */
163
+ function analyzeSfcFallback(source, _options) {
164
+ try {
165
+ const props = [];
166
+ const emits = [];
167
+ const scriptSetupMatch = source.match(/<script\s+[^>]*setup[^>]*>([\s\S]*?)<\/script>/);
168
+ if (!scriptSetupMatch) {
169
+ if (!source.match(/<script[^>]*>([\s\S]*?)<\/script>/)) return {
170
+ props: [],
171
+ emits: []
172
+ };
173
+ }
174
+ const scriptContent = scriptSetupMatch?.[1] || "";
175
+ const propsMatch = scriptContent.match(/defineProps\s*<\s*\{([\s\S]*?)\}>\s*\(/);
176
+ const propsMatch2 = scriptContent.match(/defineProps\s*<\s*\{([\s\S]*?)\}>/);
177
+ const propsBody = propsMatch?.[1] || propsMatch2?.[1];
178
+ const withDefaults = extractWithDefaults(scriptContent);
179
+ if (propsBody) {
180
+ const lines = propsBody.split("\n");
181
+ let i = 0;
182
+ while (i < lines.length) {
183
+ const line = lines[i].trim();
184
+ if (line.startsWith("/**") || line.startsWith("*") || line.startsWith("*/")) {
185
+ i++;
186
+ continue;
187
+ }
188
+ const propMatch = line.match(/^(\w+)(\?)?:\s*(.+?)(?:;?\s*)$/);
189
+ if (propMatch) {
190
+ const name = propMatch[1];
191
+ const optional = !!propMatch[2];
192
+ let type = propMatch[3].replace(/;$/, "").trim();
193
+ const defaultValue = withDefaults.get(name);
194
+ props.push({
195
+ name,
196
+ type,
197
+ required: !optional && defaultValue === void 0,
198
+ ...defaultValue !== void 0 ? { default_value: defaultValue } : {}
199
+ });
200
+ }
201
+ i++;
202
+ }
203
+ }
204
+ const emitsMatch = scriptContent.match(/defineEmits\s*<\s*\{([\s\S]*?)\}>/);
205
+ if (emitsMatch) {
206
+ const emitsBody = emitsMatch[1];
207
+ const emitRegex = /(\w+)\s*:/g;
208
+ let match;
209
+ while ((match = emitRegex.exec(emitsBody)) !== null) emits.push(match[1]);
210
+ }
211
+ return {
212
+ props,
213
+ emits
214
+ };
215
+ } catch {
216
+ return {
217
+ props: [],
218
+ emits: []
219
+ };
220
+ }
221
+ }
222
+ //#endregion
223
+ //#region src/http-error.ts
224
+ var HttpError = class extends Error {
225
+ status;
226
+ constructor(message, status) {
227
+ super(message);
228
+ this.name = "HttpError";
229
+ this.status = status;
230
+ }
231
+ };
232
+ //#endregion
233
+ //#region src/trusted-path.ts
234
+ function realpathNearest(targetPath) {
235
+ let current = path.resolve(targetPath);
236
+ const missingParts = [];
237
+ while (true) try {
238
+ const real = fs.realpathSync.native(current);
239
+ return missingParts.length > 0 ? path.join(real, ...missingParts.reverse()) : real;
240
+ } catch {
241
+ const parent = path.dirname(current);
242
+ if (parent === current) return path.resolve(targetPath);
243
+ missingParts.push(path.basename(current));
244
+ current = parent;
245
+ }
246
+ }
247
+ function isResolvedPathInside(parentDir, candidatePath) {
248
+ const parent = path.resolve(parentDir);
249
+ const candidate = path.resolve(candidatePath);
250
+ const relative = path.relative(parent, candidate);
251
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
252
+ }
253
+ function isPathInsideAny(parentDirs, candidatePath) {
254
+ const candidate = realpathNearest(candidatePath);
255
+ return parentDirs.some((parentDir) => isResolvedPathInside(realpathNearest(parentDir), candidate));
256
+ }
257
+ /**
258
+ * True when `candidatePath` stays inside the project, or inside an extra root
259
+ * that was configured to live outside the project.
260
+ *
261
+ * Extra roots whose lexical path is still inside the project are not trusted
262
+ * after realpath — a planted `src` → `/etc` symlink must not widen the
263
+ * readable boundary.
264
+ */
265
+ function isTrustedSourcePath(projectRoot, extraLexicalRoots, candidatePath) {
266
+ const lexicalProject = path.resolve(projectRoot);
267
+ const realProject = realpathNearest(projectRoot);
268
+ const realCandidate = realpathNearest(candidatePath);
269
+ if (isResolvedPathInside(realProject, realCandidate)) return true;
270
+ for (const lexicalRoot of extraLexicalRoots) {
271
+ const resolvedLexical = path.resolve(lexicalRoot);
272
+ if (isResolvedPathInside(lexicalProject, resolvedLexical)) continue;
273
+ if (isResolvedPathInside(realpathNearest(resolvedLexical), realCandidate)) return true;
274
+ }
275
+ return false;
276
+ }
277
+ function resolveTrustedSourcePath(projectRoot, extraLexicalRoots, candidatePath, label = "path") {
278
+ if (candidatePath.includes("\0")) throw new HttpError(`${label} contains an invalid character`, 400);
279
+ const resolved = path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(path.resolve(projectRoot), candidatePath);
280
+ if (!isTrustedSourcePath(projectRoot, extraLexicalRoots, resolved)) throw new HttpError(`${label} escapes the allowed directory`, 400);
281
+ return resolved;
282
+ }
283
+ function resolveInside(parentDir, candidatePath, label = "path") {
284
+ return resolveInsideAny([parentDir], candidatePath, label);
285
+ }
286
+ function resolveInsideAny(parentDirs, candidatePath, label = "path") {
287
+ if (candidatePath.includes("\0")) throw new HttpError(`${label} contains an invalid character`, 400);
288
+ if (parentDirs.length === 0) throw new HttpError(`No allowed directories configured for ${label}`, 500);
289
+ const parent = path.resolve(parentDirs[0] ?? ".");
290
+ const resolved = path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(parent, candidatePath);
291
+ if (!isPathInsideAny(parentDirs, resolved)) throw new HttpError(`${label} escapes the allowed directory`, 400);
292
+ return resolved;
293
+ }
294
+ function resolveUrlPathInside(parentDir, requestUrl, label = "path") {
295
+ let pathname = decodeUrlComponent(requestUrl.split(/[?#]/, 1)[0] || "/", label);
296
+ pathname = pathname.replaceAll("\\", "/");
297
+ if (pathname.split("/").includes("..")) throw new HttpError(`${label} must not contain parent directory segments`, 400);
298
+ return resolveInside(parentDir, `.${pathname}`, label);
299
+ }
300
+ function decodeUrlComponent(value, label = "path") {
301
+ try {
302
+ return decodeURIComponent(value);
303
+ } catch {
304
+ throw new HttpError(`${label} is not valid URL encoding`, 400);
305
+ }
306
+ }
307
+ function hasSuffix(candidatePath, suffix) {
308
+ return realpathNearest(candidatePath).toLowerCase().endsWith(suffix);
309
+ }
310
+ /**
311
+ * True when the realpath still looks like a Vue SFC. A planted
312
+ * `Evil.vue` → `.env` symlink must not be treated as component source.
313
+ */
314
+ function isVueSourcePath(candidatePath) {
315
+ return hasSuffix(candidatePath, ".vue");
316
+ }
317
+ function assertVueSourcePath(candidatePath, label = "path") {
318
+ if (!isVueSourcePath(candidatePath)) throw new HttpError(`${label} must be a .vue file`, 400);
319
+ }
320
+ /** Accept Vue SFCs and script modules while validating the resolved realpath. */
321
+ function assertComponentSourcePath(candidatePath, label = "path") {
322
+ const realPath = realpathNearest(candidatePath).toLowerCase();
323
+ if (!/\.(?:vue|ts|tsx|js|jsx)$/.test(realPath)) throw new HttpError(`${label} must be a .vue, .ts, .tsx, .js, or .jsx file`, 400);
324
+ }
325
+ function isArtVueSourcePath(candidatePath) {
326
+ return hasSuffix(candidatePath, ".art.vue");
327
+ }
328
+ function assertArtVueSourcePath(candidatePath, label = "art path") {
329
+ if (!isArtVueSourcePath(candidatePath)) throw new HttpError(`${label} must be a .art.vue file`, 400);
330
+ }
331
+ //#endregion
332
+ //#region src/security.ts
333
+ const DEFAULT_API_BODY_LIMIT_BYTES = 1024 * 1024;
334
+ function createDevSessionToken() {
335
+ return randomBytes(32).toString("base64url");
336
+ }
337
+ function collectRequestBody(req, limit = DEFAULT_API_BODY_LIMIT_BYTES) {
338
+ return new Promise((resolve, reject) => {
339
+ let body = "";
340
+ let size = 0;
341
+ let completed = false;
342
+ req.on("data", (chunk) => {
343
+ if (completed) return;
344
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
345
+ size += buffer.byteLength;
346
+ if (size > limit) {
347
+ completed = true;
348
+ reject(new HttpError(`Request body exceeds ${limit} bytes`, 413));
349
+ return;
350
+ }
351
+ body += buffer.toString("utf-8");
352
+ });
353
+ req.on("end", () => {
354
+ if (!completed) {
355
+ completed = true;
356
+ resolve(body);
357
+ }
358
+ });
359
+ req.on("error", (error) => {
360
+ if (!completed) {
361
+ completed = true;
362
+ reject(error);
363
+ }
364
+ });
365
+ });
366
+ }
367
+ function parseJsonBody(body) {
368
+ try {
369
+ return JSON.parse(body);
370
+ } catch (error) {
371
+ if (error instanceof SyntaxError) throw new HttpError("Malformed JSON body", 400);
372
+ throw error;
373
+ }
374
+ }
375
+ function isLoopbackAddress(address) {
376
+ const value = address.trim().toLowerCase().replace(/^\[|\]$/g, "");
377
+ if (value === "localhost" || value === "::1") return true;
378
+ if (value.startsWith("::ffff:") || value.startsWith(":ffff:")) return isLoopbackAddress(value.slice(value.indexOf("ffff:") + 5));
379
+ const parts = value.split(".");
380
+ if (parts.length !== 4 || parts[0] !== "127") return false;
381
+ return parts.every((part, index) => {
382
+ if (index === 0) return true;
383
+ const octet = Number(part);
384
+ return Number.isInteger(octet) && octet >= 0 && octet <= 255;
385
+ });
386
+ }
387
+ function isLoopbackRequest(req) {
388
+ const remote = req.socket?.remoteAddress;
389
+ if (!remote) return false;
390
+ return isLoopbackAddress(remote);
391
+ }
392
+ function validateDevApiRequest(req, sessionToken) {
393
+ const originError = validateOrigin(req);
394
+ if (originError) return originError;
395
+ if (!isUnsafeMethod(req.method)) return null;
396
+ if (!isLoopbackRequest(req)) return new HttpError("Musea write APIs are limited to loopback clients", 403);
397
+ if (!hasValidSessionToken(req, sessionToken)) return new HttpError("Invalid Musea dev session token", 403);
398
+ if (!isJsonRequest(req)) return new HttpError("Content-Type must be application/json", 415);
399
+ return null;
400
+ }
401
+ function serializeScriptValue(value) {
402
+ return (JSON.stringify(value) ?? "undefined").replace(/[<>&\u2028\u2029]/g, (char) => {
403
+ switch (char) {
404
+ case "<": return "\\u003C";
405
+ case ">": return "\\u003E";
406
+ case "&": return "\\u0026";
407
+ case "\u2028": return "\\u2028";
408
+ case "\u2029": return "\\u2029";
409
+ default: return char;
410
+ }
411
+ });
412
+ }
413
+ function isUnsafeMethod(method) {
414
+ return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
415
+ }
416
+ function isJsonRequest(req) {
417
+ return getHeader(req, "content-type")?.split(";")[0]?.trim().toLowerCase() === "application/json";
418
+ }
419
+ function validateOrigin(req) {
420
+ if (getHeader(req, "sec-fetch-site") === "cross-site") return new HttpError("Cross-origin Musea API requests are not allowed", 403);
421
+ const origin = getHeader(req, "origin");
422
+ if (!origin) return null;
423
+ const host = getHeader(req, "host");
424
+ if (!host) return new HttpError("Missing Host header", 400);
425
+ try {
426
+ if (new URL(origin).host !== host) return new HttpError("Cross-origin Musea API requests are not allowed", 403);
427
+ } catch {
428
+ return new HttpError("Invalid Origin header", 400);
429
+ }
430
+ return null;
431
+ }
432
+ function hasValidSessionToken(req, expectedToken) {
433
+ const actualToken = getHeader(req, "x-musea-session");
434
+ if (!actualToken) return false;
435
+ const actual = Buffer.from(actualToken);
436
+ const expected = Buffer.from(expectedToken);
437
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
438
+ }
439
+ function getHeader(req, name) {
440
+ const value = req.headers[name];
441
+ if (Array.isArray(value)) return value[0];
442
+ return value;
443
+ }
444
+ //#endregion
445
+ //#region src/theme-config.ts
446
+ /**
447
+ * Build the theme config object from plugin options for runtime injection.
448
+ */
449
+ function buildThemeConfig(theme) {
450
+ if (!theme) return void 0;
451
+ if (typeof theme === "string") return { default: theme };
452
+ const themes = Array.isArray(theme) ? theme : [theme];
453
+ const custom = {};
454
+ for (const t of themes) custom[t.name] = {
455
+ base: t.base,
456
+ colors: t.colors
457
+ };
458
+ return {
459
+ default: themes[0].name,
460
+ custom
461
+ };
462
+ }
463
+ //#endregion
464
+ //#region src/utils.ts
465
+ /**
466
+ * Shared utility functions for the Musea Vite plugin.
467
+ */
468
+ function normalizeGlobPath(filepath) {
469
+ return filepath.split(path.sep).join("/");
470
+ }
471
+ function matchesPattern(file, pattern, root) {
472
+ const normalizedPattern = normalizeGlobPath(pattern);
473
+ return matchGlob(path.isAbsolute(pattern) ? path.resolve(file) : path.relative(root, file), normalizedPattern);
474
+ }
475
+ function shouldProcess(file, include, exclude, root) {
476
+ for (const pattern of exclude) if (matchesPattern(file, pattern, root)) return false;
477
+ for (const pattern of include) if (matchesPattern(file, pattern, root)) return true;
478
+ return false;
479
+ }
480
+ function matchGlob(filepath, pattern) {
481
+ const normalizedFilepath = normalizeGlobPath(filepath);
482
+ const normalizedPattern = normalizeGlobPath(pattern);
483
+ const PLACEHOLDER = "<<GLOBSTAR>>";
484
+ const SEGMENT_PLACEHOLDER = "<<GLOBSTAR_SEGMENT>>";
485
+ const regex = normalizedPattern.replaceAll("**/", SEGMENT_PLACEHOLDER).replaceAll("**", PLACEHOLDER).replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, "[^/]*").replaceAll(SEGMENT_PLACEHOLDER, "(?:.*/)?").replaceAll(PLACEHOLDER, ".*");
486
+ return new RegExp(`^${regex}$`).test(normalizedFilepath);
487
+ }
488
+ async function readGitignoreDirectoryNames(bases) {
489
+ const names = /* @__PURE__ */ new Set();
490
+ for (const base of bases) {
491
+ let text = "";
492
+ try {
493
+ text = await fs.promises.readFile(path.join(base, ".gitignore"), "utf8");
494
+ } catch {
495
+ continue;
496
+ }
497
+ for (const line of text.split("\n")) {
498
+ const trimmed = line.trim();
499
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
500
+ const cleaned = trimmed.replace(/^\//, "").replace(/\/$/, "");
501
+ if (!cleaned || cleaned.includes("*") || cleaned.includes("/")) continue;
502
+ names.add(cleaned);
503
+ }
504
+ }
505
+ return names;
506
+ }
507
+ async function shouldSkipScanDirectory(name, fullPath, scanRoots, ignoredDirNames) {
508
+ if (scanRoots.some((scanRoot) => path.resolve(scanRoot) === path.resolve(fullPath))) return false;
509
+ if (name.startsWith(".") || ignoredDirNames.has(name)) return true;
510
+ try {
511
+ await fs.promises.lstat(path.join(fullPath, ".git"));
512
+ return true;
513
+ } catch {
514
+ return false;
515
+ }
516
+ }
517
+ function resolveScanRoot(root, pattern) {
518
+ const absolutePattern = path.isAbsolute(pattern) ? pattern : path.resolve(root, pattern);
519
+ const normalizedPattern = normalizeGlobPath(absolutePattern);
520
+ const globIndex = normalizedPattern.search(/[*[{]/);
521
+ if (globIndex === -1) return path.dirname(absolutePattern);
522
+ const staticPrefix = normalizedPattern.slice(0, globIndex);
523
+ if (!staticPrefix) return root;
524
+ if (staticPrefix.endsWith("/")) return path.resolve(staticPrefix.slice(0, -1));
525
+ return path.resolve(path.dirname(staticPrefix));
526
+ }
527
+ function resolveScanRoots(root, include) {
528
+ const roots = /* @__PURE__ */ new Set();
529
+ for (const pattern of include) roots.add(resolveScanRoot(root, pattern));
530
+ if (roots.size === 0) roots.add(root);
531
+ return [...roots];
532
+ }
533
+ async function scanArtFiles(root, include, exclude, scanInlineArt = false) {
534
+ const files = /* @__PURE__ */ new Set();
535
+ const scanRoots = resolveScanRoots(root, include);
536
+ const visitedDirs = /* @__PURE__ */ new Set();
537
+ const ignoredDirNames = await readGitignoreDirectoryNames([root, ...scanRoots]);
538
+ async function scan(dir) {
539
+ const resolvedDir = path.resolve(dir);
540
+ if (visitedDirs.has(resolvedDir)) return;
541
+ visitedDirs.add(resolvedDir);
542
+ let entries;
543
+ try {
544
+ entries = await fs.promises.readdir(resolvedDir, { withFileTypes: true });
545
+ } catch {
546
+ return;
547
+ }
548
+ for (const entry of entries) {
549
+ const fullPath = path.join(resolvedDir, entry.name);
550
+ let excluded = false;
551
+ for (const pattern of exclude) if (matchesPattern(fullPath, pattern, root) || matchGlob(entry.name, pattern)) {
552
+ excluded = true;
553
+ break;
554
+ }
555
+ if (excluded) continue;
556
+ if (entry.isSymbolicLink()) continue;
557
+ if (entry.isDirectory()) {
558
+ if (await shouldSkipScanDirectory(entry.name, fullPath, scanRoots, ignoredDirNames)) continue;
559
+ await scan(fullPath);
560
+ } else if (entry.isFile() && entry.name.endsWith(".art.vue")) {
561
+ if (shouldProcess(fullPath, include, exclude, root) && isTrustedSourcePath(root, scanRoots, fullPath)) files.add(fullPath);
562
+ } else if (scanInlineArt && entry.isFile() && entry.name.endsWith(".vue") && !entry.name.endsWith(".art.vue")) {
563
+ if (!isTrustedSourcePath(root, scanRoots, fullPath)) continue;
564
+ if ((await fs.promises.readFile(fullPath, "utf-8")).includes("<art")) files.add(fullPath);
565
+ }
566
+ }
567
+ }
568
+ for (const scanRoot of scanRoots) await scan(scanRoot);
569
+ return [...files];
570
+ }
571
+ async function generateStorybookFiles(artFiles, root, outDir) {
572
+ const binding = loadNative();
573
+ const outputDir = path.resolve(root, outDir);
574
+ await fs.promises.mkdir(outputDir, { recursive: true });
575
+ for (const [filePath, art] of artFiles) try {
576
+ const source = await fs.promises.readFile(filePath, "utf-8");
577
+ const csf = binding.artToCsf(source, { filename: filePath });
578
+ const outputPath = path.join(outputDir, csf.filename);
579
+ const code = rewriteStorybookScriptImports(rewriteStorybookComponentImport(csf.code, art, filePath, outputPath), filePath, outputPath);
580
+ await fs.promises.writeFile(outputPath, code, "utf-8");
581
+ console.log(`[musea] Generated: ${path.relative(root, outputPath)}`);
582
+ } catch (e) {
583
+ console.error(`[musea] Failed to generate CSF for ${filePath}:`, e);
584
+ }
585
+ }
586
+ function rewriteStorybookComponentImport(code, art, artPath, outputPath) {
587
+ const component = art.isInline && art.componentPath ? art.componentPath : art.metadata.component;
588
+ if (!component || isBareImport(component)) return code;
589
+ const resolvedComponent = path.isAbsolute(component) ? component : path.resolve(path.dirname(artPath), component);
590
+ let relativeComponent = normalizeGlobPath(path.relative(path.dirname(outputPath), resolvedComponent));
591
+ if (!relativeComponent.startsWith(".")) relativeComponent = `./${relativeComponent}`;
592
+ return code.replace(/import\s+(__museaComponent)\s+from\s+(['"])([^'"]+)\2;/, `import $1 from $2${relativeComponent}$2;`);
593
+ }
594
+ function isBareImport(source) {
595
+ return !source.startsWith(".") && !path.isAbsolute(source);
596
+ }
597
+ /**
598
+ * Rewrite relative import specifiers in the generated Storybook CSF file so that
599
+ * imports lifted verbatim from the art file's `<script setup>` resolve against
600
+ * the generated file's location instead of the original art file's directory.
601
+ *
602
+ * The `__museaComponent` import is already rebased by
603
+ * `rewriteStorybookComponentImport` and is skipped here.
604
+ */
605
+ function rewriteStorybookScriptImports(code, artPath, outputPath) {
606
+ const artDir = path.dirname(artPath);
607
+ const outDir = path.dirname(outputPath);
608
+ return code.replace(/(import\s+(?:[\s\S]*?)\s+from\s+)(['"])([^'"]+)\2(\s*;?)/g, (match, head, quote, specifier, tail) => {
609
+ if (!specifier.startsWith(".")) return match;
610
+ if (/\b__museaComponent\b/.test(head)) return match;
611
+ const resolved = path.resolve(artDir, specifier);
612
+ let rebased = normalizeGlobPath(path.relative(outDir, resolved));
613
+ if (!rebased.startsWith(".")) rebased = `./${rebased}`;
614
+ return `${head}${quote}${rebased}${quote}${tail}`;
615
+ });
616
+ }
617
+ function toPascalCase(str) {
618
+ const pascal = str.normalize("NFKD").replace(/[^\p{L}\p{N}]+/gu, " ").trim().split(/\s+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
619
+ if (!pascal) return "Variant";
620
+ return /^[\p{L}_$]/u.test(pascal) ? pascal : `Variant${pascal}`;
621
+ }
622
+ function escapeHtml(str) {
623
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
624
+ }
625
+ //#endregion
626
+ export { resolveUrlPathInside as C, loadNative as E, resolveTrustedSourcePath as S, analyzeSfcFallback as T, assertVueSourcePath as _, scanArtFiles as a, resolveInside as b, buildThemeConfig as c, createDevSessionToken as d, parseJsonBody as f, assertComponentSourcePath as g, assertArtVueSourcePath as h, resolveScanRoots as i, DEFAULT_API_BODY_LIMIT_BYTES as l, validateDevApiRequest as m, generateStorybookFiles as n, shouldProcess as o, serializeScriptValue as p, matchGlob as r, toPascalCase as s, escapeHtml as t, collectRequestBody as u, decodeUrlComponent as v, HttpError as w, resolveInsideAny as x, isVueSourcePath as y };
627
+
628
+ //# sourceMappingURL=utils-DeMLVlNS.mjs.map