@vizejs/vite-plugin-musea 0.82.0 → 0.84.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 +243 -178
  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
  }
@@ -1765,7 +1805,7 @@ async function parseTokens(tokensPath) {
1765
1805
  * Parse tokens from a directory.
1766
1806
  */
1767
1807
  async function parseTokenDirectory(dirPath) {
1768
- const mergedTokens = {};
1808
+ const mergedTokens = createRecord();
1769
1809
  await mergeTokenDirectory(mergedTokens, dirPath);
1770
1810
  return flattenTokens(mergedTokens);
1771
1811
  }
@@ -1789,7 +1829,7 @@ function deepMergeTokenTrees(target, source) {
1789
1829
  deepMergeTokenTrees(existing, value);
1790
1830
  continue;
1791
1831
  }
1792
- target[key] = value;
1832
+ target[key] = cloneTokenTree(value);
1793
1833
  }
1794
1834
  }
1795
1835
  /**
@@ -1815,10 +1855,19 @@ function flattenTokens(tokens, prefix = []) {
1815
1855
  * Extract token values from an object.
1816
1856
  */
1817
1857
  function extractTokens(obj) {
1818
- const tokens = {};
1858
+ const tokens = createRecord();
1819
1859
  for (const [key, value] of Object.entries(obj)) if (isTokenValue(value)) tokens[key] = normalizeToken(value);
1820
1860
  return tokens;
1821
1861
  }
1862
+ function cloneTokenTree(value) {
1863
+ if (Array.isArray(value)) return value.map(cloneTokenTree);
1864
+ if (isPlainObject(value)) {
1865
+ const cloned = createRecord();
1866
+ for (const [key, child] of Object.entries(value)) cloned[key] = cloneTokenTree(child);
1867
+ return cloned;
1868
+ }
1869
+ return value;
1870
+ }
1822
1871
  /**
1823
1872
  * Check if a value is a token definition.
1824
1873
  */
@@ -1830,6 +1879,9 @@ function isTokenValue(value) {
1830
1879
  function isPlainObject(value) {
1831
1880
  return typeof value === "object" && value !== null && !Array.isArray(value);
1832
1881
  }
1882
+ function createRecord() {
1883
+ return Object.create(null);
1884
+ }
1833
1885
  /**
1834
1886
  * Normalize token to DesignToken interface.
1835
1887
  */
@@ -1973,7 +2025,7 @@ function parseTokenPath(dotPath) {
1973
2025
  * Flatten nested categories into a flat map keyed by dot-path.
1974
2026
  */
1975
2027
  function buildTokenMap(categories, prefix = []) {
1976
- const map = {};
2028
+ const map = Object.create(null);
1977
2029
  for (const cat of categories) {
1978
2030
  const catKey = cat.name.toLowerCase().replace(/\s+/g, "-");
1979
2031
  const catPath = [...prefix, catKey];
@@ -2524,7 +2576,11 @@ async function handleArtPalette(ctx, match, sendJson, sendError) {
2524
2576
  typescript: ""
2525
2577
  };
2526
2578
  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);
2579
+ const resolvedComponentPath = resolveComponentSourcePath(art, artPath, allowedSourceRoots(ctx.config.root, ctx.scanRoots));
2580
+ if (!resolvedComponentPath) {
2581
+ sendJson(palette);
2582
+ return;
2583
+ }
2528
2584
  try {
2529
2585
  const componentSource = await fs.promises.readFile(resolvedComponentPath, "utf-8");
2530
2586
  const analysis = binding.analyzeSfc ? binding.analyzeSfc(componentSource, { filename: resolvedComponentPath }) : analyzeSfcFallback(componentSource, { filename: resolvedComponentPath });
@@ -2602,7 +2658,7 @@ async function handleArtAnalysis(ctx, match, sendJson, sendError) {
2602
2658
  return;
2603
2659
  }
2604
2660
  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;
2661
+ const resolvedComponentPath = resolveComponentSourcePath(art, artPath, allowedSourceRoots(ctx.config.root, ctx.scanRoots));
2606
2662
  if (resolvedComponentPath) {
2607
2663
  const source = await fs.promises.readFile(resolvedComponentPath, "utf-8");
2608
2664
  const binding = loadNative();
@@ -2977,12 +3033,18 @@ function createLoad(state) {
2977
3033
  if (id.startsWith("\0musea-art:")) {
2978
3034
  const artPath = id.slice(11).replace(/\?musea-virtual$/, "");
2979
3035
  const art = state.artFiles.get(artPath);
2980
- if (art) return generateArtModule(art, artPath);
3036
+ if (art) return generateArtModule(art, artPath, {
3037
+ root: state.getConfigRoot(),
3038
+ scanRoots: state.getScanRoots()
3039
+ });
2981
3040
  }
2982
3041
  if (id.startsWith("\0musea:")) {
2983
3042
  const realPath = id.slice(7).replace(/\?musea-virtual$/, "");
2984
3043
  const art = state.artFiles.get(realPath);
2985
- if (art) return generateArtModule(art, realPath);
3044
+ if (art) return generateArtModule(art, realPath, {
3045
+ root: state.getConfigRoot(),
3046
+ scanRoots: state.getScanRoots()
3047
+ });
2986
3048
  }
2987
3049
  return null;
2988
3050
  };
@@ -3071,6 +3133,7 @@ function musea(options = {}) {
3071
3133
  resolvedPreviewCss,
3072
3134
  resolvedPreviewSetup,
3073
3135
  getConfigRoot: () => config.root,
3136
+ getScanRoots: () => scanRoots,
3074
3137
  getServer: () => server,
3075
3138
  processArtFile
3076
3139
  };
@@ -3106,12 +3169,14 @@ function musea(options = {}) {
3106
3169
  devSessionToken,
3107
3170
  themeConfig,
3108
3171
  artFiles,
3172
+ scanRoots,
3109
3173
  resolvedPreviewCss,
3110
3174
  resolvedPreviewSetup
3111
3175
  });
3112
3176
  devServer.middlewares.use(`${basePath}/api`, createApiMiddleware({
3113
3177
  config,
3114
3178
  artFiles,
3179
+ scanRoots,
3115
3180
  tokensPath,
3116
3181
  basePath,
3117
3182
  resolvedPreviewCss,