@vizejs/vite-plugin-musea 0.82.0 → 0.83.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.
Files changed (27) hide show
  1. package/dist/gallery/assets/{MonacoEditor-Dmje1nZi.js → MonacoEditor-VccI_xm1.js} +2 -2
  2. package/dist/gallery/assets/{cssMode-CHkzGWlG.js → cssMode-DYl_gbi2.js} +1 -1
  3. package/dist/gallery/assets/{editor.api2-DSQ9Vpiw.js → editor.api2-C1iuJsz5.js} +1 -1
  4. package/dist/gallery/assets/{editor.main-Dr80cIvC.js → editor.main-CB_LUZEd.js} +2 -2
  5. package/dist/gallery/assets/{freemarker2-C590qJFy.js → freemarker2-CwuG5RrE.js} +1 -1
  6. package/dist/gallery/assets/{handlebars-DMRDuFi1.js → handlebars-DQnqIqtZ.js} +1 -1
  7. package/dist/gallery/assets/{html-TtN3jv37.js → html-Cj3GxbMF.js} +1 -1
  8. package/dist/gallery/assets/{htmlMode-By_JGvvp.js → htmlMode-ckJrF6jc.js} +1 -1
  9. package/dist/gallery/assets/{index-jXDqIaD8.js → index-6QLn8Kxp.js} +3 -3
  10. package/dist/gallery/assets/{index-CwT3Ex21.css → index-P1L8IaBA.css} +1 -1
  11. package/dist/gallery/assets/{javascript-y17h-ata.js → javascript-DNOPgN7f.js} +1 -1
  12. package/dist/gallery/assets/{jsonMode-n1uMwhuY.js → jsonMode-BMPHk4yv.js} +1 -1
  13. package/dist/gallery/assets/{liquid-D-a8XQan.js → liquid-cUC3T7D4.js} +1 -1
  14. package/dist/gallery/assets/{lspLanguageFeatures-6bRTxJu4.js → lspLanguageFeatures-DnNJLDpx.js} +1 -1
  15. package/dist/gallery/assets/{mdx-CC7DfHFT.js → mdx-DDDAX75G.js} +1 -1
  16. package/dist/gallery/assets/{monaco.contribution-CfGRLrR9.js → monaco.contribution-BsZfIZIS.js} +2 -2
  17. package/dist/gallery/assets/{python-BUAcshPr.js → python-CkEL94qN.js} +1 -1
  18. package/dist/gallery/assets/{razor-C5P2I6v1.js → razor-DpH3myLF.js} +1 -1
  19. package/dist/gallery/assets/{tsMode-B927vf_Z.js → tsMode-D4vJtygH.js} +1 -1
  20. package/dist/gallery/assets/{typescript-DTfzyqGo.js → typescript-f3jEZleA.js} +1 -1
  21. package/dist/gallery/assets/{workers-DiXcHCU4.js → workers-7fp33vpu.js} +1 -1
  22. package/dist/gallery/assets/{xml-CnjQeZMF.js → xml-CrndFNV8.js} +1 -1
  23. package/dist/gallery/assets/{yaml-KgJNvybZ.js → yaml-BAl6eAY9.js} +1 -1
  24. package/dist/gallery/index.html +2 -2
  25. package/dist/index.mjs +227 -174
  26. package/dist/index.mjs.map +1 -1
  27. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -6,8 +6,8 @@ import { transformWithEsbuild } from "vite";
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { vizeConfigStore } from "@vizejs/vite-plugin";
9
- import { fileURLToPath } from "node:url";
10
9
  import { randomBytes, timingSafeEqual } from "node:crypto";
10
+ import { fileURLToPath } from "node:url";
11
11
  //#region src/native-loader.ts
12
12
  /**
13
13
  * Native binding loader for @vizejs/native.
@@ -91,6 +91,156 @@ function analyzeSfcFallback(source, _options) {
91
91
  }
92
92
  }
93
93
  //#endregion
94
+ //#region src/security.ts
95
+ const DEFAULT_API_BODY_LIMIT_BYTES = 1024 * 1024;
96
+ var HttpError = class extends Error {
97
+ status;
98
+ constructor(message, status) {
99
+ super(message);
100
+ this.name = "HttpError";
101
+ this.status = status;
102
+ }
103
+ };
104
+ function createDevSessionToken() {
105
+ return randomBytes(32).toString("base64url");
106
+ }
107
+ function realpathNearest(targetPath) {
108
+ let current = path.resolve(targetPath);
109
+ const missingParts = [];
110
+ while (true) try {
111
+ const real = fs.realpathSync.native(current);
112
+ return missingParts.length > 0 ? path.join(real, ...missingParts.reverse()) : real;
113
+ } catch {
114
+ const parent = path.dirname(current);
115
+ if (parent === current) return path.resolve(targetPath);
116
+ missingParts.push(path.basename(current));
117
+ current = parent;
118
+ }
119
+ }
120
+ function isResolvedPathInside(parentDir, candidatePath) {
121
+ const parent = path.resolve(parentDir);
122
+ const candidate = path.resolve(candidatePath);
123
+ const relative = path.relative(parent, candidate);
124
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
125
+ }
126
+ function isPathInsideAny(parentDirs, candidatePath) {
127
+ const candidate = realpathNearest(candidatePath);
128
+ return parentDirs.some((parentDir) => isResolvedPathInside(realpathNearest(parentDir), candidate));
129
+ }
130
+ function resolveInside(parentDir, candidatePath, label = "path") {
131
+ return resolveInsideAny([parentDir], candidatePath, label);
132
+ }
133
+ function resolveInsideAny(parentDirs, candidatePath, label = "path") {
134
+ if (candidatePath.includes("\0")) throw new HttpError(`${label} contains an invalid character`, 400);
135
+ if (parentDirs.length === 0) throw new HttpError(`No allowed directories configured for ${label}`, 500);
136
+ const parent = path.resolve(parentDirs[0] ?? ".");
137
+ const resolved = path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(parent, candidatePath);
138
+ if (!isPathInsideAny(parentDirs, resolved)) throw new HttpError(`${label} escapes the allowed directory`, 400);
139
+ return resolved;
140
+ }
141
+ function resolveUrlPathInside(parentDir, requestUrl, label = "path") {
142
+ const rawPath = requestUrl.split(/[?#]/, 1)[0] || "/";
143
+ let pathname;
144
+ try {
145
+ pathname = decodeURIComponent(rawPath);
146
+ } catch {
147
+ throw new HttpError(`${label} is not valid URL encoding`, 400);
148
+ }
149
+ pathname = pathname.replaceAll("\\", "/");
150
+ if (pathname.split("/").includes("..")) throw new HttpError(`${label} must not contain parent directory segments`, 400);
151
+ return resolveInside(parentDir, `.${pathname}`, label);
152
+ }
153
+ function collectRequestBody(req, limit = DEFAULT_API_BODY_LIMIT_BYTES) {
154
+ return new Promise((resolve, reject) => {
155
+ let body = "";
156
+ let size = 0;
157
+ let completed = false;
158
+ req.on("data", (chunk) => {
159
+ if (completed) return;
160
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
161
+ size += buffer.byteLength;
162
+ if (size > limit) {
163
+ completed = true;
164
+ reject(new HttpError(`Request body exceeds ${limit} bytes`, 413));
165
+ return;
166
+ }
167
+ body += buffer.toString("utf-8");
168
+ });
169
+ req.on("end", () => {
170
+ if (!completed) {
171
+ completed = true;
172
+ resolve(body);
173
+ }
174
+ });
175
+ req.on("error", (error) => {
176
+ if (!completed) {
177
+ completed = true;
178
+ reject(error);
179
+ }
180
+ });
181
+ });
182
+ }
183
+ function validateDevApiRequest(req, sessionToken) {
184
+ const originError = validateOrigin(req);
185
+ if (originError) return originError;
186
+ if (!isUnsafeMethod(req.method)) return null;
187
+ if (!hasValidSessionToken(req, sessionToken)) return new HttpError("Invalid Musea dev session token", 403);
188
+ if (!isJsonRequest(req)) return new HttpError("Content-Type must be application/json", 415);
189
+ return null;
190
+ }
191
+ function serializeScriptValue(value) {
192
+ return (JSON.stringify(value) ?? "undefined").replace(/[<>&\u2028\u2029]/g, (char) => {
193
+ switch (char) {
194
+ case "<": return "\\u003C";
195
+ case ">": return "\\u003E";
196
+ case "&": return "\\u0026";
197
+ case "\u2028": return "\\u2028";
198
+ case "\u2029": return "\\u2029";
199
+ default: return char;
200
+ }
201
+ });
202
+ }
203
+ function isUnsafeMethod(method) {
204
+ return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
205
+ }
206
+ function isJsonRequest(req) {
207
+ return getHeader(req, "content-type")?.split(";")[0]?.trim().toLowerCase() === "application/json";
208
+ }
209
+ function validateOrigin(req) {
210
+ if (getHeader(req, "sec-fetch-site") === "cross-site") return new HttpError("Cross-origin Musea API requests are not allowed", 403);
211
+ const origin = getHeader(req, "origin");
212
+ if (!origin) return null;
213
+ const host = getHeader(req, "host");
214
+ if (!host) return new HttpError("Missing Host header", 400);
215
+ try {
216
+ if (new URL(origin).host !== host) return new HttpError("Cross-origin Musea API requests are not allowed", 403);
217
+ } catch {
218
+ return new HttpError("Invalid Origin header", 400);
219
+ }
220
+ return null;
221
+ }
222
+ function hasValidSessionToken(req, expectedToken) {
223
+ const actualToken = getHeader(req, "x-musea-session");
224
+ if (!actualToken) return false;
225
+ const actual = Buffer.from(actualToken);
226
+ const expected = Buffer.from(expectedToken);
227
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
228
+ }
229
+ function getHeader(req, name) {
230
+ const value = req.headers[name];
231
+ if (Array.isArray(value)) return value[0];
232
+ return value;
233
+ }
234
+ //#endregion
235
+ //#region src/component-source.ts
236
+ function allowedSourceRoots(root, scanRoots = []) {
237
+ return [...new Set([root, ...scanRoots].map((sourceRoot) => path.resolve(sourceRoot)))];
238
+ }
239
+ function resolveComponentSourcePath(art, artPath, sourceRoots) {
240
+ const componentPath = 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;
241
+ return componentPath ? resolveInsideAny(sourceRoots, componentPath, "component path") : null;
242
+ }
243
+ //#endregion
94
244
  //#region src/utils.ts
95
245
  /**
96
246
  * Shared utility functions for the Musea Vite plugin.
@@ -183,9 +333,6 @@ function toPascalCase(str) {
183
333
  if (!pascal) return "Variant";
184
334
  return /^[\p{L}_$]/u.test(pascal) ? pascal : `Variant${pascal}`;
185
335
  }
186
- function escapeTemplate(str) {
187
- return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
188
- }
189
336
  function escapeHtml(str) {
190
337
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
191
338
  }
@@ -227,6 +374,9 @@ function resolveRelativeSpecifier(specifier, artDir) {
227
374
  function rewriteRelativeImportStatement(statement, artDir) {
228
375
  return statement.replace(/\bfrom\s+(['"])([^'"]+)\1/g, (_match, quote, specifier) => `from ${quote}${resolveRelativeSpecifier(specifier, artDir)}${quote}`).replace(/^(\s*import\s+)(['"])([^'"]+)\2(\s*;?\s*)$/s, (_match, prefix, quote, specifier, suffix) => `${prefix}${quote}${resolveRelativeSpecifier(specifier, artDir)}${quote}${suffix}`);
229
376
  }
377
+ function escapeTemplateLiteral(str) {
378
+ return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
379
+ }
230
380
  function countCharBalance(source, openChar, closeChar) {
231
381
  let balance = 0;
232
382
  for (const char of source) if (char === openChar) balance++;
@@ -395,16 +545,16 @@ function parseScriptSetupForArt(content) {
395
545
  returnNames: [...returnNames]
396
546
  };
397
547
  }
398
- function generateArtModule(art, filePath) {
548
+ function generateArtModule(art, filePath, options = {}) {
399
549
  let componentImportPath;
400
- let componentName;
550
+ let componentTagName;
551
+ const componentBindingName = "__MuseaComponent";
401
552
  if (art.isInline && art.componentPath) {
402
- componentImportPath = art.componentPath;
403
- componentName = path.basename(art.componentPath, ".vue");
553
+ componentImportPath = options.root ? resolveComponentSourcePath(art, filePath, allowedSourceRoots(options.root, options.scanRoots ?? [])) ?? void 0 : art.componentPath;
554
+ componentTagName = "MuseaComponent";
404
555
  } else if (art.metadata.component) {
405
- const comp = art.metadata.component;
406
- componentImportPath = path.isAbsolute(comp) ? comp : path.resolve(path.dirname(filePath), comp);
407
- componentName = path.basename(comp, ".vue");
556
+ componentImportPath = options.root ? resolveComponentSourcePath(art, filePath, allowedSourceRoots(options.root, options.scanRoots ?? [])) ?? void 0 : path.isAbsolute(art.metadata.component) ? art.metadata.component : path.resolve(path.dirname(filePath), art.metadata.component);
557
+ componentTagName = "MuseaComponent";
408
558
  }
409
559
  const scriptSetup = art.scriptSetupContent ? parseScriptSetupForArt(art.scriptSetupContent) : null;
410
560
  let code = `
@@ -418,12 +568,9 @@ import { defineComponent, h } from 'vue';
418
568
  code += `${resolved}\n`;
419
569
  }
420
570
  }
421
- if (componentImportPath && componentName) {
422
- if (!scriptSetup?.imports.some((imp) => {
423
- if (imp.includes(`from '${componentImportPath}'`) || imp.includes(`from "${componentImportPath}"`)) return true;
424
- return new RegExp(`^import\\s+${componentName}[\\s,]`).test(imp.trim());
425
- })) code += `import ${componentName} from '${componentImportPath}';\n`;
426
- code += `export const __component__ = ${componentName};\n`;
571
+ if (componentImportPath && componentTagName) {
572
+ if (!scriptSetup?.imports.some((imp) => new RegExp(`^import\\s+${componentBindingName}[\\s,]`).test(imp.trim()))) code += `import ${componentBindingName} from ${JSON.stringify(componentImportPath)};\n`;
573
+ code += `export const __component__ = ${componentBindingName};\n`;
427
574
  }
428
575
  code += `
429
576
  export const metadata = ${JSON.stringify(art.metadata)};
@@ -433,15 +580,15 @@ export const __styles__ = ${JSON.stringify(art.styleBlocks ?? [])};
433
580
  for (const variant of art.variants) {
434
581
  const variantComponentName = toPascalCase(variant.name);
435
582
  let template = variant.template;
436
- if (componentName) template = template.replace(/<Self/g, `<${componentName}`).replace(/<\/Self>/g, `</${componentName}>`);
437
- const escapedTemplate = template.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
438
- const fullTemplate = `<div data-variant="${variant.name}">${escapedTemplate}</div>`;
439
- const componentNames = /* @__PURE__ */ new Set();
440
- if (componentName) componentNames.add(componentName);
583
+ if (componentTagName) template = template.replace(/<Self/g, `<${componentTagName}`).replace(/<\/Self>/g, `</${componentTagName}>`);
584
+ const escapedTemplate = escapeTemplateLiteral(template);
585
+ const fullTemplate = `<div data-variant="${escapeTemplateLiteral(escapeHtml(variant.name))}">${escapedTemplate}</div>`;
586
+ const componentNames = /* @__PURE__ */ new Map();
587
+ if (componentTagName) componentNames.set(componentTagName, componentBindingName);
441
588
  if (scriptSetup) {
442
- for (const name of scriptSetup.returnNames) if (/^[A-Z]/.test(name)) componentNames.add(name);
589
+ for (const name of scriptSetup.returnNames) if (/^[A-Z]/.test(name)) componentNames.set(name, name);
443
590
  }
444
- const components = componentNames.size > 0 ? ` components: { ${[...componentNames].join(", ")} },\n` : "";
591
+ const components = componentNames.size > 0 ? ` components: { ${[...componentNames].map(([name, value]) => `${JSON.stringify(name)}: ${value}`).join(", ")} },\n` : "";
445
592
  const hasSetupBody = scriptSetup?.setupBody.some((line) => line.trim().length > 0) ?? false;
446
593
  if (scriptSetup && (hasSetupBody || scriptSetup.returnNames.length > 0)) code += `
447
594
  export const ${variantComponentName} = defineComponent({
@@ -453,7 +600,7 @@ ${scriptSetup.setupBody.map((l) => ` ${l}`).join("\n")}
453
600
  template: \`${fullTemplate}\`,
454
601
  });
455
602
  `;
456
- else if (componentName) code += `
603
+ else if (componentTagName) code += `
457
604
  export const ${variantComponentName} = {
458
605
  name: '${variantComponentName}',
459
606
  ${components} template: \`${fullTemplate}\`,
@@ -496,126 +643,6 @@ function generateGalleryStyles() {
496
643
  return `${styles_base_default}\n${styles_layout_default}\n${styles_components_default}`;
497
644
  }
498
645
  //#endregion
499
- //#region src/security.ts
500
- const DEFAULT_API_BODY_LIMIT_BYTES = 1024 * 1024;
501
- var HttpError = class extends Error {
502
- status;
503
- constructor(message, status) {
504
- super(message);
505
- this.name = "HttpError";
506
- this.status = status;
507
- }
508
- };
509
- function createDevSessionToken() {
510
- return randomBytes(32).toString("base64url");
511
- }
512
- function isPathInside(parentDir, candidatePath) {
513
- const parent = path.resolve(parentDir);
514
- const candidate = path.resolve(candidatePath);
515
- const relative = path.relative(parent, candidate);
516
- return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
517
- }
518
- function resolveInside(parentDir, candidatePath, label = "path") {
519
- if (candidatePath.includes("\0")) throw new HttpError(`${label} contains an invalid character`, 400);
520
- const parent = path.resolve(parentDir);
521
- const resolved = path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(parent, candidatePath);
522
- if (!isPathInside(parent, resolved)) throw new HttpError(`${label} escapes the allowed directory`, 400);
523
- return resolved;
524
- }
525
- function resolveUrlPathInside(parentDir, requestUrl, label = "path") {
526
- const rawPath = requestUrl.split(/[?#]/, 1)[0] || "/";
527
- let pathname;
528
- try {
529
- pathname = decodeURIComponent(rawPath);
530
- } catch {
531
- throw new HttpError(`${label} is not valid URL encoding`, 400);
532
- }
533
- pathname = pathname.replaceAll("\\", "/");
534
- if (pathname.split("/").includes("..")) throw new HttpError(`${label} must not contain parent directory segments`, 400);
535
- return resolveInside(parentDir, `.${pathname}`, label);
536
- }
537
- function collectRequestBody(req, limit = DEFAULT_API_BODY_LIMIT_BYTES) {
538
- return new Promise((resolve, reject) => {
539
- let body = "";
540
- let size = 0;
541
- let completed = false;
542
- req.on("data", (chunk) => {
543
- if (completed) return;
544
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
545
- size += buffer.byteLength;
546
- if (size > limit) {
547
- completed = true;
548
- reject(new HttpError(`Request body exceeds ${limit} bytes`, 413));
549
- return;
550
- }
551
- body += buffer.toString("utf-8");
552
- });
553
- req.on("end", () => {
554
- if (!completed) {
555
- completed = true;
556
- resolve(body);
557
- }
558
- });
559
- req.on("error", (error) => {
560
- if (!completed) {
561
- completed = true;
562
- reject(error);
563
- }
564
- });
565
- });
566
- }
567
- function validateDevApiRequest(req, sessionToken) {
568
- const originError = validateOrigin(req);
569
- if (originError) return originError;
570
- if (!isUnsafeMethod(req.method)) return null;
571
- if (!hasValidSessionToken(req, sessionToken)) return new HttpError("Invalid Musea dev session token", 403);
572
- if (!isJsonRequest(req)) return new HttpError("Content-Type must be application/json", 415);
573
- return null;
574
- }
575
- function serializeScriptValue(value) {
576
- return (JSON.stringify(value) ?? "undefined").replace(/[<>&\u2028\u2029]/g, (char) => {
577
- switch (char) {
578
- case "<": return "\\u003C";
579
- case ">": return "\\u003E";
580
- case "&": return "\\u0026";
581
- case "\u2028": return "\\u2028";
582
- case "\u2029": return "\\u2029";
583
- default: return char;
584
- }
585
- });
586
- }
587
- function isUnsafeMethod(method) {
588
- return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
589
- }
590
- function isJsonRequest(req) {
591
- return getHeader(req, "content-type")?.split(";")[0]?.trim().toLowerCase() === "application/json";
592
- }
593
- function validateOrigin(req) {
594
- if (getHeader(req, "sec-fetch-site") === "cross-site") return new HttpError("Cross-origin Musea API requests are not allowed", 403);
595
- const origin = getHeader(req, "origin");
596
- if (!origin) return null;
597
- const host = getHeader(req, "host");
598
- if (!host) return new HttpError("Missing Host header", 400);
599
- try {
600
- if (new URL(origin).host !== host) return new HttpError("Cross-origin Musea API requests are not allowed", 403);
601
- } catch {
602
- return new HttpError("Invalid Origin header", 400);
603
- }
604
- return null;
605
- }
606
- function hasValidSessionToken(req, expectedToken) {
607
- const actualToken = getHeader(req, "x-musea-session");
608
- if (!actualToken) return false;
609
- const actual = Buffer.from(actualToken);
610
- const expected = Buffer.from(expectedToken);
611
- return actual.length === expected.length && timingSafeEqual(actual, expected);
612
- }
613
- function getHeader(req, name) {
614
- const value = req.headers[name];
615
- if (Array.isArray(value)) return value[0];
616
- return value;
617
- }
618
- //#endregion
619
646
  //#region src/gallery/template.ts
620
647
  /**
621
648
  * HTML structure and inline JS generation for the Musea gallery.
@@ -722,17 +749,18 @@ function generateGalleryScript(basePath) {
722
749
 
723
750
  let html = '';
724
751
  for (const [category, items] of Object.entries(categories)) {
752
+ const escapedCategory = escapeHtml(category);
725
753
  html += '<div class="sidebar-section">';
726
- html += '<div class="category-header" data-category="' + category + '">';
754
+ html += '<div class="category-header" data-category="' + escapedCategory + '">';
727
755
  html += '<svg class="category-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/></svg>';
728
- html += '<span>' + category + '</span>';
756
+ html += '<span>' + escapedCategory + '</span>';
729
757
  html += '<span class="category-count">' + items.length + '</span>';
730
758
  html += '</div>';
731
- html += '<ul class="art-list" data-category="' + category + '">';
759
+ html += '<ul class="art-list" data-category="' + escapedCategory + '">';
732
760
  for (const art of items) {
733
761
  const active = selectedArt?.path === art.path ? 'active' : '';
734
762
  const variantCount = art.variants?.length || 0;
735
- html += '<li class="art-item ' + active + '" data-path="' + art.path + '">';
763
+ html += '<li class="art-item ' + active + '" data-path="' + escapeHtml(art.path) + '">';
736
764
  html += '<span>' + escapeHtml(art.metadata.title) + '</span>';
737
765
  html += '<span class="art-variant-count">' + variantCount + ' variant' + (variantCount !== 1 ? 's' : '') + '</span>';
738
766
  html += '</li>';
@@ -755,7 +783,7 @@ function generateGalleryScript(basePath) {
755
783
  sidebar.querySelectorAll('.category-header').forEach(header => {
756
784
  header.addEventListener('click', () => {
757
785
  header.classList.toggle('collapsed');
758
- const list = sidebar.querySelector('.art-list[data-category="' + header.dataset.category + '"]');
786
+ const list = header.parentElement?.querySelector('.art-list');
759
787
  if (list) list.style.display = header.classList.contains('collapsed') ? 'none' : 'block';
760
788
  });
761
789
  });
@@ -1311,16 +1339,19 @@ function generatePreviewHtml(art, variant, _basePath, viteBase) {
1311
1339
  //#region src/preview/index.ts
1312
1340
  function generatePreviewModule(art, variantComponentName, variantName, cssImports = [], previewSetup = null) {
1313
1341
  const artModuleId = `virtual:musea-art:${art.path}`;
1314
- const escapedVariantName = escapeTemplate(variantName);
1315
- const cssImportStatements = cssImports.map((cssPath) => `import '${cssPath}';`).join("\n");
1316
- const setupImport = previewSetup ? `import __museaPreviewSetup from '${previewSetup}';` : "";
1342
+ const artModuleIdLiteral = JSON.stringify(artModuleId);
1343
+ const variantNameLiteral = JSON.stringify(variantName);
1344
+ const variantComponentNameLiteral = JSON.stringify(variantComponentName);
1345
+ const cssImportStatements = cssImports.map((cssPath) => `import ${JSON.stringify(cssPath)};`).join("\n");
1346
+ const setupImport = previewSetup ? `import __museaPreviewSetup from ${JSON.stringify(previewSetup)};` : "";
1317
1347
  const setupCall = previewSetup ? "await __museaPreviewSetup(app);" : "";
1318
1348
  const actionEvents = JSON.stringify(art.metadata.actionEvents ?? []);
1349
+ const artStyleId = `musea-art-styles-${art.path.replace(/[^\w-]+/g, "_")}`;
1319
1350
  return `
1320
1351
  ${cssImportStatements}
1321
1352
  ${setupImport}
1322
1353
  import { createApp, reactive, h } from 'vue';
1323
- import * as artModule from '${artModuleId}';
1354
+ import * as artModule from ${artModuleIdLiteral};
1324
1355
 
1325
1356
  const container = document.getElementById('app');
1326
1357
 
@@ -1331,7 +1362,7 @@ const propsOverride = reactive({});
1331
1362
  const slotsOverride = reactive({ default: '' });
1332
1363
 
1333
1364
  function ensureArtStyles(styles) {
1334
- const styleId = '${`musea-art-styles-${art.path.replace(/[^\w-]+/g, "_")}`}';
1365
+ const styleId = ${JSON.stringify(artStyleId)};
1335
1366
  const existing = document.getElementById(styleId);
1336
1367
 
1337
1368
  if (!Array.isArray(styles) || styles.length === 0) {
@@ -1390,11 +1421,11 @@ window.__museaSetSlots = (slots) => {
1390
1421
  async function mount() {
1391
1422
  try {
1392
1423
  // Get the specific variant component
1393
- const VariantComponent = artModule['${variantComponentName}'];
1424
+ const VariantComponent = artModule[${variantComponentNameLiteral}];
1394
1425
  const RawComponent = artModule.__component__;
1395
1426
 
1396
1427
  if (!VariantComponent) {
1397
- throw new Error('Variant component "${variantComponentName}" not found in art module');
1428
+ throw new Error('Variant component ' + ${variantComponentNameLiteral} + ' not found in art module');
1398
1429
  }
1399
1430
 
1400
1431
  // Create and mount the app
@@ -1406,8 +1437,8 @@ async function mount() {
1406
1437
  app.mount(container);
1407
1438
  currentApp = app;
1408
1439
 
1409
- console.log('[musea-preview] Mounted variant: ${escapedVariantName}');
1410
- __museaInitAddons(container, '${escapedVariantName}', ${actionEvents});
1440
+ console.log('[musea-preview] Mounted variant:', ${variantNameLiteral});
1441
+ __museaInitAddons(container, ${variantNameLiteral}, ${actionEvents});
1411
1442
 
1412
1443
  // Override set-props to remount with raw component + props
1413
1444
  const TargetComponent = RawComponent || VariantComponent;
@@ -1458,17 +1489,20 @@ mount();
1458
1489
  }
1459
1490
  function generatePreviewModuleWithProps(art, variantComponentName, variantName, propsOverride, cssImports = [], previewSetup = null) {
1460
1491
  const artModuleId = `virtual:musea-art:${art.path}`;
1461
- const escapedVariantName = escapeTemplate(variantName);
1492
+ const artModuleIdLiteral = JSON.stringify(artModuleId);
1493
+ const variantNameLiteral = JSON.stringify(variantName);
1494
+ const variantComponentNameLiteral = JSON.stringify(variantComponentName);
1462
1495
  const propsJson = JSON.stringify(propsOverride);
1463
- const cssImportStatements = cssImports.map((cssPath) => `import '${cssPath}';`).join("\n");
1464
- const setupImport = previewSetup ? `import __museaPreviewSetup from '${previewSetup}';` : "";
1496
+ const cssImportStatements = cssImports.map((cssPath) => `import ${JSON.stringify(cssPath)};`).join("\n");
1497
+ const setupImport = previewSetup ? `import __museaPreviewSetup from ${JSON.stringify(previewSetup)};` : "";
1465
1498
  const setupCall = previewSetup ? "await __museaPreviewSetup(app);" : "";
1466
1499
  const actionEvents = JSON.stringify(art.metadata.actionEvents ?? []);
1500
+ const artStyleId = `musea-art-styles-${art.path.replace(/[^\w-]+/g, "_")}`;
1467
1501
  return `
1468
1502
  ${cssImportStatements}
1469
1503
  ${setupImport}
1470
1504
  import { createApp, h } from 'vue';
1471
- import * as artModule from '${artModuleId}';
1505
+ import * as artModule from ${artModuleIdLiteral};
1472
1506
 
1473
1507
  const container = document.getElementById('app');
1474
1508
  const propsOverride = ${propsJson};
@@ -1476,7 +1510,7 @@ const propsOverride = ${propsJson};
1476
1510
  ${MUSEA_ADDONS_INIT_CODE}
1477
1511
 
1478
1512
  function ensureArtStyles(styles) {
1479
- const styleId = '${`musea-art-styles-${art.path.replace(/[^\w-]+/g, "_")}`}';
1513
+ const styleId = ${JSON.stringify(artStyleId)};
1480
1514
  const existing = document.getElementById(styleId);
1481
1515
 
1482
1516
  if (!Array.isArray(styles) || styles.length === 0) {
@@ -1512,9 +1546,9 @@ function renderError(title, error) {
1512
1546
 
1513
1547
  async function mount() {
1514
1548
  try {
1515
- const VariantComponent = artModule['${variantComponentName}'];
1549
+ const VariantComponent = artModule[${variantComponentNameLiteral}];
1516
1550
  if (!VariantComponent) {
1517
- throw new Error('Variant component "${variantComponentName}" not found');
1551
+ throw new Error('Variant component ' + ${variantComponentNameLiteral} + ' not found');
1518
1552
  }
1519
1553
 
1520
1554
  const WrappedComponent = {
@@ -1529,8 +1563,8 @@ async function mount() {
1529
1563
  container.innerHTML = '';
1530
1564
  container.className = 'musea-variant';
1531
1565
  app.mount(container);
1532
- console.log('[musea-preview] Mounted variant: ${escapedVariantName} with props override');
1533
- __museaInitAddons(container, '${escapedVariantName}', ${actionEvents});
1566
+ console.log('[musea-preview] Mounted variant with props override:', ${variantNameLiteral});
1567
+ __museaInitAddons(container, ${variantNameLiteral}, ${actionEvents});
1534
1568
  } catch (error) {
1535
1569
  console.error('[musea-preview] Failed to mount:', error);
1536
1570
  renderError('Failed to render', error);
@@ -1732,13 +1766,19 @@ function registerMiddleware(devServer, ctx) {
1732
1766
  res.setHeader("Cache-Control", "no-cache");
1733
1767
  res.end(result.code);
1734
1768
  } else {
1735
- const moduleCode = generateArtModule(art, artPath);
1769
+ const moduleCode = generateArtModule(art, artPath, {
1770
+ root: devServer.config.root,
1771
+ scanRoots: ctx.scanRoots
1772
+ });
1736
1773
  res.setHeader("Content-Type", "application/javascript");
1737
1774
  res.end(moduleCode);
1738
1775
  }
1739
1776
  } catch (err) {
1740
1777
  console.error("[musea] Failed to transform art module:", err);
1741
- const moduleCode = generateArtModule(art, artPath);
1778
+ const moduleCode = generateArtModule(art, artPath, {
1779
+ root: devServer.config.root,
1780
+ scanRoots: ctx.scanRoots
1781
+ });
1742
1782
  res.setHeader("Content-Type", "application/javascript");
1743
1783
  res.end(moduleCode);
1744
1784
  }
@@ -2524,7 +2564,11 @@ async function handleArtPalette(ctx, match, sendJson, sendError) {
2524
2564
  typescript: ""
2525
2565
  };
2526
2566
  if (palette.controls.length === 0 && art.metadata.component) {
2527
- const resolvedComponentPath = path.isAbsolute(art.metadata.component) ? art.metadata.component : path.resolve(path.dirname(artPath), art.metadata.component);
2567
+ const resolvedComponentPath = resolveComponentSourcePath(art, artPath, allowedSourceRoots(ctx.config.root, ctx.scanRoots));
2568
+ if (!resolvedComponentPath) {
2569
+ sendJson(palette);
2570
+ return;
2571
+ }
2528
2572
  try {
2529
2573
  const componentSource = await fs.promises.readFile(resolvedComponentPath, "utf-8");
2530
2574
  const analysis = binding.analyzeSfc ? binding.analyzeSfc(componentSource, { filename: resolvedComponentPath }) : analyzeSfcFallback(componentSource, { filename: resolvedComponentPath });
@@ -2602,7 +2646,7 @@ async function handleArtAnalysis(ctx, match, sendJson, sendError) {
2602
2646
  return;
2603
2647
  }
2604
2648
  try {
2605
- 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;
2649
+ const resolvedComponentPath = resolveComponentSourcePath(art, artPath, allowedSourceRoots(ctx.config.root, ctx.scanRoots));
2606
2650
  if (resolvedComponentPath) {
2607
2651
  const source = await fs.promises.readFile(resolvedComponentPath, "utf-8");
2608
2652
  const binding = loadNative();
@@ -2977,12 +3021,18 @@ function createLoad(state) {
2977
3021
  if (id.startsWith("\0musea-art:")) {
2978
3022
  const artPath = id.slice(11).replace(/\?musea-virtual$/, "");
2979
3023
  const art = state.artFiles.get(artPath);
2980
- if (art) return generateArtModule(art, artPath);
3024
+ if (art) return generateArtModule(art, artPath, {
3025
+ root: state.getConfigRoot(),
3026
+ scanRoots: state.getScanRoots()
3027
+ });
2981
3028
  }
2982
3029
  if (id.startsWith("\0musea:")) {
2983
3030
  const realPath = id.slice(7).replace(/\?musea-virtual$/, "");
2984
3031
  const art = state.artFiles.get(realPath);
2985
- if (art) return generateArtModule(art, realPath);
3032
+ if (art) return generateArtModule(art, realPath, {
3033
+ root: state.getConfigRoot(),
3034
+ scanRoots: state.getScanRoots()
3035
+ });
2986
3036
  }
2987
3037
  return null;
2988
3038
  };
@@ -3071,6 +3121,7 @@ function musea(options = {}) {
3071
3121
  resolvedPreviewCss,
3072
3122
  resolvedPreviewSetup,
3073
3123
  getConfigRoot: () => config.root,
3124
+ getScanRoots: () => scanRoots,
3074
3125
  getServer: () => server,
3075
3126
  processArtFile
3076
3127
  };
@@ -3106,12 +3157,14 @@ function musea(options = {}) {
3106
3157
  devSessionToken,
3107
3158
  themeConfig,
3108
3159
  artFiles,
3160
+ scanRoots,
3109
3161
  resolvedPreviewCss,
3110
3162
  resolvedPreviewSetup
3111
3163
  });
3112
3164
  devServer.middlewares.use(`${basePath}/api`, createApiMiddleware({
3113
3165
  config,
3114
3166
  artFiles,
3167
+ scanRoots,
3115
3168
  tokensPath,
3116
3169
  basePath,
3117
3170
  resolvedPreviewCss,