@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.
- 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 +243 -178
- 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
|
}
|
|
@@ -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 =
|
|
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
|
|
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,
|