@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.
- package/dist/gallery/assets/{MonacoEditor-Dmje1nZi.js → MonacoEditor-VccI_xm1.js} +2 -2
- package/dist/gallery/assets/{cssMode-CHkzGWlG.js → cssMode-DYl_gbi2.js} +1 -1
- package/dist/gallery/assets/{editor.api2-DSQ9Vpiw.js → editor.api2-C1iuJsz5.js} +1 -1
- package/dist/gallery/assets/{editor.main-Dr80cIvC.js → editor.main-CB_LUZEd.js} +2 -2
- package/dist/gallery/assets/{freemarker2-C590qJFy.js → freemarker2-CwuG5RrE.js} +1 -1
- package/dist/gallery/assets/{handlebars-DMRDuFi1.js → handlebars-DQnqIqtZ.js} +1 -1
- package/dist/gallery/assets/{html-TtN3jv37.js → html-Cj3GxbMF.js} +1 -1
- package/dist/gallery/assets/{htmlMode-By_JGvvp.js → htmlMode-ckJrF6jc.js} +1 -1
- package/dist/gallery/assets/{index-jXDqIaD8.js → index-6QLn8Kxp.js} +3 -3
- package/dist/gallery/assets/{index-CwT3Ex21.css → index-P1L8IaBA.css} +1 -1
- package/dist/gallery/assets/{javascript-y17h-ata.js → javascript-DNOPgN7f.js} +1 -1
- package/dist/gallery/assets/{jsonMode-n1uMwhuY.js → jsonMode-BMPHk4yv.js} +1 -1
- package/dist/gallery/assets/{liquid-D-a8XQan.js → liquid-cUC3T7D4.js} +1 -1
- package/dist/gallery/assets/{lspLanguageFeatures-6bRTxJu4.js → lspLanguageFeatures-DnNJLDpx.js} +1 -1
- package/dist/gallery/assets/{mdx-CC7DfHFT.js → mdx-DDDAX75G.js} +1 -1
- package/dist/gallery/assets/{monaco.contribution-CfGRLrR9.js → monaco.contribution-BsZfIZIS.js} +2 -2
- package/dist/gallery/assets/{python-BUAcshPr.js → python-CkEL94qN.js} +1 -1
- package/dist/gallery/assets/{razor-C5P2I6v1.js → razor-DpH3myLF.js} +1 -1
- package/dist/gallery/assets/{tsMode-B927vf_Z.js → tsMode-D4vJtygH.js} +1 -1
- package/dist/gallery/assets/{typescript-DTfzyqGo.js → typescript-f3jEZleA.js} +1 -1
- package/dist/gallery/assets/{workers-DiXcHCU4.js → workers-7fp33vpu.js} +1 -1
- package/dist/gallery/assets/{xml-CnjQeZMF.js → xml-CrndFNV8.js} +1 -1
- package/dist/gallery/assets/{yaml-KgJNvybZ.js → yaml-BAl6eAY9.js} +1 -1
- package/dist/gallery/index.html +2 -2
- package/dist/index.mjs +227 -174
- package/dist/index.mjs.map +1 -1
- 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
550
|
+
let componentTagName;
|
|
551
|
+
const componentBindingName = "__MuseaComponent";
|
|
401
552
|
if (art.isInline && art.componentPath) {
|
|
402
|
-
componentImportPath = art.componentPath;
|
|
403
|
-
|
|
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
|
-
|
|
406
|
-
|
|
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 &&
|
|
422
|
-
if (!scriptSetup?.imports.some((imp) => {
|
|
423
|
-
|
|
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 (
|
|
437
|
-
const escapedTemplate = template
|
|
438
|
-
const fullTemplate = `<div data-variant="${variant.name}">${escapedTemplate}</div>`;
|
|
439
|
-
const componentNames = /* @__PURE__ */ new
|
|
440
|
-
if (
|
|
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.
|
|
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 (
|
|
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="' +
|
|
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>' +
|
|
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="' +
|
|
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 =
|
|
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
|
|
1315
|
-
const
|
|
1316
|
-
const
|
|
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
|
|
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 =
|
|
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[
|
|
1424
|
+
const VariantComponent = artModule[${variantComponentNameLiteral}];
|
|
1394
1425
|
const RawComponent = artModule.__component__;
|
|
1395
1426
|
|
|
1396
1427
|
if (!VariantComponent) {
|
|
1397
|
-
throw new Error('Variant component
|
|
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: ${
|
|
1410
|
-
__museaInitAddons(container,
|
|
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
|
|
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
|
|
1464
|
-
const setupImport = previewSetup ? `import __museaPreviewSetup from
|
|
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
|
|
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 =
|
|
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[
|
|
1549
|
+
const VariantComponent = artModule[${variantComponentNameLiteral}];
|
|
1516
1550
|
if (!VariantComponent) {
|
|
1517
|
-
throw new Error('Variant component
|
|
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
|
|
1533
|
-
__museaInitAddons(container,
|
|
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 =
|
|
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
|
|
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,
|