@vizejs/vite-plugin-musea 0.101.0 → 0.104.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/README.md +4 -0
- package/dist/a11y/index.d.mts +1 -1
- package/dist/a11y/index.mjs +1 -1
- package/dist/{a11y-DNCg2qCB.mjs → a11y-62l8G1tr.mjs} +1 -1
- package/dist/{a11y-DNCg2qCB.mjs.map → a11y-62l8G1tr.mjs.map} +1 -1
- package/dist/autogen/index.mjs +1 -1
- package/dist/{autogen-3-y1d0ou.mjs → autogen-CywxMrJH.mjs} +1 -1
- package/dist/{autogen-3-y1d0ou.mjs.map → autogen-CywxMrJH.mjs.map} +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/gallery-zu8hc8Lc.mjs +483 -0
- package/dist/gallery-zu8hc8Lc.mjs.map +1 -0
- package/dist/{index-CoAc76Ob.d.mts → index-BMCTR1nC.d.mts} +2 -2
- package/dist/{index-CoAc76Ob.d.mts.map → index-BMCTR1nC.d.mts.map} +1 -1
- package/dist/index.d.mts +6 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +90 -710
- package/dist/index.mjs.map +1 -1
- package/dist/{vrt-CMJXvKjY.d.mts → vrt-CeAvfIsi.d.mts} +1 -1
- package/dist/{vrt-CMJXvKjY.d.mts.map → vrt-CeAvfIsi.d.mts.map} +1 -1
- package/dist/{vrt-CjFf5GR0.mjs → vrt-Cv1PK1EF.mjs} +1 -1
- package/dist/{vrt-CjFf5GR0.mjs.map → vrt-Cv1PK1EF.mjs.map} +1 -1
- package/dist/vrt.d.mts +1 -1
- package/dist/vrt.mjs +1 -1
- package/package.json +12 -4
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { i as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { a as collectRequestBody, c as resolveInside, d as serializeScriptValue, f as validateDevApiRequest, i as HttpError, l as resolveInsideAny, n as generateGalleryModule, o as createDevSessionToken, s as decodeUrlComponent, u as resolveUrlPathInside } from "./gallery-zu8hc8Lc.mjs";
|
|
2
|
+
import { i as MuseaVrtRunner, n as generateVrtJsonReport, r as generateVrtReport } from "./vrt-Cv1PK1EF.mjs";
|
|
3
|
+
import { t as MuseaA11yRunner } from "./a11y-62l8G1tr.mjs";
|
|
4
|
+
import { n as writeArtFile, t as generateArtFile } from "./autogen-CywxMrJH.mjs";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import { transformWithEsbuild } from "vite";
|
|
6
7
|
import fs from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { vizeConfigStore } from "@vizejs/vite-plugin";
|
|
9
|
-
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
//#region src/native-loader.ts
|
|
12
12
|
/**
|
|
@@ -91,147 +91,6 @@ 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
94
|
//#region src/component-source.ts
|
|
236
95
|
function allowedSourceRoots(root, scanRoots = []) {
|
|
237
96
|
return [...new Set([root, ...scanRoots].map((sourceRoot) => path.resolve(sourceRoot)))];
|
|
@@ -620,324 +479,6 @@ export default ${toPascalCase(defaultVariant.name)};
|
|
|
620
479
|
return code;
|
|
621
480
|
}
|
|
622
481
|
//#endregion
|
|
623
|
-
//#region src/gallery/styles-base.css?inline
|
|
624
|
-
var styles_base_default = ":root {\n --musea-bg-primary: #e6e2d6;\n --musea-bg-secondary: #ddd9cd;\n --musea-bg-tertiary: #d4d0c4;\n --musea-bg-elevated: #e6e2d6;\n --musea-accent: #121212;\n --musea-accent-hover: #2a2a2a;\n --musea-accent-subtle: #12121214;\n --musea-text: #121212;\n --musea-text-secondary: #3a3a3a;\n --musea-text-muted: #6b6b6b;\n --musea-border: #c8c4b8;\n --musea-border-subtle: #d4d0c4;\n --musea-success: #16a34a;\n --musea-shadow: 0 4px 24px #00000014;\n --musea-radius-sm: 4px;\n --musea-radius-md: 6px;\n --musea-radius-lg: 8px;\n --musea-transition: .15s ease;\n}\n\n* {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\nbody {\n background: var(--musea-bg-primary);\n color: var(--musea-text);\n -webkit-font-smoothing: antialiased;\n min-height: 100vh;\n font-family: Helvetica Neue, Helvetica, Arial, sans-serif;\n line-height: 1.5;\n}\n";
|
|
625
|
-
//#endregion
|
|
626
|
-
//#region src/gallery/styles-layout.css?inline
|
|
627
|
-
var styles_layout_default = ".header {\n background: var(--musea-bg-secondary);\n border-bottom: 1px solid var(--musea-border);\n z-index: 100;\n justify-content: space-between;\n align-items: center;\n height: 56px;\n padding: 0 1.5rem;\n display: flex;\n position: sticky;\n top: 0;\n}\n\n.header-left {\n align-items: center;\n gap: 1.5rem;\n display: flex;\n}\n\n.logo {\n color: var(--musea-accent);\n align-items: center;\n gap: .5rem;\n font-size: 1.125rem;\n font-weight: 700;\n text-decoration: none;\n display: flex;\n}\n\n.logo-svg {\n flex-shrink: 0;\n width: 32px;\n height: 32px;\n}\n\n.logo-icon svg {\n width: 16px;\n height: 16px;\n color: var(--musea-text);\n}\n\n.header-subtitle {\n color: var(--musea-text-muted);\n border-left: 1px solid var(--musea-border);\n padding-left: 1.5rem;\n font-size: .8125rem;\n font-weight: 500;\n}\n\n.search-container {\n width: 280px;\n position: relative;\n}\n\n.search-input {\n background: var(--musea-bg-tertiary);\n border: 1px solid var(--musea-border);\n border-radius: var(--musea-radius-md);\n width: 100%;\n color: var(--musea-text);\n transition: border-color var(--musea-transition),\n background var(--musea-transition);\n outline: none;\n padding: .5rem .75rem .5rem 2.25rem;\n font-size: .8125rem;\n}\n\n.search-input::placeholder {\n color: var(--musea-text-muted);\n}\n\n.search-input:focus {\n border-color: var(--musea-accent);\n background: var(--musea-bg-elevated);\n}\n\n.search-icon {\n color: var(--musea-text-muted);\n pointer-events: none;\n position: absolute;\n top: 50%;\n left: .75rem;\n transform: translateY(-50%);\n}\n\n.main {\n grid-template-columns: 260px 1fr;\n min-height: calc(100vh - 56px);\n display: grid;\n}\n\n.sidebar {\n background: var(--musea-bg-secondary);\n border-right: 1px solid var(--musea-border);\n overflow: hidden auto;\n}\n\n.sidebar::-webkit-scrollbar {\n width: 6px;\n}\n\n.sidebar::-webkit-scrollbar-track {\n background: none;\n}\n\n.sidebar::-webkit-scrollbar-thumb {\n background: var(--musea-border);\n border-radius: 3px;\n}\n\n.sidebar-section {\n padding: .75rem;\n}\n\n.category-header {\n text-transform: uppercase;\n letter-spacing: .08em;\n color: var(--musea-text-muted);\n cursor: pointer;\n user-select: none;\n border-radius: var(--musea-radius-sm);\n transition: background var(--musea-transition);\n align-items: center;\n gap: .5rem;\n padding: .625rem .75rem;\n font-size: .6875rem;\n font-weight: 600;\n display: flex;\n}\n\n.category-header:hover {\n background: var(--musea-bg-tertiary);\n}\n\n.category-icon {\n width: 16px;\n height: 16px;\n transition: transform var(--musea-transition);\n}\n\n.category-header.collapsed .category-icon {\n transform: rotate(-90deg);\n}\n\n.category-count {\n background: var(--musea-bg-tertiary);\n border-radius: 4px;\n margin-left: auto;\n padding: .125rem .375rem;\n font-size: .625rem;\n}\n\n.art-list {\n margin-top: .25rem;\n list-style: none;\n}\n\n.art-item {\n border-radius: var(--musea-radius-sm);\n cursor: pointer;\n color: var(--musea-text-secondary);\n transition: all var(--musea-transition);\n align-items: center;\n gap: .625rem;\n padding: .5rem .75rem .5rem 1.75rem;\n font-size: .8125rem;\n display: flex;\n position: relative;\n}\n\n.art-item:before {\n content: \"\";\n background: var(--musea-border);\n width: 6px;\n height: 6px;\n transition: background var(--musea-transition);\n border-radius: 50%;\n position: absolute;\n top: 50%;\n left: .75rem;\n transform: translateY(-50%);\n}\n\n.art-item:hover {\n background: var(--musea-bg-tertiary);\n color: var(--musea-text);\n}\n\n.art-item:hover:before {\n background: var(--musea-text-muted);\n}\n\n.art-item.active {\n background: var(--musea-accent-subtle);\n color: var(--musea-accent-hover);\n}\n\n.art-item.active:before {\n background: var(--musea-accent);\n}\n\n.art-variant-count {\n color: var(--musea-text-muted);\n opacity: 0;\n transition: opacity var(--musea-transition);\n margin-left: auto;\n font-size: .6875rem;\n}\n\n.art-item:hover .art-variant-count {\n opacity: 1;\n}\n\n.content {\n background: var(--musea-bg-primary);\n overflow-y: auto;\n}\n\n.content-inner {\n max-width: 1400px;\n margin: 0 auto;\n padding: 2rem;\n}\n\n.content-header {\n margin-bottom: 2rem;\n}\n\n.content-title {\n margin-bottom: .5rem;\n font-size: 1.5rem;\n font-weight: 700;\n}\n\n.content-description {\n color: var(--musea-text-muted);\n max-width: 600px;\n font-size: .9375rem;\n}\n\n.content-meta {\n align-items: center;\n gap: 1rem;\n margin-top: 1rem;\n display: flex;\n}\n\n.meta-tag {\n background: var(--musea-bg-secondary);\n border: 1px solid var(--musea-border);\n border-radius: var(--musea-radius-sm);\n color: var(--musea-text-muted);\n align-items: center;\n gap: .375rem;\n padding: .25rem .625rem;\n font-size: .75rem;\n display: inline-flex;\n}\n\n.meta-tag svg {\n width: 12px;\n height: 12px;\n}\n";
|
|
628
|
-
//#endregion
|
|
629
|
-
//#region src/gallery/styles-components.css?inline
|
|
630
|
-
var styles_components_default = ".gallery {\n grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n gap: 1.25rem;\n display: grid;\n}\n\n.variant-card {\n background: var(--musea-bg-secondary);\n border: 1px solid var(--musea-border);\n border-radius: var(--musea-radius-lg);\n overflow: hidden;\n}\n\n.variant-preview {\n aspect-ratio: 16 / 7;\n background: var(--musea-bg-tertiary);\n box-sizing: border-box;\n justify-content: center;\n align-items: center;\n display: flex;\n position: relative;\n overflow: hidden;\n}\n\n.variant-preview iframe {\n border-radius: var(--musea-radius-md);\n background: #fff;\n border: none;\n width: 70%;\n max-width: 100%;\n height: 100%;\n max-height: 100%;\n box-shadow: 0 12px 30px #0d0d0d1f;\n}\n\n.variant-preview-placeholder {\n color: var(--musea-text-muted);\n text-align: center;\n padding: 1rem;\n font-size: .8125rem;\n}\n\n.variant-preview-code {\n color: var(--musea-text-muted);\n background: var(--musea-bg-primary);\n width: 100%;\n max-height: 100%;\n padding: 1rem;\n font-family: JetBrains Mono, SF Mono, Fira Code, monospace;\n font-size: .75rem;\n overflow: auto;\n}\n\n.variant-info {\n border-top: 1px solid var(--musea-border);\n justify-content: space-between;\n align-items: center;\n padding: 1rem;\n display: flex;\n}\n\n.variant-name {\n font-size: .875rem;\n font-weight: 600;\n}\n\n.variant-badge {\n text-transform: uppercase;\n letter-spacing: .04em;\n background: var(--musea-accent-subtle);\n color: var(--musea-accent);\n border-radius: 4px;\n padding: .1875rem .5rem;\n font-size: .625rem;\n font-weight: 600;\n}\n\n.variant-actions {\n gap: .5rem;\n display: flex;\n}\n\n.variant-action-btn {\n background: var(--musea-bg-tertiary);\n border-radius: var(--musea-radius-sm);\n width: 28px;\n height: 28px;\n color: var(--musea-text-muted);\n cursor: pointer;\n transition: all var(--musea-transition);\n border: none;\n justify-content: center;\n align-items: center;\n display: flex;\n}\n\n.variant-action-btn:hover {\n background: var(--musea-bg-elevated);\n color: var(--musea-text);\n}\n\n.variant-action-btn svg {\n width: 14px;\n height: 14px;\n}\n\n.empty-state {\n text-align: center;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n min-height: 400px;\n padding: 2rem;\n display: flex;\n}\n\n.empty-state-icon {\n background: var(--musea-bg-secondary);\n border-radius: var(--musea-radius-lg);\n justify-content: center;\n align-items: center;\n width: 80px;\n height: 80px;\n margin-bottom: 1.5rem;\n display: flex;\n}\n\n.empty-state-icon svg {\n width: 40px;\n height: 40px;\n color: var(--musea-text-muted);\n}\n\n.empty-state-title {\n margin-bottom: .5rem;\n font-size: 1.125rem;\n font-weight: 600;\n}\n\n.empty-state-text {\n color: var(--musea-text-muted);\n max-width: 300px;\n font-size: .875rem;\n}\n\n.loading {\n min-height: 200px;\n color: var(--musea-text-muted);\n justify-content: center;\n align-items: center;\n gap: .75rem;\n display: flex;\n}\n\n.loading-spinner {\n border: 2px solid var(--musea-border);\n border-top-color: var(--musea-accent);\n border-radius: 50%;\n width: 20px;\n height: 20px;\n animation: .8s linear infinite spin;\n}\n\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@media (width <= 768px) {\n .main {\n grid-template-columns: 1fr;\n }\n\n .sidebar, .header-subtitle {\n display: none;\n }\n}\n";
|
|
631
|
-
//#endregion
|
|
632
|
-
//#region src/gallery/styles.ts
|
|
633
|
-
/**
|
|
634
|
-
* CSS theme variables and style definitions for the Musea gallery.
|
|
635
|
-
*
|
|
636
|
-
* CSS is split into separate .css files and imported as text
|
|
637
|
-
* via tsdown's `?inline` support.
|
|
638
|
-
*/
|
|
639
|
-
/**
|
|
640
|
-
* Generate the full gallery CSS styles string.
|
|
641
|
-
*/
|
|
642
|
-
function generateGalleryStyles() {
|
|
643
|
-
return `${styles_base_default}\n${styles_layout_default}\n${styles_components_default}`;
|
|
644
|
-
}
|
|
645
|
-
//#endregion
|
|
646
|
-
//#region src/gallery/template.ts
|
|
647
|
-
/**
|
|
648
|
-
* HTML structure and inline JS generation for the Musea gallery.
|
|
649
|
-
*
|
|
650
|
-
* Extracted from gallery.ts to keep file sizes manageable.
|
|
651
|
-
*/
|
|
652
|
-
function escapeHtmlAttribute(value) {
|
|
653
|
-
return value.replace(/[&<>"']/g, (char) => {
|
|
654
|
-
switch (char) {
|
|
655
|
-
case "&": return "&";
|
|
656
|
-
case "<": return "<";
|
|
657
|
-
case ">": return ">";
|
|
658
|
-
case "\"": return """;
|
|
659
|
-
case "'": return "'";
|
|
660
|
-
default: return char;
|
|
661
|
-
}
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
/**
|
|
665
|
-
* Generate the gallery HTML body (header, sidebar, content, and inline script).
|
|
666
|
-
*/
|
|
667
|
-
function generateGalleryBody(basePath) {
|
|
668
|
-
return `
|
|
669
|
-
<header class="header">
|
|
670
|
-
<div class="header-left">
|
|
671
|
-
<a href="${escapeHtmlAttribute(basePath)}" class="logo">
|
|
672
|
-
<svg class="logo-svg" width="32" height="32" viewBox="0 0 200 200" fill="none">
|
|
673
|
-
<g transform="translate(30, 25) scale(1.2)">
|
|
674
|
-
<g transform="translate(15, 10) skewX(-15)">
|
|
675
|
-
<path d="M 65 0 L 40 60 L 70 20 L 65 0 Z" fill="currentColor"/>
|
|
676
|
-
<path d="M 20 0 L 40 60 L 53 13 L 20 0 Z" fill="currentColor"/>
|
|
677
|
-
</g>
|
|
678
|
-
</g>
|
|
679
|
-
<g transform="translate(110, 120)">
|
|
680
|
-
<line x1="5" y1="10" x2="5" y2="50" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
|
|
681
|
-
<line x1="60" y1="10" x2="60" y2="50" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
|
|
682
|
-
<path d="M 0 10 L 32.5 0 L 65 10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
683
|
-
<rect x="15" y="18" width="14" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.7"/>
|
|
684
|
-
<rect x="36" y="18" width="14" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.7"/>
|
|
685
|
-
<rect x="23" y="35" width="18" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.6"/>
|
|
686
|
-
</g>
|
|
687
|
-
</svg>
|
|
688
|
-
Musea
|
|
689
|
-
</a>
|
|
690
|
-
<span class="header-subtitle">Component Gallery</span>
|
|
691
|
-
</div>
|
|
692
|
-
<div class="search-container">
|
|
693
|
-
<svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
694
|
-
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
|
695
|
-
</svg>
|
|
696
|
-
<input type="text" class="search-input" placeholder="Search components..." id="search">
|
|
697
|
-
</div>
|
|
698
|
-
</header>
|
|
699
|
-
|
|
700
|
-
<main class="main">
|
|
701
|
-
<aside class="sidebar" id="sidebar">
|
|
702
|
-
<div class="loading">
|
|
703
|
-
<div class="loading-spinner"></div>
|
|
704
|
-
Loading...
|
|
705
|
-
</div>
|
|
706
|
-
</aside>
|
|
707
|
-
<section class="content" id="content">
|
|
708
|
-
<div class="empty-state">
|
|
709
|
-
<div class="empty-state-icon">
|
|
710
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
711
|
-
<path d="M4 5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5Z"/>
|
|
712
|
-
<path d="M4 13a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6Z"/>
|
|
713
|
-
<path d="M16 13a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-6Z"/>
|
|
714
|
-
</svg>
|
|
715
|
-
</div>
|
|
716
|
-
<div class="empty-state-title">Select a component</div>
|
|
717
|
-
<div class="empty-state-text">Choose a component from the sidebar to view its variants and documentation</div>
|
|
718
|
-
</div>
|
|
719
|
-
</section>
|
|
720
|
-
</main>`;
|
|
721
|
-
}
|
|
722
|
-
/**
|
|
723
|
-
* Generate the gallery inline script (SPA logic).
|
|
724
|
-
*/
|
|
725
|
-
function generateGalleryScript(basePath) {
|
|
726
|
-
return `
|
|
727
|
-
const basePath = ${serializeScriptValue(basePath)};
|
|
728
|
-
let arts = [];
|
|
729
|
-
let selectedArt = null;
|
|
730
|
-
let searchQuery = '';
|
|
731
|
-
|
|
732
|
-
async function loadArts() {
|
|
733
|
-
try {
|
|
734
|
-
const res = await fetch(basePath + '/api/arts');
|
|
735
|
-
arts = await res.json();
|
|
736
|
-
renderSidebar();
|
|
737
|
-
} catch (e) {
|
|
738
|
-
console.error('Failed to load arts:', e);
|
|
739
|
-
document.getElementById('sidebar').innerHTML = '<div class="loading">Failed to load</div>';
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function renderSidebar() {
|
|
744
|
-
const sidebar = document.getElementById('sidebar');
|
|
745
|
-
const categories = {};
|
|
746
|
-
|
|
747
|
-
const filtered = searchQuery
|
|
748
|
-
? arts.filter(a => a.metadata.title.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
749
|
-
: arts;
|
|
750
|
-
|
|
751
|
-
for (const art of filtered) {
|
|
752
|
-
const cat = art.metadata.category || 'Components';
|
|
753
|
-
if (!categories[cat]) categories[cat] = [];
|
|
754
|
-
categories[cat].push(art);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
if (Object.keys(categories).length === 0) {
|
|
758
|
-
sidebar.innerHTML = '<div class="sidebar-section"><div class="loading">No components found</div></div>';
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
let html = '';
|
|
763
|
-
for (const [category, items] of Object.entries(categories)) {
|
|
764
|
-
const escapedCategory = escapeHtml(category);
|
|
765
|
-
html += '<div class="sidebar-section">';
|
|
766
|
-
html += '<div class="category-header" data-category="' + escapedCategory + '">';
|
|
767
|
-
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>';
|
|
768
|
-
html += '<span>' + escapedCategory + '</span>';
|
|
769
|
-
html += '<span class="category-count">' + items.length + '</span>';
|
|
770
|
-
html += '</div>';
|
|
771
|
-
html += '<ul class="art-list" data-category="' + escapedCategory + '">';
|
|
772
|
-
for (const art of items) {
|
|
773
|
-
const active = selectedArt?.path === art.path ? 'active' : '';
|
|
774
|
-
const variantCount = art.variants?.length || 0;
|
|
775
|
-
html += '<li class="art-item ' + active + '" data-path="' + escapeHtml(art.path) + '">';
|
|
776
|
-
html += '<span>' + escapeHtml(art.metadata.title) + '</span>';
|
|
777
|
-
html += '<span class="art-variant-count">' + variantCount + ' variant' + (variantCount !== 1 ? 's' : '') + '</span>';
|
|
778
|
-
html += '</li>';
|
|
779
|
-
}
|
|
780
|
-
html += '</ul>';
|
|
781
|
-
html += '</div>';
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
sidebar.innerHTML = html;
|
|
785
|
-
|
|
786
|
-
sidebar.querySelectorAll('.art-item').forEach(item => {
|
|
787
|
-
item.addEventListener('click', () => {
|
|
788
|
-
const artPath = item.dataset.path;
|
|
789
|
-
selectedArt = arts.find(a => a.path === artPath);
|
|
790
|
-
renderSidebar();
|
|
791
|
-
renderContent();
|
|
792
|
-
});
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
sidebar.querySelectorAll('.category-header').forEach(header => {
|
|
796
|
-
header.addEventListener('click', () => {
|
|
797
|
-
header.classList.toggle('collapsed');
|
|
798
|
-
const list = header.parentElement?.querySelector('.art-list');
|
|
799
|
-
if (list) list.style.display = header.classList.contains('collapsed') ? 'none' : 'block';
|
|
800
|
-
});
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function renderContent() {
|
|
805
|
-
const content = document.getElementById('content');
|
|
806
|
-
if (!selectedArt) {
|
|
807
|
-
content.innerHTML = \`
|
|
808
|
-
<div class="empty-state">
|
|
809
|
-
<div class="empty-state-icon">
|
|
810
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
811
|
-
<path d="M4 5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5Z"/>
|
|
812
|
-
<path d="M4 13a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6Z"/>
|
|
813
|
-
<path d="M16 13a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-6Z"/>
|
|
814
|
-
</svg>
|
|
815
|
-
</div>
|
|
816
|
-
<div class="empty-state-title">Select a component</div>
|
|
817
|
-
<div class="empty-state-text">Choose a component from the sidebar to view its variants</div>
|
|
818
|
-
</div>
|
|
819
|
-
\`;
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const meta = selectedArt.metadata;
|
|
824
|
-
const tags = meta.tags || [];
|
|
825
|
-
const variantCount = selectedArt.variants?.length || 0;
|
|
826
|
-
|
|
827
|
-
let html = '<div class="content-inner">';
|
|
828
|
-
html += '<div class="content-header">';
|
|
829
|
-
html += '<h1 class="content-title">' + escapeHtml(meta.title) + '</h1>';
|
|
830
|
-
if (meta.description) {
|
|
831
|
-
html += '<p class="content-description">' + escapeHtml(meta.description) + '</p>';
|
|
832
|
-
}
|
|
833
|
-
html += '<div class="content-meta">';
|
|
834
|
-
html += '<span class="meta-tag"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>' + variantCount + ' variant' + (variantCount !== 1 ? 's' : '') + '</span>';
|
|
835
|
-
if (meta.category) {
|
|
836
|
-
html += '<span class="meta-tag"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>' + escapeHtml(meta.category) + '</span>';
|
|
837
|
-
}
|
|
838
|
-
for (const tag of tags) {
|
|
839
|
-
html += '<span class="meta-tag">#' + escapeHtml(tag) + '</span>';
|
|
840
|
-
}
|
|
841
|
-
html += '</div>';
|
|
842
|
-
html += '</div>';
|
|
843
|
-
|
|
844
|
-
html += '<div class="gallery">';
|
|
845
|
-
for (const variant of selectedArt.variants) {
|
|
846
|
-
const previewUrl = basePath + '/preview?art=' + encodeURIComponent(selectedArt.path) + '&variant=' + encodeURIComponent(variant.name);
|
|
847
|
-
const escapedPreviewUrl = escapeHtml(previewUrl);
|
|
848
|
-
|
|
849
|
-
html += '<div class="variant-card">';
|
|
850
|
-
html += '<div class="variant-preview">';
|
|
851
|
-
html += '<iframe src="' + escapedPreviewUrl + '" loading="lazy" title="' + escapeHtml(variant.name) + '"></iframe>';
|
|
852
|
-
html += '</div>';
|
|
853
|
-
html += '<div class="variant-info">';
|
|
854
|
-
html += '<div>';
|
|
855
|
-
html += '<span class="variant-name">' + escapeHtml(variant.name) + '</span>';
|
|
856
|
-
if (variant.isDefault) html += ' <span class="variant-badge">Default</span>';
|
|
857
|
-
html += '</div>';
|
|
858
|
-
html += '<div class="variant-actions">';
|
|
859
|
-
html += '<button class="variant-action-btn" title="Open in new tab" data-preview-url="' + escapedPreviewUrl + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></button>';
|
|
860
|
-
html += '</div>';
|
|
861
|
-
html += '</div>';
|
|
862
|
-
html += '</div>';
|
|
863
|
-
}
|
|
864
|
-
html += '</div>';
|
|
865
|
-
html += '</div>';
|
|
866
|
-
|
|
867
|
-
content.innerHTML = html;
|
|
868
|
-
|
|
869
|
-
content.querySelectorAll('.variant-action-btn[data-preview-url]').forEach(button => {
|
|
870
|
-
button.addEventListener('click', () => {
|
|
871
|
-
const previewUrl = button.dataset.previewUrl;
|
|
872
|
-
if (previewUrl) window.open(previewUrl, '_blank', 'noopener');
|
|
873
|
-
});
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
function escapeHtml(str) {
|
|
878
|
-
if (!str) return '';
|
|
879
|
-
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// Search
|
|
883
|
-
document.getElementById('search').addEventListener('input', (e) => {
|
|
884
|
-
searchQuery = e.target.value;
|
|
885
|
-
renderSidebar();
|
|
886
|
-
});
|
|
887
|
-
|
|
888
|
-
// Keyboard shortcut for search
|
|
889
|
-
document.addEventListener('keydown', (e) => {
|
|
890
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
891
|
-
e.preventDefault();
|
|
892
|
-
document.getElementById('search').focus();
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
loadArts();`;
|
|
897
|
-
}
|
|
898
|
-
//#endregion
|
|
899
|
-
//#region src/gallery/index.ts
|
|
900
|
-
/**
|
|
901
|
-
* Gallery HTML generation for the Musea component gallery.
|
|
902
|
-
*
|
|
903
|
-
* Contains the inline gallery SPA template (used as a fallback when the
|
|
904
|
-
* pre-built gallery is not available) and the gallery virtual module.
|
|
905
|
-
*/
|
|
906
|
-
/**
|
|
907
|
-
* Generate the inline gallery HTML page.
|
|
908
|
-
*/
|
|
909
|
-
function generateGalleryHtml(basePath, devSessionToken, themeConfig) {
|
|
910
|
-
const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${serializeScriptValue(themeConfig)};` : "";
|
|
911
|
-
return `<!DOCTYPE html>
|
|
912
|
-
<html lang="en">
|
|
913
|
-
<head>
|
|
914
|
-
<meta charset="UTF-8">
|
|
915
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
916
|
-
<title>Musea - Component Gallery</title>
|
|
917
|
-
<script>window.__MUSEA_BASE_PATH__=${serializeScriptValue(basePath)};window.__MUSEA_SESSION_TOKEN__=${serializeScriptValue(devSessionToken)};${themeScript}<\/script>
|
|
918
|
-
<style>${generateGalleryStyles()}
|
|
919
|
-
</style>
|
|
920
|
-
</head>
|
|
921
|
-
<body>${generateGalleryBody(basePath)}
|
|
922
|
-
|
|
923
|
-
<script type="module">${generateGalleryScript(basePath)}
|
|
924
|
-
<\/script>
|
|
925
|
-
</body>
|
|
926
|
-
</html>`;
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* Generate the virtual gallery module code.
|
|
930
|
-
*/
|
|
931
|
-
function generateGalleryModule(basePath) {
|
|
932
|
-
return `
|
|
933
|
-
export const basePath = ${serializeScriptValue(basePath)};
|
|
934
|
-
export async function loadArts() {
|
|
935
|
-
const res = await fetch(basePath + '/api/arts');
|
|
936
|
-
return res.json();
|
|
937
|
-
}
|
|
938
|
-
`;
|
|
939
|
-
}
|
|
940
|
-
//#endregion
|
|
941
482
|
//#region src/preview/addons.ts
|
|
942
483
|
/**
|
|
943
484
|
* Addon initialization code for Musea preview iframes.
|
|
@@ -1597,6 +1138,15 @@ mount();
|
|
|
1597
1138
|
//#endregion
|
|
1598
1139
|
//#region src/server-middleware.ts
|
|
1599
1140
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
1141
|
+
const galleryAssetMimeTypes = {
|
|
1142
|
+
".js": "application/javascript",
|
|
1143
|
+
".css": "text/css",
|
|
1144
|
+
".svg": "image/svg+xml",
|
|
1145
|
+
".png": "image/png",
|
|
1146
|
+
".ico": "image/x-icon",
|
|
1147
|
+
".woff2": "font/woff2",
|
|
1148
|
+
".woff": "font/woff"
|
|
1149
|
+
};
|
|
1600
1150
|
function resolveGalleryDistDir() {
|
|
1601
1151
|
return path.resolve(moduleDir, "gallery");
|
|
1602
1152
|
}
|
|
@@ -1624,6 +1174,29 @@ async function tryLoadSourceGalleryHtml(devServer, url, basePath, devSessionToke
|
|
|
1624
1174
|
html = html.replace("</head>", `<script>${generateDevGlobalsScript(basePath, devSessionToken, themeConfig)}<\/script></head>`);
|
|
1625
1175
|
return devServer.transformIndexHtml(url, html);
|
|
1626
1176
|
}
|
|
1177
|
+
async function generateFallbackGalleryHtml(basePath, devSessionToken, themeConfig) {
|
|
1178
|
+
const { generateGalleryHtml } = await import("./gallery-zu8hc8Lc.mjs").then((n) => n.t);
|
|
1179
|
+
return generateGalleryHtml(basePath, devSessionToken, themeConfig);
|
|
1180
|
+
}
|
|
1181
|
+
async function serveGalleryAsset(galleryDistDir, requestUrl, res) {
|
|
1182
|
+
try {
|
|
1183
|
+
const filePath = resolveUrlPathInside(galleryDistDir, requestUrl, "asset path");
|
|
1184
|
+
if (!(await fs.promises.stat(filePath)).isFile()) return false;
|
|
1185
|
+
const content = await fs.promises.readFile(filePath);
|
|
1186
|
+
const ext = path.extname(filePath);
|
|
1187
|
+
res.setHeader("Content-Type", galleryAssetMimeTypes[ext] || "application/octet-stream");
|
|
1188
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1189
|
+
res.end(content);
|
|
1190
|
+
return true;
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
if (error instanceof HttpError) {
|
|
1193
|
+
res.statusCode = error.status;
|
|
1194
|
+
res.end(error.message);
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1627
1200
|
/**
|
|
1628
1201
|
* Register all Musea middleware on the given dev server.
|
|
1629
1202
|
*
|
|
@@ -1656,39 +1229,14 @@ function registerMiddleware(devServer, ctx) {
|
|
|
1656
1229
|
res.end(sourceHtml);
|
|
1657
1230
|
return;
|
|
1658
1231
|
}
|
|
1659
|
-
const html =
|
|
1232
|
+
const html = await generateFallbackGalleryHtml(basePath, devSessionToken, themeConfig);
|
|
1660
1233
|
res.setHeader("Content-Type", "text/html");
|
|
1661
1234
|
res.end(html);
|
|
1662
1235
|
return;
|
|
1663
1236
|
}
|
|
1664
1237
|
}
|
|
1665
1238
|
if (url.startsWith("/assets/")) {
|
|
1666
|
-
|
|
1667
|
-
try {
|
|
1668
|
-
const filePath = resolveUrlPathInside(galleryDistDir, url, "asset path");
|
|
1669
|
-
if ((await fs.promises.stat(filePath)).isFile()) {
|
|
1670
|
-
const content = await fs.promises.readFile(filePath);
|
|
1671
|
-
const ext = path.extname(filePath);
|
|
1672
|
-
res.setHeader("Content-Type", {
|
|
1673
|
-
".js": "application/javascript",
|
|
1674
|
-
".css": "text/css",
|
|
1675
|
-
".svg": "image/svg+xml",
|
|
1676
|
-
".png": "image/png",
|
|
1677
|
-
".ico": "image/x-icon",
|
|
1678
|
-
".woff2": "font/woff2",
|
|
1679
|
-
".woff": "font/woff"
|
|
1680
|
-
}[ext] || "application/octet-stream");
|
|
1681
|
-
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1682
|
-
res.end(content);
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
} catch (error) {
|
|
1686
|
-
if (error instanceof HttpError) {
|
|
1687
|
-
res.statusCode = error.status;
|
|
1688
|
-
res.end(error.message);
|
|
1689
|
-
return;
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1239
|
+
if (await serveGalleryAsset(resolveGalleryDistDir(), url, res)) return;
|
|
1692
1240
|
}
|
|
1693
1241
|
next();
|
|
1694
1242
|
});
|
|
@@ -1767,7 +1315,17 @@ function registerMiddleware(devServer, ctx) {
|
|
|
1767
1315
|
});
|
|
1768
1316
|
devServer.middlewares.use(`${basePath}/art`, async (req, res, next) => {
|
|
1769
1317
|
const url = new URL(req.url || "", "http://localhost");
|
|
1770
|
-
|
|
1318
|
+
let artPath;
|
|
1319
|
+
try {
|
|
1320
|
+
artPath = decodeUrlComponent(url.pathname.slice(1), "art path");
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
if (error instanceof HttpError) {
|
|
1323
|
+
res.statusCode = error.status;
|
|
1324
|
+
res.end(error.message);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
throw error;
|
|
1328
|
+
}
|
|
1771
1329
|
if (!artPath) {
|
|
1772
1330
|
next();
|
|
1773
1331
|
return;
|
|
@@ -1805,122 +1363,32 @@ function registerMiddleware(devServer, ctx) {
|
|
|
1805
1363
|
});
|
|
1806
1364
|
}
|
|
1807
1365
|
//#endregion
|
|
1808
|
-
//#region src/tokens/
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
*
|
|
1812
|
-
* Reads and parses design token files (JSON) and directories,
|
|
1813
|
-
* flattening nested structures into categorized token collections.
|
|
1814
|
-
*/
|
|
1815
|
-
/**
|
|
1816
|
-
* Parse Style Dictionary tokens file.
|
|
1817
|
-
*/
|
|
1818
|
-
async function parseTokens(tokensPath) {
|
|
1819
|
-
const absolutePath = path.resolve(tokensPath);
|
|
1820
|
-
if ((await fs.promises.stat(absolutePath)).isDirectory()) return parseTokenDirectory(absolutePath);
|
|
1821
|
-
const content = await fs.promises.readFile(absolutePath, "utf-8");
|
|
1822
|
-
return flattenTokens(JSON.parse(content));
|
|
1366
|
+
//#region src/tokens/native.ts
|
|
1367
|
+
function tokenNative() {
|
|
1368
|
+
return loadNative();
|
|
1823
1369
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
const mergedTokens = createRecord();
|
|
1829
|
-
await mergeTokenDirectory(mergedTokens, dirPath);
|
|
1830
|
-
return flattenTokens(mergedTokens);
|
|
1831
|
-
}
|
|
1832
|
-
async function mergeTokenDirectory(target, dirPath) {
|
|
1833
|
-
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
1834
|
-
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1835
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
1836
|
-
if (entry.isDirectory()) {
|
|
1837
|
-
await mergeTokenDirectory(target, fullPath);
|
|
1838
|
-
continue;
|
|
1839
|
-
}
|
|
1840
|
-
if (!entry.isFile() || !entry.name.endsWith(".json") && !entry.name.endsWith(".tokens.json")) continue;
|
|
1841
|
-
const content = await fs.promises.readFile(fullPath, "utf-8");
|
|
1842
|
-
deepMergeTokenTrees(target, JSON.parse(content));
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
function deepMergeTokenTrees(target, source) {
|
|
1846
|
-
for (const [key, value] of Object.entries(source)) {
|
|
1847
|
-
const existing = target[key];
|
|
1848
|
-
if (isPlainObject(existing) && isPlainObject(value) && !isTokenValue(existing) && !isTokenValue(value)) {
|
|
1849
|
-
deepMergeTokenTrees(existing, value);
|
|
1850
|
-
continue;
|
|
1851
|
-
}
|
|
1852
|
-
target[key] = cloneTokenTree(value);
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
/**
|
|
1856
|
-
* Flatten nested token structure into categories.
|
|
1857
|
-
*/
|
|
1858
|
-
function flattenTokens(tokens, prefix = []) {
|
|
1859
|
-
const categories = [];
|
|
1860
|
-
for (const [key, value] of Object.entries(tokens)) {
|
|
1861
|
-
if (isTokenValue(value)) continue;
|
|
1862
|
-
if (typeof value === "object" && value !== null) {
|
|
1863
|
-
const categoryTokens = extractTokens(value);
|
|
1864
|
-
const subcategories = flattenTokens(value, [...prefix, key]);
|
|
1865
|
-
if (Object.keys(categoryTokens).length > 0 || subcategories.length > 0) categories.push({
|
|
1866
|
-
name: formatCategoryName(key),
|
|
1867
|
-
tokens: categoryTokens,
|
|
1868
|
-
subcategories: subcategories.length > 0 ? subcategories : void 0
|
|
1869
|
-
});
|
|
1870
|
-
}
|
|
1370
|
+
function normalizeCategories(categories) {
|
|
1371
|
+
for (const category of categories) {
|
|
1372
|
+
category.tokens = nullRecord(category.tokens);
|
|
1373
|
+
if (category.subcategories) normalizeCategories(category.subcategories);
|
|
1871
1374
|
}
|
|
1872
1375
|
return categories;
|
|
1873
1376
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
*/
|
|
1877
|
-
function extractTokens(obj) {
|
|
1878
|
-
const tokens = createRecord();
|
|
1879
|
-
for (const [key, value] of Object.entries(obj)) if (isTokenValue(value)) tokens[key] = normalizeToken(value);
|
|
1880
|
-
return tokens;
|
|
1881
|
-
}
|
|
1882
|
-
function cloneTokenTree(value) {
|
|
1883
|
-
if (Array.isArray(value)) return value.map(cloneTokenTree);
|
|
1884
|
-
if (isPlainObject(value)) {
|
|
1885
|
-
const cloned = createRecord();
|
|
1886
|
-
for (const [key, child] of Object.entries(value)) cloned[key] = cloneTokenTree(child);
|
|
1887
|
-
return cloned;
|
|
1888
|
-
}
|
|
1889
|
-
return value;
|
|
1890
|
-
}
|
|
1891
|
-
/**
|
|
1892
|
-
* Check if a value is a token definition.
|
|
1893
|
-
*/
|
|
1894
|
-
function isTokenValue(value) {
|
|
1895
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1896
|
-
const obj = value;
|
|
1897
|
-
return "value" in obj && (typeof obj.value === "string" || typeof obj.value === "number") || "$value" in obj && (typeof obj.$value === "string" || typeof obj.$value === "number");
|
|
1898
|
-
}
|
|
1899
|
-
function isPlainObject(value) {
|
|
1900
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1901
|
-
}
|
|
1902
|
-
function createRecord() {
|
|
1903
|
-
return Object.create(null);
|
|
1377
|
+
function nullRecord(record) {
|
|
1378
|
+
return Object.assign(Object.create(null), record);
|
|
1904
1379
|
}
|
|
1380
|
+
//#endregion
|
|
1381
|
+
//#region src/tokens/parser.ts
|
|
1905
1382
|
/**
|
|
1906
|
-
*
|
|
1383
|
+
* Token parsing utilities for Style Dictionary integration.
|
|
1384
|
+
*
|
|
1385
|
+
* Thin native binding for design token files (JSON) and directories.
|
|
1907
1386
|
*/
|
|
1908
|
-
function normalizeToken(raw) {
|
|
1909
|
-
const token = {
|
|
1910
|
-
value: raw.value ?? raw.$value,
|
|
1911
|
-
type: raw.type ?? raw.$type,
|
|
1912
|
-
description: raw.description,
|
|
1913
|
-
attributes: raw.attributes
|
|
1914
|
-
};
|
|
1915
|
-
if (raw.$tier === "primitive" || raw.$tier === "semantic") token.$tier = raw.$tier;
|
|
1916
|
-
if (typeof raw.$reference === "string") token.$reference = raw.$reference;
|
|
1917
|
-
return token;
|
|
1918
|
-
}
|
|
1919
1387
|
/**
|
|
1920
|
-
*
|
|
1388
|
+
* Parse Style Dictionary tokens file or directory.
|
|
1921
1389
|
*/
|
|
1922
|
-
function
|
|
1923
|
-
return
|
|
1390
|
+
async function parseTokens(tokensPath) {
|
|
1391
|
+
return normalizeCategories(tokenNative().parseDesignTokensFromPath(tokensPath));
|
|
1924
1392
|
}
|
|
1925
1393
|
//#endregion
|
|
1926
1394
|
//#region src/tokens/usage.ts
|
|
@@ -2027,8 +1495,6 @@ function scanTokenUsage(artFiles, tokenMap) {
|
|
|
2027
1495
|
* Handles building flat token maps from categories, resolving reference chains,
|
|
2028
1496
|
* reading/writing raw token files, and validating semantic references.
|
|
2029
1497
|
*/
|
|
2030
|
-
const REFERENCE_PATTERN = /^\{(.+)\}$/;
|
|
2031
|
-
const MAX_RESOLVE_DEPTH = 10;
|
|
2032
1498
|
const UNSAFE_TOKEN_PATH_SEGMENTS = new Set([
|
|
2033
1499
|
"__proto__",
|
|
2034
1500
|
"prototype",
|
|
@@ -2044,53 +1510,15 @@ function parseTokenPath(dotPath) {
|
|
|
2044
1510
|
/**
|
|
2045
1511
|
* Flatten nested categories into a flat map keyed by dot-path.
|
|
2046
1512
|
*/
|
|
2047
|
-
function buildTokenMap(categories
|
|
2048
|
-
|
|
2049
|
-
for (const cat of categories) {
|
|
2050
|
-
const catKey = cat.name.toLowerCase().replace(/\s+/g, "-");
|
|
2051
|
-
const catPath = [...prefix, catKey];
|
|
2052
|
-
for (const [name, token] of Object.entries(cat.tokens)) {
|
|
2053
|
-
const dotPath = [...catPath, name].join(".");
|
|
2054
|
-
map[dotPath] = token;
|
|
2055
|
-
}
|
|
2056
|
-
if (cat.subcategories) {
|
|
2057
|
-
const subMap = buildTokenMap(cat.subcategories, catPath);
|
|
2058
|
-
Object.assign(map, subMap);
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
return map;
|
|
1513
|
+
function buildTokenMap(categories) {
|
|
1514
|
+
return nullRecord(tokenNative().buildDesignTokenMap(categories));
|
|
2062
1515
|
}
|
|
2063
1516
|
/**
|
|
2064
1517
|
* Resolve references in categories, setting $tier, $reference, and $resolvedValue.
|
|
2065
1518
|
*/
|
|
2066
|
-
function resolveReferences(categories,
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
if (cat.subcategories) resolveReferences(cat.subcategories, tokenMap);
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
function resolveTokenReference(token, tokenMap) {
|
|
2073
|
-
if (typeof token.value === "string") {
|
|
2074
|
-
const match = token.value.match(REFERENCE_PATTERN);
|
|
2075
|
-
if (match) {
|
|
2076
|
-
token.$tier = token.$tier ?? "semantic";
|
|
2077
|
-
token.$reference = match[1];
|
|
2078
|
-
token.$resolvedValue = resolveValue(match[1], tokenMap, 0, /* @__PURE__ */ new Set());
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
token.$tier = token.$tier ?? "primitive";
|
|
2083
|
-
}
|
|
2084
|
-
function resolveValue(ref, tokenMap, depth, visited) {
|
|
2085
|
-
if (depth >= MAX_RESOLVE_DEPTH || visited.has(ref)) return void 0;
|
|
2086
|
-
visited.add(ref);
|
|
2087
|
-
const target = tokenMap[ref];
|
|
2088
|
-
if (!target) return void 0;
|
|
2089
|
-
if (typeof target.value === "string") {
|
|
2090
|
-
const match = target.value.match(REFERENCE_PATTERN);
|
|
2091
|
-
if (match) return resolveValue(match[1], tokenMap, depth + 1, visited);
|
|
2092
|
-
}
|
|
2093
|
-
return target.value;
|
|
1519
|
+
function resolveReferences(categories, _tokenMap) {
|
|
1520
|
+
const resolved = tokenNative().resolveDesignTokenReferences(categories);
|
|
1521
|
+
categories.splice(0, categories.length, ...normalizeCategories(resolved.categories));
|
|
2094
1522
|
}
|
|
2095
1523
|
/**
|
|
2096
1524
|
* Read raw JSON token file.
|
|
@@ -2158,48 +1586,13 @@ function deleteTokenAtPath(data, dotPath) {
|
|
|
2158
1586
|
* Validate that a semantic reference points to an existing token and has no cycles.
|
|
2159
1587
|
*/
|
|
2160
1588
|
function validateSemanticReference(tokenMap, reference, selfPath) {
|
|
2161
|
-
|
|
2162
|
-
valid: false,
|
|
2163
|
-
error: `Reference target "${reference}" does not exist`
|
|
2164
|
-
};
|
|
2165
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2166
|
-
if (selfPath) visited.add(selfPath);
|
|
2167
|
-
let current = reference;
|
|
2168
|
-
let depth = 0;
|
|
2169
|
-
while (depth < MAX_RESOLVE_DEPTH) {
|
|
2170
|
-
if (visited.has(current)) return {
|
|
2171
|
-
valid: false,
|
|
2172
|
-
error: `Circular reference detected at "${current}"`
|
|
2173
|
-
};
|
|
2174
|
-
visited.add(current);
|
|
2175
|
-
const target = tokenMap[current];
|
|
2176
|
-
if (!target) break;
|
|
2177
|
-
if (typeof target.value === "string") {
|
|
2178
|
-
const match = target.value.match(REFERENCE_PATTERN);
|
|
2179
|
-
if (match) {
|
|
2180
|
-
current = match[1];
|
|
2181
|
-
depth++;
|
|
2182
|
-
continue;
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
break;
|
|
2186
|
-
}
|
|
2187
|
-
if (depth >= MAX_RESOLVE_DEPTH) return {
|
|
2188
|
-
valid: false,
|
|
2189
|
-
error: "Reference chain too deep (max 10)"
|
|
2190
|
-
};
|
|
2191
|
-
return { valid: true };
|
|
1589
|
+
return tokenNative().validateDesignTokenReference(tokenMap, reference, selfPath);
|
|
2192
1590
|
}
|
|
2193
1591
|
/**
|
|
2194
1592
|
* Find all tokens that reference the given path.
|
|
2195
1593
|
*/
|
|
2196
1594
|
function findDependentTokens(tokenMap, targetPath) {
|
|
2197
|
-
|
|
2198
|
-
for (const [path, token] of Object.entries(tokenMap)) if (typeof token.value === "string") {
|
|
2199
|
-
const match = token.value.match(REFERENCE_PATTERN);
|
|
2200
|
-
if (match && match[1] === targetPath) dependents.push(path);
|
|
2201
|
-
}
|
|
2202
|
-
return dependents;
|
|
1595
|
+
return tokenNative().findDependentDesignTokens(tokenMap, targetPath);
|
|
2203
1596
|
}
|
|
2204
1597
|
//#endregion
|
|
2205
1598
|
//#region src/tokens/generator.ts
|
|
@@ -2210,6 +1603,10 @@ function findDependentTokens(tokenMap, targetPath) {
|
|
|
2210
1603
|
* and provides the main processStyleDictionary orchestrator function.
|
|
2211
1604
|
*/
|
|
2212
1605
|
const SAFE_CSS_COLOR_PATTERN = /^(?:#[0-9a-fA-F]{3,8}|(?:rgb|hsl)a?\(\s*[-+.\d,%\s]+\))$/;
|
|
1606
|
+
const DANGEROUS_PROTOCOL_PATTERN = /\b(javascript|vbscript):/gi;
|
|
1607
|
+
function escapeTokenText(value) {
|
|
1608
|
+
return escapeHtml(value).replace(DANGEROUS_PROTOCOL_PATTERN, "$1:");
|
|
1609
|
+
}
|
|
2213
1610
|
function safeCssColor(value, type) {
|
|
2214
1611
|
if (typeof value !== "string") return null;
|
|
2215
1612
|
const trimmed = value.trim();
|
|
@@ -2227,16 +1624,16 @@ function generateTokensHtml(categories) {
|
|
|
2227
1624
|
${color ? `<div class="color-swatch" style="background: ${color}"></div>` : ""}
|
|
2228
1625
|
</div>
|
|
2229
1626
|
<div class="token-info">
|
|
2230
|
-
<div class="token-name">${
|
|
2231
|
-
<div class="token-value">${
|
|
2232
|
-
${token.description ? `<div class="token-description">${
|
|
1627
|
+
<div class="token-name">${escapeTokenText(name)}</div>
|
|
1628
|
+
<div class="token-value">${escapeTokenText(String(token.value))}</div>
|
|
1629
|
+
${token.description ? `<div class="token-description">${escapeTokenText(token.description)}</div>` : ""}
|
|
2233
1630
|
</div>
|
|
2234
1631
|
</div>
|
|
2235
1632
|
`;
|
|
2236
1633
|
};
|
|
2237
1634
|
const renderCategory = (category, level = 2) => {
|
|
2238
1635
|
const heading = `h${Math.min(level, 6)}`;
|
|
2239
|
-
let html = `<${heading}>${
|
|
1636
|
+
let html = `<${heading}>${escapeTokenText(category.name)}</${heading}>`;
|
|
2240
1637
|
html += "<div class=\"tokens-grid\">";
|
|
2241
1638
|
for (const [name, token] of Object.entries(category.tokens)) html += renderToken(name, token);
|
|
2242
1639
|
html += "</div>";
|
|
@@ -2330,24 +1727,7 @@ function generateTokensHtml(categories) {
|
|
|
2330
1727
|
* Generate Markdown documentation for tokens.
|
|
2331
1728
|
*/
|
|
2332
1729
|
function generateTokensMarkdown(categories) {
|
|
2333
|
-
|
|
2334
|
-
let md = `\n${"#".repeat(level)} ${category.name}\n\n`;
|
|
2335
|
-
if (Object.keys(category.tokens).length > 0) {
|
|
2336
|
-
md += "| Token | Value | Description |\n";
|
|
2337
|
-
md += "|-------|-------|-------------|\n";
|
|
2338
|
-
for (const [name, token] of Object.entries(category.tokens)) {
|
|
2339
|
-
const desc = token.description || "-";
|
|
2340
|
-
md += `| \`${name}\` | \`${token.value}\` | ${desc} |\n`;
|
|
2341
|
-
}
|
|
2342
|
-
md += "\n";
|
|
2343
|
-
}
|
|
2344
|
-
if (category.subcategories) for (const sub of category.subcategories) md += renderCategory(sub, level + 1);
|
|
2345
|
-
return md;
|
|
2346
|
-
};
|
|
2347
|
-
let markdown = "# Design Tokens\n\n";
|
|
2348
|
-
markdown += `> Generated by Musea on ${(/* @__PURE__ */ new Date()).toISOString()}\n`;
|
|
2349
|
-
for (const category of categories) markdown += renderCategory(category);
|
|
2350
|
-
return markdown;
|
|
1730
|
+
return tokenNative().generateDesignTokensMarkdown(categories, (/* @__PURE__ */ new Date()).toISOString());
|
|
2351
1731
|
}
|
|
2352
1732
|
/**
|
|
2353
1733
|
* Style Dictionary plugin for Musea.
|
|
@@ -2577,7 +1957,7 @@ async function handleTokensDelete(ctx, readBody, sendJson, sendError) {
|
|
|
2577
1957
|
*/
|
|
2578
1958
|
/** GET /api/arts/:path/palette */
|
|
2579
1959
|
async function handleArtPalette(ctx, match, sendJson, sendError) {
|
|
2580
|
-
const artPath =
|
|
1960
|
+
const artPath = decodeUrlComponent(match[1], "art path");
|
|
2581
1961
|
const art = ctx.artFiles.get(artPath);
|
|
2582
1962
|
if (!art) {
|
|
2583
1963
|
sendError("Art not found", 404);
|
|
@@ -2655,7 +2035,7 @@ async function handleArtPalette(ctx, match, sendJson, sendError) {
|
|
|
2655
2035
|
*/
|
|
2656
2036
|
/** GET /api/arts/:path/source */
|
|
2657
2037
|
async function handleArtSource(ctx, match, sendJson, sendError) {
|
|
2658
|
-
const artPath =
|
|
2038
|
+
const artPath = decodeUrlComponent(match[1], "art path");
|
|
2659
2039
|
if (!ctx.artFiles.get(artPath)) {
|
|
2660
2040
|
sendError("Art not found", 404);
|
|
2661
2041
|
return;
|
|
@@ -2671,7 +2051,7 @@ async function handleArtSource(ctx, match, sendJson, sendError) {
|
|
|
2671
2051
|
}
|
|
2672
2052
|
/** GET /api/arts/:path/analysis */
|
|
2673
2053
|
async function handleArtAnalysis(ctx, match, sendJson, sendError) {
|
|
2674
|
-
const artPath =
|
|
2054
|
+
const artPath = decodeUrlComponent(match[1], "art path");
|
|
2675
2055
|
const art = ctx.artFiles.get(artPath);
|
|
2676
2056
|
if (!art) {
|
|
2677
2057
|
sendError("Art not found", 404);
|
|
@@ -2694,7 +2074,7 @@ async function handleArtAnalysis(ctx, match, sendJson, sendError) {
|
|
|
2694
2074
|
}
|
|
2695
2075
|
/** GET /api/arts/:path/docs */
|
|
2696
2076
|
async function handleArtDocs(ctx, match, sendJson, sendError) {
|
|
2697
|
-
const artPath =
|
|
2077
|
+
const artPath = decodeUrlComponent(match[1], "art path");
|
|
2698
2078
|
const art = ctx.artFiles.get(artPath);
|
|
2699
2079
|
if (!art) {
|
|
2700
2080
|
sendError("Art not found", 404);
|
|
@@ -2744,8 +2124,8 @@ async function handleArtDocs(ctx, match, sendJson, sendError) {
|
|
|
2744
2124
|
}
|
|
2745
2125
|
/** GET /api/arts/:path/variants/:name/a11y */
|
|
2746
2126
|
function handleArtA11y(ctx, match, sendJson, sendError) {
|
|
2747
|
-
const artPath =
|
|
2748
|
-
|
|
2127
|
+
const artPath = decodeUrlComponent(match[1], "art path");
|
|
2128
|
+
decodeUrlComponent(match[2], "variant name");
|
|
2749
2129
|
if (!ctx.artFiles.get(artPath)) {
|
|
2750
2130
|
sendError("Art not found", 404);
|
|
2751
2131
|
return;
|
|
@@ -2917,7 +2297,7 @@ function createApiMiddleware(ctx) {
|
|
|
2917
2297
|
if (url?.startsWith("/arts/") && req.method === "PUT") {
|
|
2918
2298
|
const sourceMatch = url.slice(6).match(/^(.+)\/source$/);
|
|
2919
2299
|
if (sourceMatch) {
|
|
2920
|
-
const artPath =
|
|
2300
|
+
const artPath = decodeUrlComponent(sourceMatch[1], "art path");
|
|
2921
2301
|
if (!ctx.artFiles.get(artPath)) {
|
|
2922
2302
|
sendError("Art not found", 404);
|
|
2923
2303
|
return;
|
|
@@ -2964,7 +2344,7 @@ function createApiMiddleware(ctx) {
|
|
|
2964
2344
|
handleArtA11y(ctx, a11yMatch, sendJson, sendError);
|
|
2965
2345
|
return;
|
|
2966
2346
|
}
|
|
2967
|
-
const artPath =
|
|
2347
|
+
const artPath = decodeUrlComponent(rest, "art path");
|
|
2968
2348
|
const art = ctx.artFiles.get(artPath);
|
|
2969
2349
|
if (art) sendJson(art);
|
|
2970
2350
|
else sendError("Art not found", 404);
|