@vizejs/vite-plugin-musea 0.345.0 → 0.350.2
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/a11y/index.d.mts +1 -1
- package/dist/a11y/index.mjs +1 -1
- package/dist/{a11y-62l8G1tr.mjs → a11y-Cht-W4kd.mjs} +1 -1
- package/dist/{a11y-62l8G1tr.mjs.map → a11y-Cht-W4kd.mjs.map} +1 -1
- package/dist/autogen/index.d.mts.map +1 -1
- package/dist/autogen/index.mjs +1 -1
- package/dist/{autogen-B7_TO0n5.mjs → autogen-B-N0cJbZ.mjs} +1 -1
- package/dist/{autogen-B7_TO0n5.mjs.map → autogen-B-N0cJbZ.mjs.map} +1 -1
- package/dist/cli/index.d.mts.map +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/cli/index.mjs.map +1 -1
- package/dist/gallery/assets/MonacoEditor--hIp2XW8.js +2 -0
- package/dist/gallery/assets/MonacoEditor-Db3Q217Z.css +1 -0
- package/dist/gallery/assets/codicon-Brq4_Ui5.ttf +0 -0
- package/dist/gallery/assets/{css.worker-Wv5dxAWO.js → css.worker-BjrR-8m8.js} +36 -32
- package/dist/gallery/assets/cssMode-ZHNilfqs.js +1 -0
- package/dist/gallery/assets/editor-8AbsNvUX.css +1 -0
- package/dist/gallery/assets/editor.api-D1ZVmYNy.js +847 -0
- package/dist/gallery/assets/editor.worker-s_hwqnK9.js +30 -0
- package/dist/gallery/assets/{freemarker2-BZ6nC3A3.js → freemarker2-DTuUuWg9.js} +1 -1
- package/dist/gallery/assets/handlebars-LhSSvR3a.js +1 -0
- package/dist/gallery/assets/html-CSSv3RQ6.js +1 -0
- package/dist/gallery/assets/{html.worker-CQP8QQsS.js → html.worker-BFmRaMni.js} +28 -24
- package/dist/gallery/assets/htmlMode-0CF_bjtn.js +1 -0
- package/dist/gallery/assets/{index-De8nZlO8.js → index-aDilKAxX.js} +26 -25
- package/dist/gallery/assets/{javascript-BnHRMqEr.js → javascript-DVhxh8Pu.js} +1 -1
- package/dist/gallery/assets/json.worker-CD0GJfW6.js +62 -0
- package/dist/gallery/assets/{jsonMode-jcQUywhl.js → jsonMode-QJbmCz1q.js} +1 -1
- package/dist/gallery/assets/liquid-B52Ojsd5.js +1 -0
- package/dist/gallery/assets/lspLanguageFeatures-C5GMmpHQ.js +4 -0
- package/dist/gallery/assets/mdx-DNKO_bCY.js +1 -0
- package/dist/gallery/assets/python-5n83nryU.js +1 -0
- package/dist/gallery/assets/razor-Cv2UCBnr.js +1 -0
- package/dist/gallery/assets/register-DCDIi0Et.js +2 -0
- package/dist/gallery/assets/toggleHighContrast-CFGa3V3O.css +1 -0
- package/dist/gallery/assets/toggleHighContrast-DTCsvl0z.js +51 -0
- package/dist/gallery/assets/{ts.worker-METxwbDZ.js → ts.worker-BFy34-VB.js} +190 -171
- package/dist/gallery/assets/{tsMode-Kibx_hev.js → tsMode-D3AmenYN.js} +1 -1
- package/dist/gallery/assets/typescript-DPV4YtP_.js +1 -0
- package/dist/gallery/assets/vs-Ce8dYDBN.js +6 -0
- package/dist/gallery/assets/workers-BY7_4E6g.js +1 -0
- package/dist/gallery/assets/xml-CYxaE2hx.js +1 -0
- package/dist/gallery/assets/yaml-BilRBwjA.js +1 -0
- package/dist/gallery/index.html +1 -1
- package/dist/{gallery-onvFYjip.mjs → gallery-3Sqabvai.mjs} +2 -2
- package/dist/{gallery-onvFYjip.mjs.map → gallery-3Sqabvai.mjs.map} +1 -1
- package/dist/{index-leLThbJH.d.mts → index-75q-ZCzF.d.mts} +2 -2
- package/dist/index-75q-ZCzF.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.mjs.map +1 -1
- package/dist/{template-CFSGRuGu.mjs → template-ByMVejqE.mjs} +1 -1
- package/dist/{template-CFSGRuGu.mjs.map → template-ByMVejqE.mjs.map} +1 -1
- package/dist/{vrt-C5O4NHsc.d.mts → vrt-D454uyH6.d.mts} +1 -1
- package/dist/vrt-D454uyH6.d.mts.map +1 -0
- package/dist/{vrt-Cv1PK1EF.mjs → vrt-DgVSWKGl.mjs} +1 -1
- package/dist/{vrt-Cv1PK1EF.mjs.map → vrt-DgVSWKGl.mjs.map} +1 -1
- package/dist/vrt.d.mts +2 -2
- package/dist/vrt.mjs +1 -1
- package/package.json +10 -10
- package/dist/gallery/assets/MonacoEditor-DsbIpkQL.js +0 -2
- package/dist/gallery/assets/MonacoEditor-iu5ZxbsG.css +0 -1
- package/dist/gallery/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/gallery/assets/cssMode-BEYj0tak.js +0 -1
- package/dist/gallery/assets/editor-BnkT-hT5.css +0 -1
- package/dist/gallery/assets/editor.api2-Bz2kYzUN.js +0 -872
- package/dist/gallery/assets/editor.main-BVHs2SSr.js +0 -6
- package/dist/gallery/assets/editor.worker-Bd9IXS8d.js +0 -26
- package/dist/gallery/assets/handlebars-DEfvHxqu.js +0 -1
- package/dist/gallery/assets/html-BQHhPRs4.js +0 -1
- package/dist/gallery/assets/htmlMode-BJg8FTpN.js +0 -1
- package/dist/gallery/assets/json.worker-DzV-CpCQ.js +0 -58
- package/dist/gallery/assets/liquid-DBFdARj0.js +0 -1
- package/dist/gallery/assets/lspLanguageFeatures-BAQ0wWN5.js +0 -4
- package/dist/gallery/assets/mdx-B4klyOk6.js +0 -1
- package/dist/gallery/assets/monaco.contribution-DurMprOc.js +0 -2
- package/dist/gallery/assets/python-DVtrR2b4.js +0 -1
- package/dist/gallery/assets/razor-DvGAQ27Z.js +0 -1
- package/dist/gallery/assets/typescript-qDteoCEw.js +0 -1
- package/dist/gallery/assets/workers-Cruk8RxZ.js +0 -1
- package/dist/gallery/assets/xml-CTVXfCOD.js +0 -1
- package/dist/gallery/assets/yaml-BuZYNjL4.js +0 -1
- package/dist/index-leLThbJH.d.mts.map +0 -1
- package/dist/vrt-C5O4NHsc.d.mts.map +0 -1
- /package/dist/gallery/assets/{abap-DVwoIrM0.js → abap-08VXUWAP.js} +0 -0
- /package/dist/gallery/assets/{apex-DnfrpC_v.js → apex-BWPQTe0t.js} +0 -0
- /package/dist/gallery/assets/{azcli-CE6n6ErR.js → azcli-Bc_sGQ0U.js} +0 -0
- /package/dist/gallery/assets/{bat-ainFW1qj.js → bat-i0X4ZdIN.js} +0 -0
- /package/dist/gallery/assets/{bicep-Lzdk7NqX.js → bicep-B5-_aFwp.js} +0 -0
- /package/dist/gallery/assets/{cameligo-BjyZ5cgY.js → cameligo-DMUM7wLl.js} +0 -0
- /package/dist/gallery/assets/{clojure-B-6owjux.js → clojure-Cm7r79vr.js} +0 -0
- /package/dist/gallery/assets/{coffee-npIvPqmH.js → coffee-Ba7i2nA0.js} +0 -0
- /package/dist/gallery/assets/{cpp--A8GPZYM.js → cpp-C7h46wYY.js} +0 -0
- /package/dist/gallery/assets/{csharp-Daa9qY-p.js → csharp-BKxtCVv1.js} +0 -0
- /package/dist/gallery/assets/{csp-N0NfYrCc.js → csp-bTuwJoIa.js} +0 -0
- /package/dist/gallery/assets/{css-CUssoHYv.js → css-DIMkf-bt.js} +0 -0
- /package/dist/gallery/assets/{cypher-CmQpMFeE.js → cypher-CVaqCwHa.js} +0 -0
- /package/dist/gallery/assets/{dart-DOjZJNmR.js → dart-onAF5SnQ.js} +0 -0
- /package/dist/gallery/assets/{dockerfile-CxIfCbxU.js → dockerfile-DZFCIeNp.js} +0 -0
- /package/dist/gallery/assets/{ecl-AnA3JP56.js → ecl-D05T4iGw.js} +0 -0
- /package/dist/gallery/assets/{elixir-D-N3eh22.js → elixir-6RTg0lbw.js} +0 -0
- /package/dist/gallery/assets/{flow9-BR6FT9qg.js → flow9-C5_-GSwl.js} +0 -0
- /package/dist/gallery/assets/{fsharp-DR0IQ95q.js → fsharp-C8Ef5oNN.js} +0 -0
- /package/dist/gallery/assets/{go-B27OpVON.js → go-C-y9NEjX.js} +0 -0
- /package/dist/gallery/assets/{graphql-Cw7HtomI.js → graphql-fmXr3nnJ.js} +0 -0
- /package/dist/gallery/assets/{hcl-CDDd0gYG.js → hcl-CpzslTdj.js} +0 -0
- /package/dist/gallery/assets/{ini-ChiSjCUM.js → ini-sBoK_t0W.js} +0 -0
- /package/dist/gallery/assets/{java-CKVuuvX6.js → java-BEtHBSE6.js} +0 -0
- /package/dist/gallery/assets/{julia-BcKGx43g.js → julia-Bri6UV-V.js} +0 -0
- /package/dist/gallery/assets/{kotlin-DemwFMfK.js → kotlin-BOotOW0E.js} +0 -0
- /package/dist/gallery/assets/{less-BFpYPxgE.js → less-B9JPFI3C.js} +0 -0
- /package/dist/gallery/assets/{lexon-DDPF3See.js → lexon-CfSJPG6W.js} +0 -0
- /package/dist/gallery/assets/{lua-CmzM4S9z.js → lua-CsQS60Ue.js} +0 -0
- /package/dist/gallery/assets/{m3-C75GLUav.js → m3-D-oSqn_W.js} +0 -0
- /package/dist/gallery/assets/{markdown-B6XL0Y9j.js → markdown-Cimd5fb3.js} +0 -0
- /package/dist/gallery/assets/{mips-BG4Fy7Bl.js → mips-CIPQ_RoX.js} +0 -0
- /package/dist/gallery/assets/{msdax-H0aqYz0U.js → msdax-DauUninz.js} +0 -0
- /package/dist/gallery/assets/{mysql-CDbOhBhf.js → mysql-SOo6toE5.js} +0 -0
- /package/dist/gallery/assets/{objective-c-DKE6-VEf.js → objective-c-FvmIjYaQ.js} +0 -0
- /package/dist/gallery/assets/{pascal-DBuqflGM.js → pascal-DrH0SRf2.js} +0 -0
- /package/dist/gallery/assets/{pascaligo-BVtulzHb.js → pascaligo-D-ptJ9y-.js} +0 -0
- /package/dist/gallery/assets/{perl-xkTv78ng.js → perl-oz_6vUea.js} +0 -0
- /package/dist/gallery/assets/{pgsql-Cxti3J5E.js → pgsql-DTj74zXo.js} +0 -0
- /package/dist/gallery/assets/{php-Bh5BD3dg.js → php-nr791fC2.js} +0 -0
- /package/dist/gallery/assets/{pla-DSsYzlXV.js → pla-CopQ2nXW.js} +0 -0
- /package/dist/gallery/assets/{postiats-De0qivlp.js → postiats-43DmfD33.js} +0 -0
- /package/dist/gallery/assets/{powerquery-KGKq89F-.js → powerquery-D3hlyOfw.js} +0 -0
- /package/dist/gallery/assets/{powershell-Djwhihrv.js → powershell-DmHpPYUd.js} +0 -0
- /package/dist/gallery/assets/{protobuf-Jbp01qUU.js → protobuf-C531GsRP.js} +0 -0
- /package/dist/gallery/assets/{pug-BntfJCN7.js → pug-Z5eAx3Zn.js} +0 -0
- /package/dist/gallery/assets/{qsharp-CHH1r_aq.js → qsharp-DkqhCAOL.js} +0 -0
- /package/dist/gallery/assets/{r-BbeUcBN9.js → r-BwWrilGY.js} +0 -0
- /package/dist/gallery/assets/{redis-DR9m_VtD.js → redis-ClamHrr6.js} +0 -0
- /package/dist/gallery/assets/{redshift-D97Qa-FW.js → redshift-DT7zqm-g.js} +0 -0
- /package/dist/gallery/assets/{restructuredtext-DQ1MtboI.js → restructuredtext-BYgofb2h.js} +0 -0
- /package/dist/gallery/assets/{ruby-ByLGeogt.js → ruby-DezsRK8O.js} +0 -0
- /package/dist/gallery/assets/{rust-CIqtS9ON.js → rust-DdL9SqIa.js} +0 -0
- /package/dist/gallery/assets/{sb-ByVTEZ1d.js → sb-CcwsVR0C.js} +0 -0
- /package/dist/gallery/assets/{scala-DvkPypTh.js → scala-DHpiXF5c.js} +0 -0
- /package/dist/gallery/assets/{scheme-CQy1Ya2H.js → scheme-BeGwcela.js} +0 -0
- /package/dist/gallery/assets/{scss-DLIO8qmP.js → scss-gp-XZpBa.js} +0 -0
- /package/dist/gallery/assets/{shell-BZaILY8J.js → shell-CC2rA5mh.js} +0 -0
- /package/dist/gallery/assets/{solidity-D80FpOWz.js → solidity-BEEn4gHE.js} +0 -0
- /package/dist/gallery/assets/{sophia-DXh1T4eB.js → sophia-CRfGWb83.js} +0 -0
- /package/dist/gallery/assets/{sparql-DHSgmKlJ.js → sparql-D_Lu-MrJ.js} +0 -0
- /package/dist/gallery/assets/{sql-9GboOSCN.js → sql-NEE52Syq.js} +0 -0
- /package/dist/gallery/assets/{st--m1Z2h3c.js → st-DbInun42.js} +0 -0
- /package/dist/gallery/assets/{swift-DMo7Bf1r.js → swift-Bxkupp3x.js} +0 -0
- /package/dist/gallery/assets/{systemverilog-D6kP5wsA.js → systemverilog-Bz4Y3fRF.js} +0 -0
- /package/dist/gallery/assets/{tcl-HAhMyY2Y.js → tcl-DISqw1ZD.js} +0 -0
- /package/dist/gallery/assets/{twig-RNzllx71.js → twig-De2hgUGE.js} +0 -0
- /package/dist/gallery/assets/{typespec-DeyXqKVJ.js → typespec-B8J7ngcE.js} +0 -0
- /package/dist/gallery/assets/{vb-BfpeX2r9.js → vb-DV3o63ZY.js} +0 -0
- /package/dist/gallery/assets/{wgsl-B52428dy.js → wgsl-DpFanUEy.js} +0 -0
|
@@ -438,4 +438,4 @@ function generateGalleryScript(basePath) {
|
|
|
438
438
|
//#endregion
|
|
439
439
|
export { generateGalleryGlobalsScript as a, collectRequestBody as c, parseJsonBody as d, resolveInside as f, validateDevApiRequest as g, serializeScriptValue as h, generateDevGlobalsScript as i, createDevSessionToken as l, resolveUrlPathInside as m, generateGalleryScript as n, DEFAULT_API_BODY_LIMIT_BYTES as o, resolveInsideAny as p, generateGalleryModule as r, HttpError as s, generateGalleryBody as t, decodeUrlComponent as u };
|
|
440
440
|
|
|
441
|
-
//# sourceMappingURL=template-
|
|
441
|
+
//# sourceMappingURL=template-ByMVejqE.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-CFSGRuGu.mjs","names":[],"sources":["../src/security.ts","../src/gallery/globals.ts","../src/gallery/module.ts","../src/gallery/template.ts"],"sourcesContent":["import { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type { IncomingMessage } from \"node:http\";\nimport path from \"node:path\";\n\nexport const DEFAULT_API_BODY_LIMIT_BYTES = 1024 * 1024;\n\nexport class HttpError extends Error {\n readonly status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.name = \"HttpError\";\n this.status = status;\n }\n}\n\nexport function createDevSessionToken(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\nfunction realpathNearest(targetPath: string): string {\n let current = path.resolve(targetPath);\n const missingParts: string[] = [];\n\n while (true) {\n try {\n const real = fs.realpathSync.native(current);\n return missingParts.length > 0 ? path.join(real, ...missingParts.reverse()) : real;\n } catch {\n const parent = path.dirname(current);\n if (parent === current) {\n return path.resolve(targetPath);\n }\n missingParts.push(path.basename(current));\n current = parent;\n }\n }\n}\n\nfunction isResolvedPathInside(parentDir: string, candidatePath: string): boolean {\n const parent = path.resolve(parentDir);\n const candidate = path.resolve(candidatePath);\n const relative = path.relative(parent, candidate);\n return relative === \"\" || (!relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n\nexport function isPathInside(parentDir: string, candidatePath: string): boolean {\n return isResolvedPathInside(realpathNearest(parentDir), realpathNearest(candidatePath));\n}\n\nexport function isPathInsideAny(parentDirs: string[], candidatePath: string): boolean {\n const candidate = realpathNearest(candidatePath);\n return parentDirs.some((parentDir) =>\n isResolvedPathInside(realpathNearest(parentDir), candidate),\n );\n}\n\nexport function resolveInside(parentDir: string, candidatePath: string, label = \"path\"): string {\n return resolveInsideAny([parentDir], candidatePath, label);\n}\n\nexport function resolveInsideAny(\n parentDirs: string[],\n candidatePath: string,\n label = \"path\",\n): string {\n if (candidatePath.includes(\"\\0\")) {\n throw new HttpError(`${label} contains an invalid character`, 400);\n }\n\n if (parentDirs.length === 0) {\n throw new HttpError(`No allowed directories configured for ${label}`, 500);\n }\n\n const parent = path.resolve(parentDirs[0] ?? \".\");\n const resolved = path.isAbsolute(candidatePath)\n ? path.resolve(candidatePath)\n : path.resolve(parent, candidatePath);\n\n if (!isPathInsideAny(parentDirs, resolved)) {\n throw new HttpError(`${label} escapes the allowed directory`, 400);\n }\n\n return resolved;\n}\n\nexport function resolveUrlPathInside(\n parentDir: string,\n requestUrl: string,\n label = \"path\",\n): string {\n const rawPath = requestUrl.split(/[?#]/, 1)[0] || \"/\";\n let pathname = decodeUrlComponent(rawPath, label);\n\n pathname = pathname.replaceAll(\"\\\\\", \"/\");\n if (pathname.split(\"/\").includes(\"..\")) {\n throw new HttpError(`${label} must not contain parent directory segments`, 400);\n }\n\n const relativePath = `.${pathname}`;\n return resolveInside(parentDir, relativePath, label);\n}\n\nexport function decodeUrlComponent(value: string, label = \"path\"): string {\n try {\n return decodeURIComponent(value);\n } catch {\n throw new HttpError(`${label} is not valid URL encoding`, 400);\n }\n}\n\nexport function collectRequestBody(\n req: IncomingMessage,\n limit = DEFAULT_API_BODY_LIMIT_BYTES,\n): Promise<string> {\n return new Promise((resolve, reject) => {\n let body = \"\";\n let size = 0;\n let completed = false;\n\n req.on(\"data\", (chunk: Buffer | string) => {\n if (completed) return;\n\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n size += buffer.byteLength;\n if (size > limit) {\n completed = true;\n reject(new HttpError(`Request body exceeds ${limit} bytes`, 413));\n return;\n }\n\n body += buffer.toString(\"utf-8\");\n });\n\n req.on(\"end\", () => {\n if (!completed) {\n completed = true;\n resolve(body);\n }\n });\n\n req.on(\"error\", (error) => {\n if (!completed) {\n completed = true;\n reject(error);\n }\n });\n });\n}\n\nexport function parseJsonBody<T = unknown>(body: string): T {\n try {\n return JSON.parse(body) as T;\n } catch (error) {\n if (error instanceof SyntaxError) {\n throw new HttpError(\"Malformed JSON body\", 400);\n }\n throw error;\n }\n}\n\nexport function validateDevApiRequest(\n req: IncomingMessage,\n sessionToken: string,\n): HttpError | null {\n const originError = validateOrigin(req);\n if (originError) return originError;\n\n if (!isUnsafeMethod(req.method)) {\n return null;\n }\n\n if (!hasValidSessionToken(req, sessionToken)) {\n return new HttpError(\"Invalid Musea dev session token\", 403);\n }\n\n if (!isJsonRequest(req)) {\n return new HttpError(\"Content-Type must be application/json\", 415);\n }\n\n return null;\n}\n\nexport function serializeScriptValue(value: unknown): string {\n return (JSON.stringify(value) ?? \"undefined\").replace(/[<>&\\u2028\\u2029]/g, (char) => {\n switch (char) {\n case \"<\":\n return \"\\\\u003C\";\n case \">\":\n return \"\\\\u003E\";\n case \"&\":\n return \"\\\\u0026\";\n case \"\\u2028\":\n return \"\\\\u2028\";\n case \"\\u2029\":\n return \"\\\\u2029\";\n default:\n return char;\n }\n });\n}\n\nfunction isUnsafeMethod(method: string | undefined): boolean {\n return method === \"POST\" || method === \"PUT\" || method === \"PATCH\" || method === \"DELETE\";\n}\n\nfunction isJsonRequest(req: IncomingMessage): boolean {\n const contentType = getHeader(req, \"content-type\");\n return contentType?.split(\";\")[0]?.trim().toLowerCase() === \"application/json\";\n}\n\nfunction validateOrigin(req: IncomingMessage): HttpError | null {\n const secFetchSite = getHeader(req, \"sec-fetch-site\");\n if (secFetchSite === \"cross-site\") {\n return new HttpError(\"Cross-origin Musea API requests are not allowed\", 403);\n }\n\n const origin = getHeader(req, \"origin\");\n if (!origin) return null;\n\n const host = getHeader(req, \"host\");\n if (!host) {\n return new HttpError(\"Missing Host header\", 400);\n }\n\n try {\n const originUrl = new URL(origin);\n if (originUrl.host !== host) {\n return new HttpError(\"Cross-origin Musea API requests are not allowed\", 403);\n }\n } catch {\n return new HttpError(\"Invalid Origin header\", 400);\n }\n\n return null;\n}\n\nfunction hasValidSessionToken(req: IncomingMessage, expectedToken: string): boolean {\n const actualToken = getHeader(req, \"x-musea-session\");\n if (!actualToken) return false;\n\n const actual = Buffer.from(actualToken);\n const expected = Buffer.from(expectedToken);\n return actual.length === expected.length && timingSafeEqual(actual, expected);\n}\n\nfunction getHeader(req: IncomingMessage, name: string): string | undefined {\n const value = req.headers[name];\n if (Array.isArray(value)) return value[0];\n return value;\n}\n","import { serializeScriptValue } from \"../security.js\";\nimport type { MuseaTokenPreviewConfig } from \"../tokens/preview.js\";\n\nexport interface MuseaGalleryGlobals {\n basePath: string;\n devSessionToken?: string;\n staticPreviews?: Record<string, Record<string, string>>;\n themeConfig?: { default: string; custom?: Record<string, unknown> };\n tokenPreviewConfig?: MuseaTokenPreviewConfig;\n}\n\nexport function generateGalleryGlobalsScript(globals: MuseaGalleryGlobals): string {\n const parts = [`window.__MUSEA_BASE_PATH__=${serializeScriptValue(globals.basePath)};`];\n if (globals.devSessionToken !== undefined) {\n parts.push(`window.__MUSEA_SESSION_TOKEN__=${serializeScriptValue(globals.devSessionToken)};`);\n }\n if (globals.staticPreviews !== undefined) {\n parts.push(\"window.__MUSEA_STATIC__=true;\");\n parts.push(`window.__MUSEA_STATIC_PREVIEWS__=${serializeScriptValue(globals.staticPreviews)};`);\n }\n if (globals.themeConfig) {\n parts.push(`window.__MUSEA_THEME_CONFIG__=${serializeScriptValue(globals.themeConfig)};`);\n }\n if (globals.tokenPreviewConfig) {\n parts.push(\n `window.__MUSEA_TOKEN_PREVIEWS__=${serializeScriptValue(globals.tokenPreviewConfig)};`,\n );\n }\n return parts.join(\"\");\n}\n\nexport function generateDevGlobalsScript(\n basePath: string,\n devSessionToken: string,\n themeConfig?: MuseaGalleryGlobals[\"themeConfig\"],\n tokenPreviewConfig?: MuseaTokenPreviewConfig,\n): string {\n return generateGalleryGlobalsScript({\n basePath,\n devSessionToken,\n themeConfig,\n tokenPreviewConfig,\n });\n}\n","import { serializeScriptValue } from \"../security.js\";\n\nexport function generateGalleryModule(basePath: string): string {\n return `\nexport const basePath = ${serializeScriptValue(basePath)};\nexport async function loadArts() {\n const res = await fetch(basePath + '/api/arts');\n return res.json();\n}\n`;\n}\n","/**\n * HTML structure and inline JS generation for the Musea gallery.\n *\n * Extracted from gallery.ts to keep file sizes manageable.\n */\n\nimport { serializeScriptValue } from \"../security.js\";\n\nfunction escapeHtmlAttribute(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => {\n switch (char) {\n case \"&\":\n return \"&\";\n case \"<\":\n return \"<\";\n case \">\":\n return \">\";\n case '\"':\n return \""\";\n case \"'\":\n return \"'\";\n default:\n return char;\n }\n });\n}\n\n/**\n * Generate the gallery HTML body (header, sidebar, content, and inline script).\n */\nexport function generateGalleryBody(basePath: string): string {\n const escapedBasePath = escapeHtmlAttribute(basePath);\n\n return `\n <header class=\"header\">\n <div class=\"header-left\">\n <a href=\"${escapedBasePath}\" class=\"logo\">\n <svg class=\"logo-svg\" width=\"32\" height=\"32\" viewBox=\"0 0 200 200\" fill=\"none\">\n <g transform=\"translate(30, 25) scale(1.2)\">\n <g transform=\"translate(15, 10) skewX(-15)\">\n <path d=\"M 65 0 L 40 60 L 70 20 L 65 0 Z\" fill=\"currentColor\"/>\n <path d=\"M 20 0 L 40 60 L 53 13 L 20 0 Z\" fill=\"currentColor\"/>\n </g>\n </g>\n <g transform=\"translate(110, 120)\">\n <line x1=\"5\" y1=\"10\" x2=\"5\" y2=\"50\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\"/>\n <line x1=\"60\" y1=\"10\" x2=\"60\" y2=\"50\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\"/>\n <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\"/>\n <rect x=\"15\" y=\"18\" width=\"14\" height=\"12\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" opacity=\"0.7\"/>\n <rect x=\"36\" y=\"18\" width=\"14\" height=\"12\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" opacity=\"0.7\"/>\n <rect x=\"23\" y=\"35\" width=\"18\" height=\"12\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" opacity=\"0.6\"/>\n </g>\n </svg>\n Musea\n </a>\n <span class=\"header-subtitle\">Component Gallery</span>\n </div>\n <div class=\"search-container\">\n <svg class=\"search-icon\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.35-4.35\"/>\n </svg>\n <input type=\"text\" class=\"search-input\" placeholder=\"Search components...\" id=\"search\">\n </div>\n </header>\n\n <main class=\"main\">\n <aside class=\"sidebar\" id=\"sidebar\">\n <div class=\"loading\">\n <div class=\"loading-spinner\"></div>\n Loading...\n </div>\n </aside>\n <section class=\"content\" id=\"content\">\n <div class=\"empty-state\">\n <div class=\"empty-state-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <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\"/>\n <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\"/>\n <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\"/>\n </svg>\n </div>\n <div class=\"empty-state-title\">Select a component</div>\n <div class=\"empty-state-text\">Choose a component from the sidebar to view its variants and documentation</div>\n </div>\n </section>\n </main>`;\n}\n\n/**\n * Generate the gallery inline script (SPA logic).\n */\nexport function generateGalleryScript(basePath: string): string {\n return `\n const basePath = ${serializeScriptValue(basePath)};\n let arts = [];\n let selectedArt = null;\n let searchQuery = '';\n\n async function loadArts() {\n try {\n const res = await fetch(basePath + '/api/arts');\n arts = await res.json();\n renderSidebar();\n } catch (e) {\n console.error('Failed to load arts:', e);\n document.getElementById('sidebar').innerHTML = '<div class=\"loading\">Failed to load</div>';\n }\n }\n\n function renderSidebar() {\n const sidebar = document.getElementById('sidebar');\n const categories = {};\n\n const filtered = searchQuery\n ? arts.filter(a => a.metadata.title.toLowerCase().includes(searchQuery.toLowerCase()))\n : arts;\n\n for (const art of filtered) {\n const cat = art.metadata.category || 'Components';\n if (!categories[cat]) categories[cat] = [];\n categories[cat].push(art);\n }\n\n if (Object.keys(categories).length === 0) {\n sidebar.innerHTML = '<div class=\"sidebar-section\"><div class=\"loading\">No components found</div></div>';\n return;\n }\n\n let html = '';\n for (const [category, items] of Object.entries(categories)) {\n const escapedCategory = escapeHtml(category);\n html += '<div class=\"sidebar-section\">';\n html += '<div class=\"category-header\" data-category=\"' + escapedCategory + '\">';\n 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>';\n html += '<span>' + escapedCategory + '</span>';\n html += '<span class=\"category-count\">' + items.length + '</span>';\n html += '</div>';\n html += '<ul class=\"art-list\" data-category=\"' + escapedCategory + '\">';\n for (const art of items) {\n const active = selectedArt?.path === art.path ? 'active' : '';\n const variantCount = art.variants?.length || 0;\n html += '<li class=\"art-item ' + active + '\" data-path=\"' + escapeHtml(art.path) + '\">';\n html += '<span>' + escapeHtml(art.metadata.title) + '</span>';\n html += '<span class=\"art-variant-count\">' + variantCount + ' variant' + (variantCount !== 1 ? 's' : '') + '</span>';\n html += '</li>';\n }\n html += '</ul>';\n html += '</div>';\n }\n\n sidebar.innerHTML = html;\n\n sidebar.querySelectorAll('.art-item').forEach(item => {\n item.addEventListener('click', () => {\n const artPath = item.dataset.path;\n selectedArt = arts.find(a => a.path === artPath);\n renderSidebar();\n renderContent();\n });\n });\n\n sidebar.querySelectorAll('.category-header').forEach(header => {\n header.addEventListener('click', () => {\n header.classList.toggle('collapsed');\n const list = header.parentElement?.querySelector('.art-list');\n if (list) list.style.display = header.classList.contains('collapsed') ? 'none' : 'block';\n });\n });\n }\n\n function renderContent() {\n const content = document.getElementById('content');\n if (!selectedArt) {\n content.innerHTML = \\`\n <div class=\"empty-state\">\n <div class=\"empty-state-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <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\"/>\n <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\"/>\n <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\"/>\n </svg>\n </div>\n <div class=\"empty-state-title\">Select a component</div>\n <div class=\"empty-state-text\">Choose a component from the sidebar to view its variants</div>\n </div>\n \\`;\n return;\n }\n\n const meta = selectedArt.metadata;\n const tags = meta.tags || [];\n const variantCount = selectedArt.variants?.length || 0;\n\n let html = '<div class=\"content-inner\">';\n html += '<div class=\"content-header\">';\n html += '<h1 class=\"content-title\">' + escapeHtml(meta.title) + '</h1>';\n if (meta.description) {\n html += '<p class=\"content-description\">' + escapeHtml(meta.description) + '</p>';\n }\n html += '<div class=\"content-meta\">';\n 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>';\n if (meta.category) {\n 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>';\n }\n for (const tag of tags) {\n html += '<span class=\"meta-tag\">#' + escapeHtml(tag) + '</span>';\n }\n html += '</div>';\n html += '</div>';\n\n html += '<div class=\"gallery\">';\n for (const variant of selectedArt.variants) {\n const previewUrl = basePath + '/preview?art=' + encodeURIComponent(selectedArt.path) + '&variant=' + encodeURIComponent(variant.name);\n const escapedPreviewUrl = escapeHtml(previewUrl);\n\n html += '<div class=\"variant-card\">';\n html += '<div class=\"variant-preview\">';\n html += '<iframe src=\"' + escapedPreviewUrl + '\" loading=\"lazy\" title=\"' + escapeHtml(variant.name) + '\"></iframe>';\n html += '</div>';\n html += '<div class=\"variant-info\">';\n html += '<div>';\n html += '<span class=\"variant-name\">' + escapeHtml(variant.name) + '</span>';\n if (variant.isDefault) html += ' <span class=\"variant-badge\">Default</span>';\n html += '</div>';\n html += '<div class=\"variant-actions\">';\n 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>';\n html += '</div>';\n html += '</div>';\n html += '</div>';\n }\n html += '</div>';\n html += '</div>';\n\n content.innerHTML = html;\n\n content.querySelectorAll('.variant-action-btn[data-preview-url]').forEach(button => {\n button.addEventListener('click', () => {\n const previewUrl = button.dataset.previewUrl;\n if (previewUrl) window.open(previewUrl, '_blank', 'noopener');\n });\n });\n }\n\n function escapeHtml(str) {\n if (!str) return '';\n return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"').replace(/'/g, ''');\n }\n\n // Search\n document.getElementById('search').addEventListener('input', (e) => {\n searchQuery = e.target.value;\n renderSidebar();\n });\n\n // Keyboard shortcut for search\n document.addEventListener('keydown', (e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n document.getElementById('search').focus();\n }\n });\n\n loadArts();`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"template-ByMVejqE.mjs","names":[],"sources":["../src/security.ts","../src/gallery/globals.ts","../src/gallery/module.ts","../src/gallery/template.ts"],"sourcesContent":["import { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type { IncomingMessage } from \"node:http\";\nimport path from \"node:path\";\n\nexport const DEFAULT_API_BODY_LIMIT_BYTES = 1024 * 1024;\n\nexport class HttpError extends Error {\n readonly status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.name = \"HttpError\";\n this.status = status;\n }\n}\n\nexport function createDevSessionToken(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\nfunction realpathNearest(targetPath: string): string {\n let current = path.resolve(targetPath);\n const missingParts: string[] = [];\n\n while (true) {\n try {\n const real = fs.realpathSync.native(current);\n return missingParts.length > 0 ? path.join(real, ...missingParts.reverse()) : real;\n } catch {\n const parent = path.dirname(current);\n if (parent === current) {\n return path.resolve(targetPath);\n }\n missingParts.push(path.basename(current));\n current = parent;\n }\n }\n}\n\nfunction isResolvedPathInside(parentDir: string, candidatePath: string): boolean {\n const parent = path.resolve(parentDir);\n const candidate = path.resolve(candidatePath);\n const relative = path.relative(parent, candidate);\n return relative === \"\" || (!relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n\nexport function isPathInside(parentDir: string, candidatePath: string): boolean {\n return isResolvedPathInside(realpathNearest(parentDir), realpathNearest(candidatePath));\n}\n\nexport function isPathInsideAny(parentDirs: string[], candidatePath: string): boolean {\n const candidate = realpathNearest(candidatePath);\n return parentDirs.some((parentDir) =>\n isResolvedPathInside(realpathNearest(parentDir), candidate),\n );\n}\n\nexport function resolveInside(parentDir: string, candidatePath: string, label = \"path\"): string {\n return resolveInsideAny([parentDir], candidatePath, label);\n}\n\nexport function resolveInsideAny(\n parentDirs: string[],\n candidatePath: string,\n label = \"path\",\n): string {\n if (candidatePath.includes(\"\\0\")) {\n throw new HttpError(`${label} contains an invalid character`, 400);\n }\n\n if (parentDirs.length === 0) {\n throw new HttpError(`No allowed directories configured for ${label}`, 500);\n }\n\n const parent = path.resolve(parentDirs[0] ?? \".\");\n const resolved = path.isAbsolute(candidatePath)\n ? path.resolve(candidatePath)\n : path.resolve(parent, candidatePath);\n\n if (!isPathInsideAny(parentDirs, resolved)) {\n throw new HttpError(`${label} escapes the allowed directory`, 400);\n }\n\n return resolved;\n}\n\nexport function resolveUrlPathInside(\n parentDir: string,\n requestUrl: string,\n label = \"path\",\n): string {\n const rawPath = requestUrl.split(/[?#]/, 1)[0] || \"/\";\n let pathname = decodeUrlComponent(rawPath, label);\n\n pathname = pathname.replaceAll(\"\\\\\", \"/\");\n if (pathname.split(\"/\").includes(\"..\")) {\n throw new HttpError(`${label} must not contain parent directory segments`, 400);\n }\n\n const relativePath = `.${pathname}`;\n return resolveInside(parentDir, relativePath, label);\n}\n\nexport function decodeUrlComponent(value: string, label = \"path\"): string {\n try {\n return decodeURIComponent(value);\n } catch {\n throw new HttpError(`${label} is not valid URL encoding`, 400);\n }\n}\n\nexport function collectRequestBody(\n req: IncomingMessage,\n limit = DEFAULT_API_BODY_LIMIT_BYTES,\n): Promise<string> {\n return new Promise((resolve, reject) => {\n let body = \"\";\n let size = 0;\n let completed = false;\n\n req.on(\"data\", (chunk: Buffer | string) => {\n if (completed) return;\n\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n size += buffer.byteLength;\n if (size > limit) {\n completed = true;\n reject(new HttpError(`Request body exceeds ${limit} bytes`, 413));\n return;\n }\n\n body += buffer.toString(\"utf-8\");\n });\n\n req.on(\"end\", () => {\n if (!completed) {\n completed = true;\n resolve(body);\n }\n });\n\n req.on(\"error\", (error) => {\n if (!completed) {\n completed = true;\n reject(error);\n }\n });\n });\n}\n\nexport function parseJsonBody<T = unknown>(body: string): T {\n try {\n return JSON.parse(body) as T;\n } catch (error) {\n if (error instanceof SyntaxError) {\n throw new HttpError(\"Malformed JSON body\", 400);\n }\n throw error;\n }\n}\n\nexport function validateDevApiRequest(\n req: IncomingMessage,\n sessionToken: string,\n): HttpError | null {\n const originError = validateOrigin(req);\n if (originError) return originError;\n\n if (!isUnsafeMethod(req.method)) {\n return null;\n }\n\n if (!hasValidSessionToken(req, sessionToken)) {\n return new HttpError(\"Invalid Musea dev session token\", 403);\n }\n\n if (!isJsonRequest(req)) {\n return new HttpError(\"Content-Type must be application/json\", 415);\n }\n\n return null;\n}\n\nexport function serializeScriptValue(value: unknown): string {\n return (JSON.stringify(value) ?? \"undefined\").replace(/[<>&\\u2028\\u2029]/g, (char) => {\n switch (char) {\n case \"<\":\n return \"\\\\u003C\";\n case \">\":\n return \"\\\\u003E\";\n case \"&\":\n return \"\\\\u0026\";\n case \"\\u2028\":\n return \"\\\\u2028\";\n case \"\\u2029\":\n return \"\\\\u2029\";\n default:\n return char;\n }\n });\n}\n\nfunction isUnsafeMethod(method: string | undefined): boolean {\n return method === \"POST\" || method === \"PUT\" || method === \"PATCH\" || method === \"DELETE\";\n}\n\nfunction isJsonRequest(req: IncomingMessage): boolean {\n const contentType = getHeader(req, \"content-type\");\n return contentType?.split(\";\")[0]?.trim().toLowerCase() === \"application/json\";\n}\n\nfunction validateOrigin(req: IncomingMessage): HttpError | null {\n const secFetchSite = getHeader(req, \"sec-fetch-site\");\n if (secFetchSite === \"cross-site\") {\n return new HttpError(\"Cross-origin Musea API requests are not allowed\", 403);\n }\n\n const origin = getHeader(req, \"origin\");\n if (!origin) return null;\n\n const host = getHeader(req, \"host\");\n if (!host) {\n return new HttpError(\"Missing Host header\", 400);\n }\n\n try {\n const originUrl = new URL(origin);\n if (originUrl.host !== host) {\n return new HttpError(\"Cross-origin Musea API requests are not allowed\", 403);\n }\n } catch {\n return new HttpError(\"Invalid Origin header\", 400);\n }\n\n return null;\n}\n\nfunction hasValidSessionToken(req: IncomingMessage, expectedToken: string): boolean {\n const actualToken = getHeader(req, \"x-musea-session\");\n if (!actualToken) return false;\n\n const actual = Buffer.from(actualToken);\n const expected = Buffer.from(expectedToken);\n return actual.length === expected.length && timingSafeEqual(actual, expected);\n}\n\nfunction getHeader(req: IncomingMessage, name: string): string | undefined {\n const value = req.headers[name];\n if (Array.isArray(value)) return value[0];\n return value;\n}\n","import { serializeScriptValue } from \"../security.js\";\nimport type { MuseaTokenPreviewConfig } from \"../tokens/preview.js\";\n\nexport interface MuseaGalleryGlobals {\n basePath: string;\n devSessionToken?: string;\n staticPreviews?: Record<string, Record<string, string>>;\n themeConfig?: { default: string; custom?: Record<string, unknown> };\n tokenPreviewConfig?: MuseaTokenPreviewConfig;\n}\n\nexport function generateGalleryGlobalsScript(globals: MuseaGalleryGlobals): string {\n const parts = [`window.__MUSEA_BASE_PATH__=${serializeScriptValue(globals.basePath)};`];\n if (globals.devSessionToken !== undefined) {\n parts.push(`window.__MUSEA_SESSION_TOKEN__=${serializeScriptValue(globals.devSessionToken)};`);\n }\n if (globals.staticPreviews !== undefined) {\n parts.push(\"window.__MUSEA_STATIC__=true;\");\n parts.push(`window.__MUSEA_STATIC_PREVIEWS__=${serializeScriptValue(globals.staticPreviews)};`);\n }\n if (globals.themeConfig) {\n parts.push(`window.__MUSEA_THEME_CONFIG__=${serializeScriptValue(globals.themeConfig)};`);\n }\n if (globals.tokenPreviewConfig) {\n parts.push(\n `window.__MUSEA_TOKEN_PREVIEWS__=${serializeScriptValue(globals.tokenPreviewConfig)};`,\n );\n }\n return parts.join(\"\");\n}\n\nexport function generateDevGlobalsScript(\n basePath: string,\n devSessionToken: string,\n themeConfig?: MuseaGalleryGlobals[\"themeConfig\"],\n tokenPreviewConfig?: MuseaTokenPreviewConfig,\n): string {\n return generateGalleryGlobalsScript({\n basePath,\n devSessionToken,\n themeConfig,\n tokenPreviewConfig,\n });\n}\n","import { serializeScriptValue } from \"../security.js\";\n\nexport function generateGalleryModule(basePath: string): string {\n return `\nexport const basePath = ${serializeScriptValue(basePath)};\nexport async function loadArts() {\n const res = await fetch(basePath + '/api/arts');\n return res.json();\n}\n`;\n}\n","/**\n * HTML structure and inline JS generation for the Musea gallery.\n *\n * Extracted from gallery.ts to keep file sizes manageable.\n */\n\nimport { serializeScriptValue } from \"../security.js\";\n\nfunction escapeHtmlAttribute(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => {\n switch (char) {\n case \"&\":\n return \"&\";\n case \"<\":\n return \"<\";\n case \">\":\n return \">\";\n case '\"':\n return \""\";\n case \"'\":\n return \"'\";\n default:\n return char;\n }\n });\n}\n\n/**\n * Generate the gallery HTML body (header, sidebar, content, and inline script).\n */\nexport function generateGalleryBody(basePath: string): string {\n const escapedBasePath = escapeHtmlAttribute(basePath);\n\n return `\n <header class=\"header\">\n <div class=\"header-left\">\n <a href=\"${escapedBasePath}\" class=\"logo\">\n <svg class=\"logo-svg\" width=\"32\" height=\"32\" viewBox=\"0 0 200 200\" fill=\"none\">\n <g transform=\"translate(30, 25) scale(1.2)\">\n <g transform=\"translate(15, 10) skewX(-15)\">\n <path d=\"M 65 0 L 40 60 L 70 20 L 65 0 Z\" fill=\"currentColor\"/>\n <path d=\"M 20 0 L 40 60 L 53 13 L 20 0 Z\" fill=\"currentColor\"/>\n </g>\n </g>\n <g transform=\"translate(110, 120)\">\n <line x1=\"5\" y1=\"10\" x2=\"5\" y2=\"50\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\"/>\n <line x1=\"60\" y1=\"10\" x2=\"60\" y2=\"50\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\"/>\n <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\"/>\n <rect x=\"15\" y=\"18\" width=\"14\" height=\"12\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" opacity=\"0.7\"/>\n <rect x=\"36\" y=\"18\" width=\"14\" height=\"12\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" opacity=\"0.7\"/>\n <rect x=\"23\" y=\"35\" width=\"18\" height=\"12\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" opacity=\"0.6\"/>\n </g>\n </svg>\n Musea\n </a>\n <span class=\"header-subtitle\">Component Gallery</span>\n </div>\n <div class=\"search-container\">\n <svg class=\"search-icon\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.35-4.35\"/>\n </svg>\n <input type=\"text\" class=\"search-input\" placeholder=\"Search components...\" id=\"search\">\n </div>\n </header>\n\n <main class=\"main\">\n <aside class=\"sidebar\" id=\"sidebar\">\n <div class=\"loading\">\n <div class=\"loading-spinner\"></div>\n Loading...\n </div>\n </aside>\n <section class=\"content\" id=\"content\">\n <div class=\"empty-state\">\n <div class=\"empty-state-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <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\"/>\n <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\"/>\n <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\"/>\n </svg>\n </div>\n <div class=\"empty-state-title\">Select a component</div>\n <div class=\"empty-state-text\">Choose a component from the sidebar to view its variants and documentation</div>\n </div>\n </section>\n </main>`;\n}\n\n/**\n * Generate the gallery inline script (SPA logic).\n */\nexport function generateGalleryScript(basePath: string): string {\n return `\n const basePath = ${serializeScriptValue(basePath)};\n let arts = [];\n let selectedArt = null;\n let searchQuery = '';\n\n async function loadArts() {\n try {\n const res = await fetch(basePath + '/api/arts');\n arts = await res.json();\n renderSidebar();\n } catch (e) {\n console.error('Failed to load arts:', e);\n document.getElementById('sidebar').innerHTML = '<div class=\"loading\">Failed to load</div>';\n }\n }\n\n function renderSidebar() {\n const sidebar = document.getElementById('sidebar');\n const categories = {};\n\n const filtered = searchQuery\n ? arts.filter(a => a.metadata.title.toLowerCase().includes(searchQuery.toLowerCase()))\n : arts;\n\n for (const art of filtered) {\n const cat = art.metadata.category || 'Components';\n if (!categories[cat]) categories[cat] = [];\n categories[cat].push(art);\n }\n\n if (Object.keys(categories).length === 0) {\n sidebar.innerHTML = '<div class=\"sidebar-section\"><div class=\"loading\">No components found</div></div>';\n return;\n }\n\n let html = '';\n for (const [category, items] of Object.entries(categories)) {\n const escapedCategory = escapeHtml(category);\n html += '<div class=\"sidebar-section\">';\n html += '<div class=\"category-header\" data-category=\"' + escapedCategory + '\">';\n 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>';\n html += '<span>' + escapedCategory + '</span>';\n html += '<span class=\"category-count\">' + items.length + '</span>';\n html += '</div>';\n html += '<ul class=\"art-list\" data-category=\"' + escapedCategory + '\">';\n for (const art of items) {\n const active = selectedArt?.path === art.path ? 'active' : '';\n const variantCount = art.variants?.length || 0;\n html += '<li class=\"art-item ' + active + '\" data-path=\"' + escapeHtml(art.path) + '\">';\n html += '<span>' + escapeHtml(art.metadata.title) + '</span>';\n html += '<span class=\"art-variant-count\">' + variantCount + ' variant' + (variantCount !== 1 ? 's' : '') + '</span>';\n html += '</li>';\n }\n html += '</ul>';\n html += '</div>';\n }\n\n sidebar.innerHTML = html;\n\n sidebar.querySelectorAll('.art-item').forEach(item => {\n item.addEventListener('click', () => {\n const artPath = item.dataset.path;\n selectedArt = arts.find(a => a.path === artPath);\n renderSidebar();\n renderContent();\n });\n });\n\n sidebar.querySelectorAll('.category-header').forEach(header => {\n header.addEventListener('click', () => {\n header.classList.toggle('collapsed');\n const list = header.parentElement?.querySelector('.art-list');\n if (list) list.style.display = header.classList.contains('collapsed') ? 'none' : 'block';\n });\n });\n }\n\n function renderContent() {\n const content = document.getElementById('content');\n if (!selectedArt) {\n content.innerHTML = \\`\n <div class=\"empty-state\">\n <div class=\"empty-state-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <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\"/>\n <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\"/>\n <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\"/>\n </svg>\n </div>\n <div class=\"empty-state-title\">Select a component</div>\n <div class=\"empty-state-text\">Choose a component from the sidebar to view its variants</div>\n </div>\n \\`;\n return;\n }\n\n const meta = selectedArt.metadata;\n const tags = meta.tags || [];\n const variantCount = selectedArt.variants?.length || 0;\n\n let html = '<div class=\"content-inner\">';\n html += '<div class=\"content-header\">';\n html += '<h1 class=\"content-title\">' + escapeHtml(meta.title) + '</h1>';\n if (meta.description) {\n html += '<p class=\"content-description\">' + escapeHtml(meta.description) + '</p>';\n }\n html += '<div class=\"content-meta\">';\n 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>';\n if (meta.category) {\n 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>';\n }\n for (const tag of tags) {\n html += '<span class=\"meta-tag\">#' + escapeHtml(tag) + '</span>';\n }\n html += '</div>';\n html += '</div>';\n\n html += '<div class=\"gallery\">';\n for (const variant of selectedArt.variants) {\n const previewUrl = basePath + '/preview?art=' + encodeURIComponent(selectedArt.path) + '&variant=' + encodeURIComponent(variant.name);\n const escapedPreviewUrl = escapeHtml(previewUrl);\n\n html += '<div class=\"variant-card\">';\n html += '<div class=\"variant-preview\">';\n html += '<iframe src=\"' + escapedPreviewUrl + '\" loading=\"lazy\" title=\"' + escapeHtml(variant.name) + '\"></iframe>';\n html += '</div>';\n html += '<div class=\"variant-info\">';\n html += '<div>';\n html += '<span class=\"variant-name\">' + escapeHtml(variant.name) + '</span>';\n if (variant.isDefault) html += ' <span class=\"variant-badge\">Default</span>';\n html += '</div>';\n html += '<div class=\"variant-actions\">';\n 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>';\n html += '</div>';\n html += '</div>';\n html += '</div>';\n }\n html += '</div>';\n html += '</div>';\n\n content.innerHTML = html;\n\n content.querySelectorAll('.variant-action-btn[data-preview-url]').forEach(button => {\n button.addEventListener('click', () => {\n const previewUrl = button.dataset.previewUrl;\n if (previewUrl) window.open(previewUrl, '_blank', 'noopener');\n });\n });\n }\n\n function escapeHtml(str) {\n if (!str) return '';\n return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"').replace(/'/g, ''');\n }\n\n // Search\n document.getElementById('search').addEventListener('input', (e) => {\n searchQuery = e.target.value;\n renderSidebar();\n });\n\n // Keyboard shortcut for search\n document.addEventListener('keydown', (e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n document.getElementById('search').focus();\n }\n });\n\n loadArts();`;\n}\n"],"mappings":"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"}
|
|
@@ -288,4 +288,4 @@ declare function generateVrtReport(results: VrtResult[], summary: VrtSummary): s
|
|
|
288
288
|
declare function generateVrtJsonReport(results: VrtResult[], summary: VrtSummary): string;
|
|
289
289
|
//#endregion
|
|
290
290
|
export { PixelCompareOptions as a, ArtFileInfo as c, CsfOutput as d, CaptureConfig as f, VrtOptions as g, ViewportConfig as h, ExtendedVrtOptions as i, ArtMetadata as l, ComparisonConfig as m, generateVrtReport as n, VrtResult as o, CiConfig as p, MuseaVrtRunner as r, VrtSummary as s, generateVrtJsonReport as t, ArtVariant as u };
|
|
291
|
-
//# sourceMappingURL=vrt-
|
|
291
|
+
//# sourceMappingURL=vrt-D454uyH6.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vrt-D454uyH6.d.mts","names":[],"sources":["../src/types/vrt.ts","../src/types/art.ts","../src/vrt/types.ts","../src/vrt/runner.ts","../src/vrt/report.ts"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vrt-Cv1PK1EF.mjs","names":[],"sources":["../src/vrt/comparison.ts","../src/vrt/utils.ts","../src/vrt/runner-comparison.ts","../src/vrt/runner.ts","../src/vrt/report.ts","../src/vrt.ts"],"sourcesContent":["/**\n * Pixel comparison utilities for VRT.\n *\n * Provides PNG reading/writing, color delta calculation using YIQ color space,\n * and anti-aliasing detection for pixel-level image comparison.\n */\n\nimport fs from \"node:fs\";\nimport { PNG } from \"pngjs\";\n\n/**\n * Read PNG file and return PNG object.\n */\nexport async function readPng(filepath: string): Promise<PNG> {\n return new Promise((resolve, reject) => {\n fs.createReadStream(filepath)\n .pipe(new PNG())\n .on(\"parsed\", function (this: PNG) {\n resolve(this);\n })\n .on(\"error\", reject);\n });\n}\n\n/**\n * Write PNG object to file.\n */\nexport async function writePng(png: PNG, filepath: string): Promise<void> {\n return new Promise((resolve, reject) => {\n png.pack().pipe(fs.createWriteStream(filepath)).on(\"finish\", resolve).on(\"error\", reject);\n });\n}\n\n/**\n * Calculate color delta using YIQ color space.\n */\nexport function colorDelta(\n r1: number,\n g1: number,\n b1: number,\n a1: number,\n r2: number,\n g2: number,\n b2: number,\n a2: number,\n): number {\n if (a1 !== 255) {\n r1 = blend(r1, 255, a1 / 255);\n g1 = blend(g1, 255, a1 / 255);\n b1 = blend(b1, 255, a1 / 255);\n }\n if (a2 !== 255) {\n r2 = blend(r2, 255, a2 / 255);\n g2 = blend(g2, 255, a2 / 255);\n b2 = blend(b2, 255, a2 / 255);\n }\n\n const y1 = r1 * 0.29889531 + g1 * 0.58662247 + b1 * 0.11448223;\n const i1 = r1 * 0.59597799 - g1 * 0.2741761 - b1 * 0.32180189;\n const q1 = r1 * 0.21147017 - g1 * 0.52261711 + b1 * 0.31114694;\n\n const y2 = r2 * 0.29889531 + g2 * 0.58662247 + b2 * 0.11448223;\n const i2 = r2 * 0.59597799 - g2 * 0.2741761 - b2 * 0.32180189;\n const q2 = r2 * 0.21147017 - g2 * 0.52261711 + b2 * 0.31114694;\n\n const dy = y1 - y2;\n const di = i1 - i2;\n const dq = q1 - q2;\n\n return dy * dy * 0.5053 + di * di * 0.299 + dq * dq * 0.1957;\n}\n\n/**\n * Blend foreground with background using alpha.\n */\nfunction blend(fg: number, bg: number, alpha: number): number {\n return bg + (fg - bg) * alpha;\n}\n\n/**\n * Check if file exists.\n */\nexport async function fileExists(filepath: string): Promise<boolean> {\n try {\n await fs.promises.access(filepath);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Simple anti-aliasing detection.\n * A pixel is likely anti-aliased if its neighbors have high contrast in opposite directions.\n */\nexport function isAntiAliased(\n img1: PNG,\n img2: PNG,\n x: number,\n y: number,\n width: number,\n height: number,\n): boolean {\n const minX = Math.max(0, x - 1);\n const maxX = Math.min(width - 1, x + 1);\n const minY = Math.max(0, y - 1);\n const maxY = Math.min(height - 1, y + 1);\n\n let zeroes = 0;\n let positives = 0;\n let negatives = 0;\n\n for (let ny = minY; ny <= maxY; ny++) {\n for (let nx = minX; nx <= maxX; nx++) {\n if (nx === x && ny === y) continue;\n const idx = (ny * width + nx) * 4;\n\n const delta = colorDelta(\n img1.data[idx],\n img1.data[idx + 1],\n img1.data[idx + 2],\n img1.data[idx + 3],\n img2.data[idx],\n img2.data[idx + 1],\n img2.data[idx + 2],\n img2.data[idx + 3],\n );\n\n if (delta === 0) {\n zeroes++;\n } else if (delta > 0) {\n positives++;\n } else {\n negatives++;\n }\n }\n }\n\n // If neighbors are mixed (some match, some differ), it's likely AA\n return zeroes > 0 && (positives > 0 || negatives > 0) && positives + negatives < 4;\n}\n\n/**\n * Simple glob matching for pattern-based filtering.\n */\nexport function matchGlob(filepath: string, pattern: string): boolean {\n const regex = pattern\n .replace(/\\./g, \"\\\\.\")\n .replace(/\\*\\*/g, \".*\")\n .replace(/\\*(?!\\*)/g, \"[^/]*\");\n return new RegExp(`^${regex}$`).test(filepath);\n}\n\n/**\n * Escape HTML special characters.\n */\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","/**\n * Utility functions for VRT runner.\n *\n * Includes summary generation, URL builders, and helpers\n * used across the runner modules.\n */\n\nimport type { VrtResult, VrtSummary } from \"./types.js\";\n\n/**\n * Build URL for variant preview.\n */\nexport function buildVariantUrl(baseUrl: string, artPath: string, variantName: string): string {\n const encodedPath = encodeURIComponent(artPath);\n const encodedVariant = encodeURIComponent(variantName);\n return `${baseUrl}/__musea__/preview?art=${encodedPath}&variant=${encodedVariant}`;\n}\n\n/**\n * Compute VRT summary statistics from a list of results.\n */\nexport function computeSummary(results: VrtResult[], startTime: number): VrtSummary {\n return {\n total: results.length,\n passed: results.filter((r) => r.passed && !r.isNew).length,\n failed: results.filter((r) => !r.passed && !r.error).length,\n new: results.filter((r) => r.isNew).length,\n skipped: results.filter((r) => r.error).length,\n duration: Date.now() - startTime,\n };\n}\n","/**\n * Screenshot capture and pixel comparison logic for VRT.\n *\n * Contains the captureAndCompare flow (browser context, navigation,\n * element hiding/masking, screenshot, baseline check) and the\n * pixel-level compareImages implementation.\n */\n\nimport type { BrowserContext, Page } from \"playwright\";\nimport type { ArtFileInfo, ViewportConfig } from \"../types/index.js\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { PNG } from \"pngjs\";\n\nimport { readPng, writePng, colorDelta, isAntiAliased, fileExists } from \"./comparison.js\";\nimport type { VrtResult } from \"./types.js\";\nimport type { MuseaVrtRunner } from \"./runner.js\";\nimport { buildVariantUrl } from \"./utils.js\";\n\n/**\n * Capture screenshot and compare with baseline.\n *\n * Standalone function that operates on a MuseaVrtRunner instance.\n */\nexport async function captureAndCompare(\n runner: MuseaVrtRunner,\n art: ArtFileInfo,\n variantName: string,\n viewport: ViewportConfig,\n baseUrl: string,\n): Promise<VrtResult> {\n const browser = runner.getBrowser();\n if (!browser) {\n throw new Error(\"VRT runner not initialized. Call init() first.\");\n }\n\n const options = runner.getOptions();\n const capture = runner.getCapture();\n const comparison = runner.getComparison();\n\n const snapshotDir = options.snapshotDir;\n const artBaseName = path.basename(art.path, \".art.vue\");\n const viewportName = viewport.name || `${viewport.width}x${viewport.height}`;\n const snapshotName = `${artBaseName}--${variantName}--${viewportName}.png`;\n const snapshotPath = path.join(snapshotDir, snapshotName);\n const currentPath = path.join(snapshotDir, \"current\", snapshotName);\n const diffPath = path.join(snapshotDir, \"diff\", snapshotName);\n\n // Ensure directories exist\n await fs.promises.mkdir(path.dirname(snapshotPath), { recursive: true });\n await fs.promises.mkdir(path.join(snapshotDir, \"current\"), { recursive: true });\n await fs.promises.mkdir(path.join(snapshotDir, \"diff\"), { recursive: true });\n\n let context: BrowserContext | null = null;\n let page: Page | null = null;\n\n try {\n context = await browser.newContext({\n viewport: {\n width: viewport.width,\n height: viewport.height,\n },\n deviceScaleFactor: viewport.deviceScaleFactor ?? 1,\n });\n page = await context.newPage();\n\n // Navigate to variant preview URL\n const variantUrl = buildVariantUrl(baseUrl, art.path, variantName);\n const waitUntil = capture.waitForNetwork ? (\"networkidle\" as const) : (\"load\" as const);\n await page.goto(variantUrl, { waitUntil });\n\n // Wait for content to render\n await page.waitForSelector(capture.waitSelector, { timeout: 10000 });\n\n // Additional wait for animations to settle\n await page.waitForTimeout(capture.settleTime);\n\n // Hide elements before capture\n if (capture.hideElements.length > 0) {\n for (const selector of capture.hideElements) {\n await page.evaluate((sel) => {\n document.querySelectorAll(sel).forEach((el) => {\n (el as HTMLElement).style.visibility = \"hidden\";\n });\n }, selector);\n }\n }\n\n // Mask elements before capture (replace with colored box)\n if (capture.maskElements.length > 0) {\n for (const selector of capture.maskElements) {\n await page.evaluate((sel) => {\n document.querySelectorAll(sel).forEach((el) => {\n const htmlEl = el as HTMLElement;\n htmlEl.style.background = \"#ff00ff\";\n htmlEl.style.color = \"transparent\";\n htmlEl.innerHTML = \"\";\n });\n }, selector);\n }\n }\n\n // Take screenshot\n await page.screenshot({\n path: currentPath,\n fullPage: capture.fullPage,\n });\n\n // Check if baseline exists\n const hasBaseline = await fileExists(snapshotPath);\n\n if (!hasBaseline) {\n // First run - save as baseline\n await fs.promises.copyFile(currentPath, snapshotPath);\n return {\n artPath: art.path,\n variantName,\n viewport,\n passed: true,\n snapshotPath,\n currentPath,\n isNew: true,\n };\n }\n\n // Compare images using pixel comparison\n const comparisonResult = await compareImages(snapshotPath, currentPath, diffPath, comparison);\n\n const passed = comparisonResult.diffPercentage <= options.threshold;\n\n return {\n artPath: art.path,\n variantName,\n viewport,\n passed,\n snapshotPath,\n currentPath,\n diffPath: passed ? undefined : diffPath,\n diffPercentage: comparisonResult.diffPercentage,\n diffPixels: comparisonResult.diffPixels,\n totalPixels: comparisonResult.totalPixels,\n };\n } catch (error) {\n return {\n artPath: art.path,\n variantName,\n viewport,\n passed: false,\n snapshotPath,\n error: error instanceof Error ? error.message : String(error),\n };\n } finally {\n if (page) await page.close();\n if (context) await context.close();\n }\n}\n\n/**\n * Compare two PNG images and generate a diff image.\n * Returns pixel difference statistics.\n */\nexport async function compareImages(\n baselinePath: string,\n currentPath: string,\n diffPath: string,\n comparison: {\n antiAliasing?: boolean;\n alpha?: boolean;\n diffColor?: { r: number; g: number; b: number };\n },\n): Promise<{ diffPixels: number; totalPixels: number; diffPercentage: number }> {\n const baseline = await readPng(baselinePath);\n const current = await readPng(currentPath);\n\n // Handle size mismatch\n if (baseline.width !== current.width || baseline.height !== current.height) {\n const width = Math.max(baseline.width, current.width);\n const height = Math.max(baseline.height, current.height);\n const diff = new PNG({ width, height });\n\n // Fill with red to indicate size mismatch\n for (let i = 0; i < diff.data.length; i += 4) {\n diff.data[i] = 255; // R\n diff.data[i + 1] = 0; // G\n diff.data[i + 2] = 0; // B\n diff.data[i + 3] = 255; // A\n }\n\n await writePng(diff, diffPath);\n\n return {\n diffPixels: width * height,\n totalPixels: width * height,\n diffPercentage: 100,\n };\n }\n\n const width = baseline.width;\n const height = baseline.height;\n const totalPixels = width * height;\n const diff = new PNG({ width, height });\n\n const useAntiAliasing = comparison.antiAliasing ?? true;\n const useAlpha = comparison.alpha ?? true;\n const diffColor = comparison.diffColor ?? { r: 255, g: 0, b: 0 };\n\n // Pixel comparison\n let diffPixels = 0;\n const threshold = 0.1; // Color difference threshold\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = (y * width + x) * 4;\n\n const r1 = baseline.data[idx];\n const g1 = baseline.data[idx + 1];\n const b1 = baseline.data[idx + 2];\n const a1 = useAlpha ? baseline.data[idx + 3] : 255;\n\n const r2 = current.data[idx];\n const g2 = current.data[idx + 1];\n const b2 = current.data[idx + 2];\n const a2 = useAlpha ? current.data[idx + 3] : 255;\n\n // Calculate color difference using YIQ color space\n const delta = colorDelta(r1, g1, b1, a1, r2, g2, b2, a2);\n\n if (delta > threshold * 255 * 255) {\n // Anti-aliasing detection: check if pixel is likely AA\n if (useAntiAliasing && isAntiAliased(baseline, current, x, y, width, height)) {\n // Mark as AA (yellow)\n diff.data[idx] = 255;\n diff.data[idx + 1] = 200;\n diff.data[idx + 2] = 0;\n diff.data[idx + 3] = 128;\n } else {\n // Mark as different\n diffPixels++;\n diff.data[idx] = diffColor.r;\n diff.data[idx + 1] = diffColor.g;\n diff.data[idx + 2] = diffColor.b;\n diff.data[idx + 3] = 255;\n }\n } else {\n // Grayscale for matching pixels\n const gray = Math.round((r2 + g2 + b2) / 3);\n diff.data[idx] = gray;\n diff.data[idx + 1] = gray;\n diff.data[idx + 2] = gray;\n diff.data[idx + 3] = 128; // Semi-transparent\n }\n }\n }\n\n // Only write diff if there are differences\n if (diffPixels > 0) {\n await writePng(diff, diffPath);\n }\n\n const diffPercentage = (diffPixels / totalPixels) * 100;\n\n return {\n diffPixels,\n totalPixels,\n diffPercentage,\n };\n}\n","/**\n * VRT runner using Playwright for browser automation.\n *\n * Manages browser lifecycle, screenshot capture, and baseline comparison\n * for visual regression testing of Musea art file variants.\n */\n\nimport type { Browser, BrowserContext, Page } from \"playwright\";\nimport type {\n ArtFileInfo,\n VrtOptions,\n ViewportConfig,\n CaptureConfig,\n ComparisonConfig,\n CiConfig,\n} from \"../types/index.js\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { fileExists, matchGlob } from \"./comparison.js\";\nimport { captureAndCompare } from \"./runner-comparison.js\";\nimport { computeSummary } from \"./utils.js\";\n\nexport type { VrtResult, VrtSummary, ExtendedVrtOptions, PixelCompareOptions } from \"./types.js\";\n\nimport type { VrtResult, VrtSummary, ExtendedVrtOptions } from \"./types.js\";\n\n/**\n * VRT runner using Playwright.\n */\nexport class MuseaVrtRunner {\n private options: Required<VrtOptions>;\n private capture: Required<CaptureConfig>;\n private comparison: ComparisonConfig;\n private ci: CiConfig;\n private browser: Browser | null = null;\n private startTime: number = 0;\n\n constructor(options: ExtendedVrtOptions = {}) {\n this.options = {\n snapshotDir: options.snapshotDir ?? \".vize/snapshots\",\n threshold: options.threshold ?? 0.1,\n viewports: options.viewports ?? [\n { width: 1280, height: 720, name: \"desktop\" },\n { width: 375, height: 667, name: \"mobile\" },\n ],\n };\n this.capture = {\n fullPage: options.capture?.fullPage ?? false,\n waitForNetwork: options.capture?.waitForNetwork ?? true,\n settleTime: options.capture?.settleTime ?? 100,\n waitSelector: options.capture?.waitSelector ?? \".musea-variant\",\n hideElements: options.capture?.hideElements ?? [],\n maskElements: options.capture?.maskElements ?? [],\n };\n this.comparison = options.comparison ?? {};\n this.ci = options.ci ?? {};\n }\n\n // --- Internal accessors used by runner-comparison ---\n\n /** @internal */\n getBrowser(): Browser | null {\n return this.browser;\n }\n\n /** @internal */\n getOptions(): Required<VrtOptions> {\n return this.options;\n }\n\n /** @internal */\n getCapture(): Required<CaptureConfig> {\n return this.capture;\n }\n\n /** @internal */\n getComparison(): ComparisonConfig {\n return this.comparison;\n }\n\n /**\n * Initialize Playwright browser.\n */\n async init(): Promise<void> {\n const { chromium } = await import(\"playwright\");\n this.browser = await chromium.launch({ headless: true });\n this.startTime = Date.now();\n }\n\n /**\n * Close browser and cleanup.\n */\n async close(): Promise<void> {\n if (this.browser) {\n await this.browser.close();\n this.browser = null;\n }\n }\n\n /**\n * Alias for init() - used by the plugin API.\n */\n async start(): Promise<void> {\n return this.init();\n }\n\n /**\n * Alias for close() - used by the plugin API.\n */\n async stop(): Promise<void> {\n return this.close();\n }\n\n /**\n * Run VRT tests for all Art files.\n */\n async runAllTests(artFiles: ArtFileInfo[], baseUrl: string): Promise<VrtResult[]> {\n if (!this.browser) {\n throw new Error(\"VRT runner not initialized. Call init() first.\");\n }\n\n const results: VrtResult[] = [];\n const retries = this.ci.retries ?? 0;\n\n for (const art of artFiles) {\n for (const variant of art.variants) {\n if (variant.skipVrt) {\n continue;\n }\n\n // Determine viewports: use per-variant viewport if defined, else global viewports\n const viewports = variant.args?.viewport\n ? [variant.args.viewport as ViewportConfig]\n : this.options.viewports;\n\n for (const viewport of viewports) {\n let result: VrtResult | null = null;\n let attempts = 0;\n\n while (attempts <= retries) {\n result = await this.captureAndCompare(art, variant.name, viewport, baseUrl);\n if (result.passed || result.isNew || !result.error) {\n break;\n }\n attempts++;\n if (attempts <= retries) {\n console.log(\n `[vrt] Retry ${attempts}/${retries}: ${path.basename(art.path)}/${variant.name}`,\n );\n }\n }\n\n if (result) {\n results.push(result);\n }\n }\n }\n }\n\n return results;\n }\n\n /**\n * Run VRT tests - alias used by the plugin API that accepts options.\n */\n async runTests(\n artFiles: ArtFileInfo[],\n baseUrl: string,\n _options?: { updateSnapshots?: boolean },\n ): Promise<VrtResult[]> {\n const results = await this.runAllTests(artFiles, baseUrl);\n if (_options?.updateSnapshots) {\n await this.updateBaselines(results);\n }\n return results;\n }\n\n /**\n * Capture screenshot and compare with baseline.\n */\n async captureAndCompare(\n art: ArtFileInfo,\n variantName: string,\n viewport: ViewportConfig,\n baseUrl: string,\n ): Promise<VrtResult> {\n return captureAndCompare(this, art, variantName, viewport, baseUrl);\n }\n\n /**\n * Get the Playwright Page for external use (e.g., a11y auditing).\n */\n async createPage(viewport: ViewportConfig): Promise<{ page: Page; context: BrowserContext }> {\n if (!this.browser) {\n throw new Error(\"VRT runner not initialized. Call init() first.\");\n }\n const context = await this.browser.newContext({\n viewport: { width: viewport.width, height: viewport.height },\n deviceScaleFactor: viewport.deviceScaleFactor ?? 1,\n });\n const page = await context.newPage();\n return { page, context };\n }\n\n /**\n * Update baseline snapshots with current screenshots.\n */\n async updateBaselines(results: VrtResult[]): Promise<number> {\n let updated = 0;\n const snapshotDir = this.options.snapshotDir;\n const currentDir = path.join(snapshotDir, \"current\");\n\n for (const result of results) {\n const currentPath = path.join(currentDir, path.basename(result.snapshotPath));\n\n if (await fileExists(currentPath)) {\n await fs.promises.copyFile(currentPath, result.snapshotPath);\n updated++;\n console.log(`[vrt] Updated: ${path.basename(result.snapshotPath)}`);\n }\n }\n\n return updated;\n }\n\n /**\n * Approve specific failed results (update their baselines).\n */\n async approveResults(results: VrtResult[], pattern?: string): Promise<number> {\n const toApprove = pattern\n ? results.filter((r) => {\n const name = `${path.basename(r.artPath, \".art.vue\")}/${r.variantName}`;\n return name.includes(pattern) || matchGlob(name, pattern);\n })\n : results.filter((r) => !r.passed && !r.error);\n\n return this.updateBaselines(toApprove);\n }\n\n /**\n * Clean orphaned snapshots (no corresponding art/variant).\n */\n async cleanOrphans(artFiles: ArtFileInfo[]): Promise<number> {\n const snapshotDir = this.options.snapshotDir;\n let cleaned = 0;\n\n try {\n const files = await fs.promises.readdir(snapshotDir);\n const validNames = new Set<string>();\n\n for (const art of artFiles) {\n const artBaseName = path.basename(art.path, \".art.vue\");\n for (const variant of art.variants) {\n if (variant.skipVrt) continue;\n for (const viewport of this.options.viewports) {\n const viewportName = viewport.name || `${viewport.width}x${viewport.height}`;\n validNames.add(`${artBaseName}--${variant.name}--${viewportName}.png`);\n }\n }\n }\n\n for (const file of files) {\n if (file.endsWith(\".png\") && !validNames.has(file)) {\n await fs.promises.unlink(path.join(snapshotDir, file));\n cleaned++;\n console.log(`[vrt] Cleaned: ${file}`);\n }\n }\n } catch {\n // Directory may not exist yet\n }\n\n return cleaned;\n }\n\n /**\n * Get VRT summary statistics.\n */\n getSummary(results: VrtResult[]): VrtSummary {\n return computeSummary(results, this.startTime);\n }\n}\n","/**\n * VRT report generation for Musea.\n *\n * Generates HTML and JSON reports from VRT results for visual review\n * and CI integration.\n */\n\nimport path from \"node:path\";\n\nimport type { VrtResult, VrtSummary } from \"./types.js\";\nimport { escapeHtml } from \"./comparison.js\";\n\n/**\n * Generate VRT report in HTML format.\n * Supports side-by-side, overlay, and slider comparison modes.\n */\nexport function generateVrtReport(results: VrtResult[], summary: VrtSummary): string {\n const formatDuration = (ms: number): string => {\n if (ms < 1000) return `${ms}ms`;\n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n if (minutes === 0) return `${seconds}s`;\n return `${minutes}m ${seconds % 60}s`;\n };\n\n const timestamp = new Date().toLocaleString(\"ja-JP\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n\n const html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>VRT Report - Musea</title>\n <style>\n :root {\n --musea-bg-primary: #0d0d0d;\n --musea-bg-secondary: #1a1815;\n --musea-bg-tertiary: #252220;\n --musea-accent: #a34828;\n --musea-accent-hover: #c45a32;\n --musea-text: #e6e9f0;\n --musea-text-muted: #7b8494;\n --musea-border: #3a3530;\n --musea-success: #4ade80;\n --musea-error: #f87171;\n --musea-info: #60a5fa;\n --musea-warning: #fbbf24;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n background: var(--musea-bg-primary);\n color: var(--musea-text);\n min-height: 100vh;\n line-height: 1.5;\n }\n\n .header {\n background: var(--musea-bg-secondary);\n border-bottom: 1px solid var(--musea-border);\n padding: 1rem 2rem;\n display: flex;\n align-items: center;\n justify-content: space-between;\n position: sticky;\n top: 0;\n z-index: 100;\n }\n .header-left { display: flex; align-items: center; gap: 1rem; }\n .logo { font-size: 1.25rem; font-weight: 700; color: var(--musea-accent); }\n .header-title { color: var(--musea-text-muted); font-size: 0.875rem; }\n .header-meta { display: flex; align-items: center; gap: 1.5rem; font-size: 0.8125rem; color: var(--musea-text-muted); }\n .header-meta span { display: flex; align-items: center; gap: 0.375rem; }\n\n .main { max-width: 1400px; margin: 0 auto; padding: 2rem; }\n\n .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; margin-bottom: 2rem; }\n .stat { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 8px; padding: 1.25rem; position: relative; overflow: hidden; }\n .stat::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; }\n .stat.passed::before { background: var(--musea-success); }\n .stat.failed::before { background: var(--musea-error); }\n .stat.new::before { background: var(--musea-info); }\n .stat.skipped::before { background: var(--musea-warning); }\n .stat-value { font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; margin-bottom: 0.25rem; }\n .stat.passed .stat-value { color: var(--musea-success); }\n .stat.failed .stat-value { color: var(--musea-error); }\n .stat.new .stat-value { color: var(--musea-info); }\n .stat.skipped .stat-value { color: var(--musea-warning); }\n .stat-label { color: var(--musea-text-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500; }\n\n .filters { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }\n .filter-btn { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); color: var(--musea-text); padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.8125rem; font-weight: 500; transition: all 0.15s ease; }\n .filter-btn:hover { background: var(--musea-bg-tertiary); border-color: var(--musea-text-muted); }\n .filter-btn.active { background: var(--musea-accent); border-color: var(--musea-accent); color: #fff; }\n .filter-btn .count { opacity: 0.7; margin-left: 0.25rem; }\n\n /* Comparison mode toggle */\n .compare-modes { display: flex; gap: 0.25rem; margin-bottom: 1.5rem; background: var(--musea-bg-secondary); border-radius: 6px; padding: 0.25rem; width: fit-content; }\n .compare-mode-btn { background: none; border: none; color: var(--musea-text-muted); padding: 0.375rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-weight: 500; transition: all 0.15s ease; }\n .compare-mode-btn.active { background: var(--musea-bg-tertiary); color: var(--musea-text); }\n\n .results { display: flex; flex-direction: column; gap: 0.75rem; }\n .result { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 8px; overflow: hidden; transition: border-color 0.15s ease; }\n .result:hover { border-color: var(--musea-text-muted); }\n .result-header { padding: 1rem 1.25rem; display: flex; justify-content: space-between; align-items: center; cursor: pointer; border-left: 3px solid transparent; background: var(--musea-bg-tertiary); }\n .result.passed .result-header { border-left-color: var(--musea-success); }\n .result.failed .result-header { border-left-color: var(--musea-error); }\n .result.new .result-header { border-left-color: var(--musea-info); }\n .result.error .result-header { border-left-color: var(--musea-warning); }\n\n .result-info { display: flex; align-items: center; gap: 1rem; }\n .result-name { font-weight: 600; font-size: 0.9375rem; }\n .result-meta { color: var(--musea-text-muted); font-size: 0.8125rem; padding: 0.125rem 0.5rem; background: var(--musea-bg-secondary); border-radius: 4px; }\n .result-badge { padding: 0.25rem 0.625rem; border-radius: 4px; font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }\n .result.passed .result-badge { background: rgba(74, 222, 128, 0.15); color: var(--musea-success); }\n .result.failed .result-badge { background: rgba(248, 113, 113, 0.15); color: var(--musea-error); }\n .result.new .result-badge { background: rgba(96, 165, 250, 0.15); color: var(--musea-info); }\n .result.error .result-badge { background: rgba(251, 191, 36, 0.15); color: var(--musea-warning); }\n\n .result-body { border-top: 1px solid var(--musea-border); }\n .result-details { padding: 0.875rem 1.25rem; font-size: 0.8125rem; color: var(--musea-text-muted); font-family: 'SF Mono', 'Fira Code', monospace; background: var(--musea-bg-primary); }\n .result-details.error { color: var(--musea-error); }\n\n .result-images { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; padding: 1.25rem; background: var(--musea-bg-primary); }\n .result-images.overlay { grid-template-columns: 1fr; }\n .image-container { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 6px; overflow: hidden; }\n .image-label { padding: 0.625rem 0.875rem; font-size: 0.6875rem; font-weight: 600; color: var(--musea-text-muted); text-transform: uppercase; letter-spacing: 0.08em; background: var(--musea-bg-tertiary); border-bottom: 1px solid var(--musea-border); }\n .image-wrapper { padding: 0.5rem; background: repeating-conic-gradient(var(--musea-bg-tertiary) 0% 25%, var(--musea-bg-secondary) 0% 50%) 50% / 16px 16px; }\n .image-container img { width: 100%; height: auto; display: block; border-radius: 2px; }\n\n /* Slider comparison */\n .slider-compare { position: relative; overflow: hidden; }\n .slider-compare img { display: block; width: 100%; }\n .slider-overlay { position: absolute; top: 0; left: 0; bottom: 0; overflow: hidden; border-right: 2px solid var(--musea-accent); }\n .slider-overlay img { display: block; min-width: 100%; height: 100%; object-fit: cover; }\n\n .empty-state { text-align: center; padding: 4rem 2rem; color: var(--musea-text-muted); }\n .all-passed { background: rgba(74, 222, 128, 0.1); border: 1px solid rgba(74, 222, 128, 0.2); border-radius: 8px; padding: 1.5rem; text-align: center; margin-bottom: 1.5rem; }\n .all-passed-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }\n .all-passed-text { color: var(--musea-success); font-weight: 600; }\n </style>\n</head>\n<body>\n <header class=\"header\">\n <div class=\"header-left\">\n <div class=\"logo\">Musea</div>\n <span class=\"header-title\">Visual Regression Report</span>\n </div>\n <div class=\"header-meta\">\n <span>\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/>\n </svg>\n ${formatDuration(summary.duration)}\n </span>\n <span>\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"/><line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"/><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"/><line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"/>\n </svg>\n ${timestamp}\n </span>\n </div>\n </header>\n\n <main class=\"main\">\n <div class=\"summary\">\n <div class=\"stat passed\"><div class=\"stat-value\">${summary.passed}</div><div class=\"stat-label\">Passed</div></div>\n <div class=\"stat failed\"><div class=\"stat-value\">${summary.failed}</div><div class=\"stat-label\">Failed</div></div>\n <div class=\"stat new\"><div class=\"stat-value\">${summary.new}</div><div class=\"stat-label\">New</div></div>\n <div class=\"stat skipped\"><div class=\"stat-value\">${summary.skipped}</div><div class=\"stat-label\">Skipped</div></div>\n </div>\n\n ${\n summary.failed === 0 && summary.skipped === 0 && summary.total > 0\n ? `<div class=\"all-passed\">\n <div class=\"all-passed-icon\">✓</div>\n <div class=\"all-passed-text\">All ${summary.total} visual tests passed</div>\n </div>`\n : \"\"\n }\n\n <div class=\"filters\">\n <button class=\"filter-btn active\" data-filter=\"all\">All<span class=\"count\">(${summary.total})</span></button>\n <button class=\"filter-btn\" data-filter=\"failed\">Failed<span class=\"count\">(${summary.failed})</span></button>\n <button class=\"filter-btn\" data-filter=\"passed\">Passed<span class=\"count\">(${summary.passed})</span></button>\n <button class=\"filter-btn\" data-filter=\"new\">New<span class=\"count\">(${summary.new})</span></button>\n </div>\n\n <div class=\"compare-modes\">\n <button class=\"compare-mode-btn active\" data-mode=\"side-by-side\">Side by Side</button>\n <button class=\"compare-mode-btn\" data-mode=\"overlay\">Overlay</button>\n <button class=\"compare-mode-btn\" data-mode=\"slider\">Slider</button>\n </div>\n\n <div class=\"results\">\n ${\n results.length === 0\n ? `<div class=\"empty-state\"><p>No visual tests found</p></div>`\n : results\n .map((r) => {\n const status = r.error ? \"error\" : r.isNew ? \"new\" : r.passed ? \"passed\" : \"failed\";\n const badge = r.error ? \"Error\" : r.isNew ? \"New\" : r.passed ? \"Passed\" : \"Failed\";\n const artName = path.basename(r.artPath, \".art.vue\");\n const viewportName = r.viewport.name || `${r.viewport.width}×${r.viewport.height}`;\n\n let details = \"\";\n if (r.error) {\n details = `<div class=\"result-details error\">${escapeHtml(r.error)}</div>`;\n } else if (r.diffPercentage !== undefined) {\n const diffFormatted = r.diffPercentage.toFixed(3);\n const pixelsFormatted = r.diffPixels?.toLocaleString() ?? \"0\";\n const totalFormatted = r.totalPixels?.toLocaleString() ?? \"0\";\n details = `<div class=\"result-details\">diff: ${diffFormatted}% (${pixelsFormatted} / ${totalFormatted} pixels)</div>`;\n }\n\n let images = \"\";\n if (!r.error && !r.passed && r.diffPath) {\n images = `<div class=\"result-images\" data-baseline=\"file://${r.snapshotPath}\" data-current=\"file://${r.currentPath}\" data-diff=\"file://${r.diffPath}\">\n ${r.snapshotPath ? `<div class=\"image-container\"><div class=\"image-label\">Baseline</div><div class=\"image-wrapper\"><img src=\"file://${r.snapshotPath}\" alt=\"Baseline\" loading=\"lazy\" /></div></div>` : \"\"}\n ${r.currentPath ? `<div class=\"image-container\"><div class=\"image-label\">Current</div><div class=\"image-wrapper\"><img src=\"file://${r.currentPath}\" alt=\"Current\" loading=\"lazy\" /></div></div>` : \"\"}\n ${r.diffPath ? `<div class=\"image-container\"><div class=\"image-label\">Diff</div><div class=\"image-wrapper\"><img src=\"file://${r.diffPath}\" alt=\"Diff\" loading=\"lazy\" /></div></div>` : \"\"}\n </div>`;\n }\n\n const hasBody = details || images;\n\n return `<div class=\"result ${status}\" data-status=\"${status}\">\n <div class=\"result-header\">\n <div class=\"result-info\">\n <div class=\"result-name\">${escapeHtml(artName)} / ${escapeHtml(r.variantName)}</div>\n <div class=\"result-meta\">${escapeHtml(viewportName)}</div>\n </div>\n <span class=\"result-badge\">${badge}</span>\n </div>\n ${hasBody ? `<div class=\"result-body\">${details}${images}</div>` : \"\"}\n </div>`;\n })\n .join(\"\")\n }\n </div>\n </main>\n\n <script>\n // Filter buttons\n document.querySelectorAll('.filter-btn').forEach(btn => {\n btn.addEventListener('click', () => {\n document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));\n btn.classList.add('active');\n const filter = btn.dataset.filter;\n document.querySelectorAll('.result').forEach(result => {\n result.style.display = (filter === 'all' || result.dataset.status === filter) ? 'block' : 'none';\n });\n });\n });\n\n // Compare mode buttons\n document.querySelectorAll('.compare-mode-btn').forEach(btn => {\n btn.addEventListener('click', () => {\n document.querySelectorAll('.compare-mode-btn').forEach(b => b.classList.remove('active'));\n btn.classList.add('active');\n // Mode switching would update result-images display; this is a static report for now\n });\n });\n </script>\n</body>\n</html>`;\n\n return html;\n}\n\n/**\n * Generate VRT JSON report for CI integration.\n */\nexport function generateVrtJsonReport(results: VrtResult[], summary: VrtSummary): string {\n return JSON.stringify(\n {\n timestamp: new Date().toISOString(),\n summary,\n results: results.map((r) => ({\n art: path.basename(r.artPath, \".art.vue\"),\n variant: r.variantName,\n viewport: r.viewport.name || `${r.viewport.width}x${r.viewport.height}`,\n status: r.error ? \"error\" : r.isNew ? \"new\" : r.passed ? \"passed\" : \"failed\",\n diffPercentage: r.diffPercentage,\n error: r.error,\n })),\n },\n null,\n 2,\n );\n}\n","/**\n * Visual Regression Testing (VRT) module for Musea.\n * Uses Playwright for browser automation and pixel comparison.\n *\n * This file re-exports from the split VRT submodules for backward compatibility.\n */\n\nexport {\n MuseaVrtRunner,\n type VrtResult,\n type VrtSummary,\n type ExtendedVrtOptions,\n type PixelCompareOptions,\n} from \"./vrt/runner.js\";\n\nexport { generateVrtReport, generateVrtJsonReport } from \"./vrt/report.js\";\n\nimport { MuseaVrtRunner } from \"./vrt/runner.js\";\nexport default MuseaVrtRunner;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vrt-DgVSWKGl.mjs","names":[],"sources":["../src/vrt/comparison.ts","../src/vrt/utils.ts","../src/vrt/runner-comparison.ts","../src/vrt/runner.ts","../src/vrt/report.ts","../src/vrt.ts"],"sourcesContent":["/**\n * Pixel comparison utilities for VRT.\n *\n * Provides PNG reading/writing, color delta calculation using YIQ color space,\n * and anti-aliasing detection for pixel-level image comparison.\n */\n\nimport fs from \"node:fs\";\nimport { PNG } from \"pngjs\";\n\n/**\n * Read PNG file and return PNG object.\n */\nexport async function readPng(filepath: string): Promise<PNG> {\n return new Promise((resolve, reject) => {\n fs.createReadStream(filepath)\n .pipe(new PNG())\n .on(\"parsed\", function (this: PNG) {\n resolve(this);\n })\n .on(\"error\", reject);\n });\n}\n\n/**\n * Write PNG object to file.\n */\nexport async function writePng(png: PNG, filepath: string): Promise<void> {\n return new Promise((resolve, reject) => {\n png.pack().pipe(fs.createWriteStream(filepath)).on(\"finish\", resolve).on(\"error\", reject);\n });\n}\n\n/**\n * Calculate color delta using YIQ color space.\n */\nexport function colorDelta(\n r1: number,\n g1: number,\n b1: number,\n a1: number,\n r2: number,\n g2: number,\n b2: number,\n a2: number,\n): number {\n if (a1 !== 255) {\n r1 = blend(r1, 255, a1 / 255);\n g1 = blend(g1, 255, a1 / 255);\n b1 = blend(b1, 255, a1 / 255);\n }\n if (a2 !== 255) {\n r2 = blend(r2, 255, a2 / 255);\n g2 = blend(g2, 255, a2 / 255);\n b2 = blend(b2, 255, a2 / 255);\n }\n\n const y1 = r1 * 0.29889531 + g1 * 0.58662247 + b1 * 0.11448223;\n const i1 = r1 * 0.59597799 - g1 * 0.2741761 - b1 * 0.32180189;\n const q1 = r1 * 0.21147017 - g1 * 0.52261711 + b1 * 0.31114694;\n\n const y2 = r2 * 0.29889531 + g2 * 0.58662247 + b2 * 0.11448223;\n const i2 = r2 * 0.59597799 - g2 * 0.2741761 - b2 * 0.32180189;\n const q2 = r2 * 0.21147017 - g2 * 0.52261711 + b2 * 0.31114694;\n\n const dy = y1 - y2;\n const di = i1 - i2;\n const dq = q1 - q2;\n\n return dy * dy * 0.5053 + di * di * 0.299 + dq * dq * 0.1957;\n}\n\n/**\n * Blend foreground with background using alpha.\n */\nfunction blend(fg: number, bg: number, alpha: number): number {\n return bg + (fg - bg) * alpha;\n}\n\n/**\n * Check if file exists.\n */\nexport async function fileExists(filepath: string): Promise<boolean> {\n try {\n await fs.promises.access(filepath);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Simple anti-aliasing detection.\n * A pixel is likely anti-aliased if its neighbors have high contrast in opposite directions.\n */\nexport function isAntiAliased(\n img1: PNG,\n img2: PNG,\n x: number,\n y: number,\n width: number,\n height: number,\n): boolean {\n const minX = Math.max(0, x - 1);\n const maxX = Math.min(width - 1, x + 1);\n const minY = Math.max(0, y - 1);\n const maxY = Math.min(height - 1, y + 1);\n\n let zeroes = 0;\n let positives = 0;\n let negatives = 0;\n\n for (let ny = minY; ny <= maxY; ny++) {\n for (let nx = minX; nx <= maxX; nx++) {\n if (nx === x && ny === y) continue;\n const idx = (ny * width + nx) * 4;\n\n const delta = colorDelta(\n img1.data[idx],\n img1.data[idx + 1],\n img1.data[idx + 2],\n img1.data[idx + 3],\n img2.data[idx],\n img2.data[idx + 1],\n img2.data[idx + 2],\n img2.data[idx + 3],\n );\n\n if (delta === 0) {\n zeroes++;\n } else if (delta > 0) {\n positives++;\n } else {\n negatives++;\n }\n }\n }\n\n // If neighbors are mixed (some match, some differ), it's likely AA\n return zeroes > 0 && (positives > 0 || negatives > 0) && positives + negatives < 4;\n}\n\n/**\n * Simple glob matching for pattern-based filtering.\n */\nexport function matchGlob(filepath: string, pattern: string): boolean {\n const regex = pattern\n .replace(/\\./g, \"\\\\.\")\n .replace(/\\*\\*/g, \".*\")\n .replace(/\\*(?!\\*)/g, \"[^/]*\");\n return new RegExp(`^${regex}$`).test(filepath);\n}\n\n/**\n * Escape HTML special characters.\n */\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","/**\n * Utility functions for VRT runner.\n *\n * Includes summary generation, URL builders, and helpers\n * used across the runner modules.\n */\n\nimport type { VrtResult, VrtSummary } from \"./types.js\";\n\n/**\n * Build URL for variant preview.\n */\nexport function buildVariantUrl(baseUrl: string, artPath: string, variantName: string): string {\n const encodedPath = encodeURIComponent(artPath);\n const encodedVariant = encodeURIComponent(variantName);\n return `${baseUrl}/__musea__/preview?art=${encodedPath}&variant=${encodedVariant}`;\n}\n\n/**\n * Compute VRT summary statistics from a list of results.\n */\nexport function computeSummary(results: VrtResult[], startTime: number): VrtSummary {\n return {\n total: results.length,\n passed: results.filter((r) => r.passed && !r.isNew).length,\n failed: results.filter((r) => !r.passed && !r.error).length,\n new: results.filter((r) => r.isNew).length,\n skipped: results.filter((r) => r.error).length,\n duration: Date.now() - startTime,\n };\n}\n","/**\n * Screenshot capture and pixel comparison logic for VRT.\n *\n * Contains the captureAndCompare flow (browser context, navigation,\n * element hiding/masking, screenshot, baseline check) and the\n * pixel-level compareImages implementation.\n */\n\nimport type { BrowserContext, Page } from \"playwright\";\nimport type { ArtFileInfo, ViewportConfig } from \"../types/index.js\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { PNG } from \"pngjs\";\n\nimport { readPng, writePng, colorDelta, isAntiAliased, fileExists } from \"./comparison.js\";\nimport type { VrtResult } from \"./types.js\";\nimport type { MuseaVrtRunner } from \"./runner.js\";\nimport { buildVariantUrl } from \"./utils.js\";\n\n/**\n * Capture screenshot and compare with baseline.\n *\n * Standalone function that operates on a MuseaVrtRunner instance.\n */\nexport async function captureAndCompare(\n runner: MuseaVrtRunner,\n art: ArtFileInfo,\n variantName: string,\n viewport: ViewportConfig,\n baseUrl: string,\n): Promise<VrtResult> {\n const browser = runner.getBrowser();\n if (!browser) {\n throw new Error(\"VRT runner not initialized. Call init() first.\");\n }\n\n const options = runner.getOptions();\n const capture = runner.getCapture();\n const comparison = runner.getComparison();\n\n const snapshotDir = options.snapshotDir;\n const artBaseName = path.basename(art.path, \".art.vue\");\n const viewportName = viewport.name || `${viewport.width}x${viewport.height}`;\n const snapshotName = `${artBaseName}--${variantName}--${viewportName}.png`;\n const snapshotPath = path.join(snapshotDir, snapshotName);\n const currentPath = path.join(snapshotDir, \"current\", snapshotName);\n const diffPath = path.join(snapshotDir, \"diff\", snapshotName);\n\n // Ensure directories exist\n await fs.promises.mkdir(path.dirname(snapshotPath), { recursive: true });\n await fs.promises.mkdir(path.join(snapshotDir, \"current\"), { recursive: true });\n await fs.promises.mkdir(path.join(snapshotDir, \"diff\"), { recursive: true });\n\n let context: BrowserContext | null = null;\n let page: Page | null = null;\n\n try {\n context = await browser.newContext({\n viewport: {\n width: viewport.width,\n height: viewport.height,\n },\n deviceScaleFactor: viewport.deviceScaleFactor ?? 1,\n });\n page = await context.newPage();\n\n // Navigate to variant preview URL\n const variantUrl = buildVariantUrl(baseUrl, art.path, variantName);\n const waitUntil = capture.waitForNetwork ? (\"networkidle\" as const) : (\"load\" as const);\n await page.goto(variantUrl, { waitUntil });\n\n // Wait for content to render\n await page.waitForSelector(capture.waitSelector, { timeout: 10000 });\n\n // Additional wait for animations to settle\n await page.waitForTimeout(capture.settleTime);\n\n // Hide elements before capture\n if (capture.hideElements.length > 0) {\n for (const selector of capture.hideElements) {\n await page.evaluate((sel) => {\n document.querySelectorAll(sel).forEach((el) => {\n (el as HTMLElement).style.visibility = \"hidden\";\n });\n }, selector);\n }\n }\n\n // Mask elements before capture (replace with colored box)\n if (capture.maskElements.length > 0) {\n for (const selector of capture.maskElements) {\n await page.evaluate((sel) => {\n document.querySelectorAll(sel).forEach((el) => {\n const htmlEl = el as HTMLElement;\n htmlEl.style.background = \"#ff00ff\";\n htmlEl.style.color = \"transparent\";\n htmlEl.innerHTML = \"\";\n });\n }, selector);\n }\n }\n\n // Take screenshot\n await page.screenshot({\n path: currentPath,\n fullPage: capture.fullPage,\n });\n\n // Check if baseline exists\n const hasBaseline = await fileExists(snapshotPath);\n\n if (!hasBaseline) {\n // First run - save as baseline\n await fs.promises.copyFile(currentPath, snapshotPath);\n return {\n artPath: art.path,\n variantName,\n viewport,\n passed: true,\n snapshotPath,\n currentPath,\n isNew: true,\n };\n }\n\n // Compare images using pixel comparison\n const comparisonResult = await compareImages(snapshotPath, currentPath, diffPath, comparison);\n\n const passed = comparisonResult.diffPercentage <= options.threshold;\n\n return {\n artPath: art.path,\n variantName,\n viewport,\n passed,\n snapshotPath,\n currentPath,\n diffPath: passed ? undefined : diffPath,\n diffPercentage: comparisonResult.diffPercentage,\n diffPixels: comparisonResult.diffPixels,\n totalPixels: comparisonResult.totalPixels,\n };\n } catch (error) {\n return {\n artPath: art.path,\n variantName,\n viewport,\n passed: false,\n snapshotPath,\n error: error instanceof Error ? error.message : String(error),\n };\n } finally {\n if (page) await page.close();\n if (context) await context.close();\n }\n}\n\n/**\n * Compare two PNG images and generate a diff image.\n * Returns pixel difference statistics.\n */\nexport async function compareImages(\n baselinePath: string,\n currentPath: string,\n diffPath: string,\n comparison: {\n antiAliasing?: boolean;\n alpha?: boolean;\n diffColor?: { r: number; g: number; b: number };\n },\n): Promise<{ diffPixels: number; totalPixels: number; diffPercentage: number }> {\n const baseline = await readPng(baselinePath);\n const current = await readPng(currentPath);\n\n // Handle size mismatch\n if (baseline.width !== current.width || baseline.height !== current.height) {\n const width = Math.max(baseline.width, current.width);\n const height = Math.max(baseline.height, current.height);\n const diff = new PNG({ width, height });\n\n // Fill with red to indicate size mismatch\n for (let i = 0; i < diff.data.length; i += 4) {\n diff.data[i] = 255; // R\n diff.data[i + 1] = 0; // G\n diff.data[i + 2] = 0; // B\n diff.data[i + 3] = 255; // A\n }\n\n await writePng(diff, diffPath);\n\n return {\n diffPixels: width * height,\n totalPixels: width * height,\n diffPercentage: 100,\n };\n }\n\n const width = baseline.width;\n const height = baseline.height;\n const totalPixels = width * height;\n const diff = new PNG({ width, height });\n\n const useAntiAliasing = comparison.antiAliasing ?? true;\n const useAlpha = comparison.alpha ?? true;\n const diffColor = comparison.diffColor ?? { r: 255, g: 0, b: 0 };\n\n // Pixel comparison\n let diffPixels = 0;\n const threshold = 0.1; // Color difference threshold\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const idx = (y * width + x) * 4;\n\n const r1 = baseline.data[idx];\n const g1 = baseline.data[idx + 1];\n const b1 = baseline.data[idx + 2];\n const a1 = useAlpha ? baseline.data[idx + 3] : 255;\n\n const r2 = current.data[idx];\n const g2 = current.data[idx + 1];\n const b2 = current.data[idx + 2];\n const a2 = useAlpha ? current.data[idx + 3] : 255;\n\n // Calculate color difference using YIQ color space\n const delta = colorDelta(r1, g1, b1, a1, r2, g2, b2, a2);\n\n if (delta > threshold * 255 * 255) {\n // Anti-aliasing detection: check if pixel is likely AA\n if (useAntiAliasing && isAntiAliased(baseline, current, x, y, width, height)) {\n // Mark as AA (yellow)\n diff.data[idx] = 255;\n diff.data[idx + 1] = 200;\n diff.data[idx + 2] = 0;\n diff.data[idx + 3] = 128;\n } else {\n // Mark as different\n diffPixels++;\n diff.data[idx] = diffColor.r;\n diff.data[idx + 1] = diffColor.g;\n diff.data[idx + 2] = diffColor.b;\n diff.data[idx + 3] = 255;\n }\n } else {\n // Grayscale for matching pixels\n const gray = Math.round((r2 + g2 + b2) / 3);\n diff.data[idx] = gray;\n diff.data[idx + 1] = gray;\n diff.data[idx + 2] = gray;\n diff.data[idx + 3] = 128; // Semi-transparent\n }\n }\n }\n\n // Only write diff if there are differences\n if (diffPixels > 0) {\n await writePng(diff, diffPath);\n }\n\n const diffPercentage = (diffPixels / totalPixels) * 100;\n\n return {\n diffPixels,\n totalPixels,\n diffPercentage,\n };\n}\n","/**\n * VRT runner using Playwright for browser automation.\n *\n * Manages browser lifecycle, screenshot capture, and baseline comparison\n * for visual regression testing of Musea art file variants.\n */\n\nimport type { Browser, BrowserContext, Page } from \"playwright\";\nimport type {\n ArtFileInfo,\n VrtOptions,\n ViewportConfig,\n CaptureConfig,\n ComparisonConfig,\n CiConfig,\n} from \"../types/index.js\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { fileExists, matchGlob } from \"./comparison.js\";\nimport { captureAndCompare } from \"./runner-comparison.js\";\nimport { computeSummary } from \"./utils.js\";\n\nexport type { VrtResult, VrtSummary, ExtendedVrtOptions, PixelCompareOptions } from \"./types.js\";\n\nimport type { VrtResult, VrtSummary, ExtendedVrtOptions } from \"./types.js\";\n\n/**\n * VRT runner using Playwright.\n */\nexport class MuseaVrtRunner {\n private options: Required<VrtOptions>;\n private capture: Required<CaptureConfig>;\n private comparison: ComparisonConfig;\n private ci: CiConfig;\n private browser: Browser | null = null;\n private startTime: number = 0;\n\n constructor(options: ExtendedVrtOptions = {}) {\n this.options = {\n snapshotDir: options.snapshotDir ?? \".vize/snapshots\",\n threshold: options.threshold ?? 0.1,\n viewports: options.viewports ?? [\n { width: 1280, height: 720, name: \"desktop\" },\n { width: 375, height: 667, name: \"mobile\" },\n ],\n };\n this.capture = {\n fullPage: options.capture?.fullPage ?? false,\n waitForNetwork: options.capture?.waitForNetwork ?? true,\n settleTime: options.capture?.settleTime ?? 100,\n waitSelector: options.capture?.waitSelector ?? \".musea-variant\",\n hideElements: options.capture?.hideElements ?? [],\n maskElements: options.capture?.maskElements ?? [],\n };\n this.comparison = options.comparison ?? {};\n this.ci = options.ci ?? {};\n }\n\n // --- Internal accessors used by runner-comparison ---\n\n /** @internal */\n getBrowser(): Browser | null {\n return this.browser;\n }\n\n /** @internal */\n getOptions(): Required<VrtOptions> {\n return this.options;\n }\n\n /** @internal */\n getCapture(): Required<CaptureConfig> {\n return this.capture;\n }\n\n /** @internal */\n getComparison(): ComparisonConfig {\n return this.comparison;\n }\n\n /**\n * Initialize Playwright browser.\n */\n async init(): Promise<void> {\n const { chromium } = await import(\"playwright\");\n this.browser = await chromium.launch({ headless: true });\n this.startTime = Date.now();\n }\n\n /**\n * Close browser and cleanup.\n */\n async close(): Promise<void> {\n if (this.browser) {\n await this.browser.close();\n this.browser = null;\n }\n }\n\n /**\n * Alias for init() - used by the plugin API.\n */\n async start(): Promise<void> {\n return this.init();\n }\n\n /**\n * Alias for close() - used by the plugin API.\n */\n async stop(): Promise<void> {\n return this.close();\n }\n\n /**\n * Run VRT tests for all Art files.\n */\n async runAllTests(artFiles: ArtFileInfo[], baseUrl: string): Promise<VrtResult[]> {\n if (!this.browser) {\n throw new Error(\"VRT runner not initialized. Call init() first.\");\n }\n\n const results: VrtResult[] = [];\n const retries = this.ci.retries ?? 0;\n\n for (const art of artFiles) {\n for (const variant of art.variants) {\n if (variant.skipVrt) {\n continue;\n }\n\n // Determine viewports: use per-variant viewport if defined, else global viewports\n const viewports = variant.args?.viewport\n ? [variant.args.viewport as ViewportConfig]\n : this.options.viewports;\n\n for (const viewport of viewports) {\n let result: VrtResult | null = null;\n let attempts = 0;\n\n while (attempts <= retries) {\n result = await this.captureAndCompare(art, variant.name, viewport, baseUrl);\n if (result.passed || result.isNew || !result.error) {\n break;\n }\n attempts++;\n if (attempts <= retries) {\n console.log(\n `[vrt] Retry ${attempts}/${retries}: ${path.basename(art.path)}/${variant.name}`,\n );\n }\n }\n\n if (result) {\n results.push(result);\n }\n }\n }\n }\n\n return results;\n }\n\n /**\n * Run VRT tests - alias used by the plugin API that accepts options.\n */\n async runTests(\n artFiles: ArtFileInfo[],\n baseUrl: string,\n _options?: { updateSnapshots?: boolean },\n ): Promise<VrtResult[]> {\n const results = await this.runAllTests(artFiles, baseUrl);\n if (_options?.updateSnapshots) {\n await this.updateBaselines(results);\n }\n return results;\n }\n\n /**\n * Capture screenshot and compare with baseline.\n */\n async captureAndCompare(\n art: ArtFileInfo,\n variantName: string,\n viewport: ViewportConfig,\n baseUrl: string,\n ): Promise<VrtResult> {\n return captureAndCompare(this, art, variantName, viewport, baseUrl);\n }\n\n /**\n * Get the Playwright Page for external use (e.g., a11y auditing).\n */\n async createPage(viewport: ViewportConfig): Promise<{ page: Page; context: BrowserContext }> {\n if (!this.browser) {\n throw new Error(\"VRT runner not initialized. Call init() first.\");\n }\n const context = await this.browser.newContext({\n viewport: { width: viewport.width, height: viewport.height },\n deviceScaleFactor: viewport.deviceScaleFactor ?? 1,\n });\n const page = await context.newPage();\n return { page, context };\n }\n\n /**\n * Update baseline snapshots with current screenshots.\n */\n async updateBaselines(results: VrtResult[]): Promise<number> {\n let updated = 0;\n const snapshotDir = this.options.snapshotDir;\n const currentDir = path.join(snapshotDir, \"current\");\n\n for (const result of results) {\n const currentPath = path.join(currentDir, path.basename(result.snapshotPath));\n\n if (await fileExists(currentPath)) {\n await fs.promises.copyFile(currentPath, result.snapshotPath);\n updated++;\n console.log(`[vrt] Updated: ${path.basename(result.snapshotPath)}`);\n }\n }\n\n return updated;\n }\n\n /**\n * Approve specific failed results (update their baselines).\n */\n async approveResults(results: VrtResult[], pattern?: string): Promise<number> {\n const toApprove = pattern\n ? results.filter((r) => {\n const name = `${path.basename(r.artPath, \".art.vue\")}/${r.variantName}`;\n return name.includes(pattern) || matchGlob(name, pattern);\n })\n : results.filter((r) => !r.passed && !r.error);\n\n return this.updateBaselines(toApprove);\n }\n\n /**\n * Clean orphaned snapshots (no corresponding art/variant).\n */\n async cleanOrphans(artFiles: ArtFileInfo[]): Promise<number> {\n const snapshotDir = this.options.snapshotDir;\n let cleaned = 0;\n\n try {\n const files = await fs.promises.readdir(snapshotDir);\n const validNames = new Set<string>();\n\n for (const art of artFiles) {\n const artBaseName = path.basename(art.path, \".art.vue\");\n for (const variant of art.variants) {\n if (variant.skipVrt) continue;\n for (const viewport of this.options.viewports) {\n const viewportName = viewport.name || `${viewport.width}x${viewport.height}`;\n validNames.add(`${artBaseName}--${variant.name}--${viewportName}.png`);\n }\n }\n }\n\n for (const file of files) {\n if (file.endsWith(\".png\") && !validNames.has(file)) {\n await fs.promises.unlink(path.join(snapshotDir, file));\n cleaned++;\n console.log(`[vrt] Cleaned: ${file}`);\n }\n }\n } catch {\n // Directory may not exist yet\n }\n\n return cleaned;\n }\n\n /**\n * Get VRT summary statistics.\n */\n getSummary(results: VrtResult[]): VrtSummary {\n return computeSummary(results, this.startTime);\n }\n}\n","/**\n * VRT report generation for Musea.\n *\n * Generates HTML and JSON reports from VRT results for visual review\n * and CI integration.\n */\n\nimport path from \"node:path\";\n\nimport type { VrtResult, VrtSummary } from \"./types.js\";\nimport { escapeHtml } from \"./comparison.js\";\n\n/**\n * Generate VRT report in HTML format.\n * Supports side-by-side, overlay, and slider comparison modes.\n */\nexport function generateVrtReport(results: VrtResult[], summary: VrtSummary): string {\n const formatDuration = (ms: number): string => {\n if (ms < 1000) return `${ms}ms`;\n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n if (minutes === 0) return `${seconds}s`;\n return `${minutes}m ${seconds % 60}s`;\n };\n\n const timestamp = new Date().toLocaleString(\"ja-JP\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n\n const html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>VRT Report - Musea</title>\n <style>\n :root {\n --musea-bg-primary: #0d0d0d;\n --musea-bg-secondary: #1a1815;\n --musea-bg-tertiary: #252220;\n --musea-accent: #a34828;\n --musea-accent-hover: #c45a32;\n --musea-text: #e6e9f0;\n --musea-text-muted: #7b8494;\n --musea-border: #3a3530;\n --musea-success: #4ade80;\n --musea-error: #f87171;\n --musea-info: #60a5fa;\n --musea-warning: #fbbf24;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n background: var(--musea-bg-primary);\n color: var(--musea-text);\n min-height: 100vh;\n line-height: 1.5;\n }\n\n .header {\n background: var(--musea-bg-secondary);\n border-bottom: 1px solid var(--musea-border);\n padding: 1rem 2rem;\n display: flex;\n align-items: center;\n justify-content: space-between;\n position: sticky;\n top: 0;\n z-index: 100;\n }\n .header-left { display: flex; align-items: center; gap: 1rem; }\n .logo { font-size: 1.25rem; font-weight: 700; color: var(--musea-accent); }\n .header-title { color: var(--musea-text-muted); font-size: 0.875rem; }\n .header-meta { display: flex; align-items: center; gap: 1.5rem; font-size: 0.8125rem; color: var(--musea-text-muted); }\n .header-meta span { display: flex; align-items: center; gap: 0.375rem; }\n\n .main { max-width: 1400px; margin: 0 auto; padding: 2rem; }\n\n .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; margin-bottom: 2rem; }\n .stat { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 8px; padding: 1.25rem; position: relative; overflow: hidden; }\n .stat::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; }\n .stat.passed::before { background: var(--musea-success); }\n .stat.failed::before { background: var(--musea-error); }\n .stat.new::before { background: var(--musea-info); }\n .stat.skipped::before { background: var(--musea-warning); }\n .stat-value { font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; margin-bottom: 0.25rem; }\n .stat.passed .stat-value { color: var(--musea-success); }\n .stat.failed .stat-value { color: var(--musea-error); }\n .stat.new .stat-value { color: var(--musea-info); }\n .stat.skipped .stat-value { color: var(--musea-warning); }\n .stat-label { color: var(--musea-text-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500; }\n\n .filters { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }\n .filter-btn { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); color: var(--musea-text); padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.8125rem; font-weight: 500; transition: all 0.15s ease; }\n .filter-btn:hover { background: var(--musea-bg-tertiary); border-color: var(--musea-text-muted); }\n .filter-btn.active { background: var(--musea-accent); border-color: var(--musea-accent); color: #fff; }\n .filter-btn .count { opacity: 0.7; margin-left: 0.25rem; }\n\n /* Comparison mode toggle */\n .compare-modes { display: flex; gap: 0.25rem; margin-bottom: 1.5rem; background: var(--musea-bg-secondary); border-radius: 6px; padding: 0.25rem; width: fit-content; }\n .compare-mode-btn { background: none; border: none; color: var(--musea-text-muted); padding: 0.375rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-weight: 500; transition: all 0.15s ease; }\n .compare-mode-btn.active { background: var(--musea-bg-tertiary); color: var(--musea-text); }\n\n .results { display: flex; flex-direction: column; gap: 0.75rem; }\n .result { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 8px; overflow: hidden; transition: border-color 0.15s ease; }\n .result:hover { border-color: var(--musea-text-muted); }\n .result-header { padding: 1rem 1.25rem; display: flex; justify-content: space-between; align-items: center; cursor: pointer; border-left: 3px solid transparent; background: var(--musea-bg-tertiary); }\n .result.passed .result-header { border-left-color: var(--musea-success); }\n .result.failed .result-header { border-left-color: var(--musea-error); }\n .result.new .result-header { border-left-color: var(--musea-info); }\n .result.error .result-header { border-left-color: var(--musea-warning); }\n\n .result-info { display: flex; align-items: center; gap: 1rem; }\n .result-name { font-weight: 600; font-size: 0.9375rem; }\n .result-meta { color: var(--musea-text-muted); font-size: 0.8125rem; padding: 0.125rem 0.5rem; background: var(--musea-bg-secondary); border-radius: 4px; }\n .result-badge { padding: 0.25rem 0.625rem; border-radius: 4px; font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }\n .result.passed .result-badge { background: rgba(74, 222, 128, 0.15); color: var(--musea-success); }\n .result.failed .result-badge { background: rgba(248, 113, 113, 0.15); color: var(--musea-error); }\n .result.new .result-badge { background: rgba(96, 165, 250, 0.15); color: var(--musea-info); }\n .result.error .result-badge { background: rgba(251, 191, 36, 0.15); color: var(--musea-warning); }\n\n .result-body { border-top: 1px solid var(--musea-border); }\n .result-details { padding: 0.875rem 1.25rem; font-size: 0.8125rem; color: var(--musea-text-muted); font-family: 'SF Mono', 'Fira Code', monospace; background: var(--musea-bg-primary); }\n .result-details.error { color: var(--musea-error); }\n\n .result-images { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; padding: 1.25rem; background: var(--musea-bg-primary); }\n .result-images.overlay { grid-template-columns: 1fr; }\n .image-container { background: var(--musea-bg-secondary); border: 1px solid var(--musea-border); border-radius: 6px; overflow: hidden; }\n .image-label { padding: 0.625rem 0.875rem; font-size: 0.6875rem; font-weight: 600; color: var(--musea-text-muted); text-transform: uppercase; letter-spacing: 0.08em; background: var(--musea-bg-tertiary); border-bottom: 1px solid var(--musea-border); }\n .image-wrapper { padding: 0.5rem; background: repeating-conic-gradient(var(--musea-bg-tertiary) 0% 25%, var(--musea-bg-secondary) 0% 50%) 50% / 16px 16px; }\n .image-container img { width: 100%; height: auto; display: block; border-radius: 2px; }\n\n /* Slider comparison */\n .slider-compare { position: relative; overflow: hidden; }\n .slider-compare img { display: block; width: 100%; }\n .slider-overlay { position: absolute; top: 0; left: 0; bottom: 0; overflow: hidden; border-right: 2px solid var(--musea-accent); }\n .slider-overlay img { display: block; min-width: 100%; height: 100%; object-fit: cover; }\n\n .empty-state { text-align: center; padding: 4rem 2rem; color: var(--musea-text-muted); }\n .all-passed { background: rgba(74, 222, 128, 0.1); border: 1px solid rgba(74, 222, 128, 0.2); border-radius: 8px; padding: 1.5rem; text-align: center; margin-bottom: 1.5rem; }\n .all-passed-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }\n .all-passed-text { color: var(--musea-success); font-weight: 600; }\n </style>\n</head>\n<body>\n <header class=\"header\">\n <div class=\"header-left\">\n <div class=\"logo\">Musea</div>\n <span class=\"header-title\">Visual Regression Report</span>\n </div>\n <div class=\"header-meta\">\n <span>\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/>\n </svg>\n ${formatDuration(summary.duration)}\n </span>\n <span>\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"/><line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"/><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"/><line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"/>\n </svg>\n ${timestamp}\n </span>\n </div>\n </header>\n\n <main class=\"main\">\n <div class=\"summary\">\n <div class=\"stat passed\"><div class=\"stat-value\">${summary.passed}</div><div class=\"stat-label\">Passed</div></div>\n <div class=\"stat failed\"><div class=\"stat-value\">${summary.failed}</div><div class=\"stat-label\">Failed</div></div>\n <div class=\"stat new\"><div class=\"stat-value\">${summary.new}</div><div class=\"stat-label\">New</div></div>\n <div class=\"stat skipped\"><div class=\"stat-value\">${summary.skipped}</div><div class=\"stat-label\">Skipped</div></div>\n </div>\n\n ${\n summary.failed === 0 && summary.skipped === 0 && summary.total > 0\n ? `<div class=\"all-passed\">\n <div class=\"all-passed-icon\">✓</div>\n <div class=\"all-passed-text\">All ${summary.total} visual tests passed</div>\n </div>`\n : \"\"\n }\n\n <div class=\"filters\">\n <button class=\"filter-btn active\" data-filter=\"all\">All<span class=\"count\">(${summary.total})</span></button>\n <button class=\"filter-btn\" data-filter=\"failed\">Failed<span class=\"count\">(${summary.failed})</span></button>\n <button class=\"filter-btn\" data-filter=\"passed\">Passed<span class=\"count\">(${summary.passed})</span></button>\n <button class=\"filter-btn\" data-filter=\"new\">New<span class=\"count\">(${summary.new})</span></button>\n </div>\n\n <div class=\"compare-modes\">\n <button class=\"compare-mode-btn active\" data-mode=\"side-by-side\">Side by Side</button>\n <button class=\"compare-mode-btn\" data-mode=\"overlay\">Overlay</button>\n <button class=\"compare-mode-btn\" data-mode=\"slider\">Slider</button>\n </div>\n\n <div class=\"results\">\n ${\n results.length === 0\n ? `<div class=\"empty-state\"><p>No visual tests found</p></div>`\n : results\n .map((r) => {\n const status = r.error ? \"error\" : r.isNew ? \"new\" : r.passed ? \"passed\" : \"failed\";\n const badge = r.error ? \"Error\" : r.isNew ? \"New\" : r.passed ? \"Passed\" : \"Failed\";\n const artName = path.basename(r.artPath, \".art.vue\");\n const viewportName = r.viewport.name || `${r.viewport.width}×${r.viewport.height}`;\n\n let details = \"\";\n if (r.error) {\n details = `<div class=\"result-details error\">${escapeHtml(r.error)}</div>`;\n } else if (r.diffPercentage !== undefined) {\n const diffFormatted = r.diffPercentage.toFixed(3);\n const pixelsFormatted = r.diffPixels?.toLocaleString() ?? \"0\";\n const totalFormatted = r.totalPixels?.toLocaleString() ?? \"0\";\n details = `<div class=\"result-details\">diff: ${diffFormatted}% (${pixelsFormatted} / ${totalFormatted} pixels)</div>`;\n }\n\n let images = \"\";\n if (!r.error && !r.passed && r.diffPath) {\n images = `<div class=\"result-images\" data-baseline=\"file://${r.snapshotPath}\" data-current=\"file://${r.currentPath}\" data-diff=\"file://${r.diffPath}\">\n ${r.snapshotPath ? `<div class=\"image-container\"><div class=\"image-label\">Baseline</div><div class=\"image-wrapper\"><img src=\"file://${r.snapshotPath}\" alt=\"Baseline\" loading=\"lazy\" /></div></div>` : \"\"}\n ${r.currentPath ? `<div class=\"image-container\"><div class=\"image-label\">Current</div><div class=\"image-wrapper\"><img src=\"file://${r.currentPath}\" alt=\"Current\" loading=\"lazy\" /></div></div>` : \"\"}\n ${r.diffPath ? `<div class=\"image-container\"><div class=\"image-label\">Diff</div><div class=\"image-wrapper\"><img src=\"file://${r.diffPath}\" alt=\"Diff\" loading=\"lazy\" /></div></div>` : \"\"}\n </div>`;\n }\n\n const hasBody = details || images;\n\n return `<div class=\"result ${status}\" data-status=\"${status}\">\n <div class=\"result-header\">\n <div class=\"result-info\">\n <div class=\"result-name\">${escapeHtml(artName)} / ${escapeHtml(r.variantName)}</div>\n <div class=\"result-meta\">${escapeHtml(viewportName)}</div>\n </div>\n <span class=\"result-badge\">${badge}</span>\n </div>\n ${hasBody ? `<div class=\"result-body\">${details}${images}</div>` : \"\"}\n </div>`;\n })\n .join(\"\")\n }\n </div>\n </main>\n\n <script>\n // Filter buttons\n document.querySelectorAll('.filter-btn').forEach(btn => {\n btn.addEventListener('click', () => {\n document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));\n btn.classList.add('active');\n const filter = btn.dataset.filter;\n document.querySelectorAll('.result').forEach(result => {\n result.style.display = (filter === 'all' || result.dataset.status === filter) ? 'block' : 'none';\n });\n });\n });\n\n // Compare mode buttons\n document.querySelectorAll('.compare-mode-btn').forEach(btn => {\n btn.addEventListener('click', () => {\n document.querySelectorAll('.compare-mode-btn').forEach(b => b.classList.remove('active'));\n btn.classList.add('active');\n // Mode switching would update result-images display; this is a static report for now\n });\n });\n </script>\n</body>\n</html>`;\n\n return html;\n}\n\n/**\n * Generate VRT JSON report for CI integration.\n */\nexport function generateVrtJsonReport(results: VrtResult[], summary: VrtSummary): string {\n return JSON.stringify(\n {\n timestamp: new Date().toISOString(),\n summary,\n results: results.map((r) => ({\n art: path.basename(r.artPath, \".art.vue\"),\n variant: r.variantName,\n viewport: r.viewport.name || `${r.viewport.width}x${r.viewport.height}`,\n status: r.error ? \"error\" : r.isNew ? \"new\" : r.passed ? \"passed\" : \"failed\",\n diffPercentage: r.diffPercentage,\n error: r.error,\n })),\n },\n null,\n 2,\n );\n}\n","/**\n * Visual Regression Testing (VRT) module for Musea.\n * Uses Playwright for browser automation and pixel comparison.\n *\n * This file re-exports from the split VRT submodules for backward compatibility.\n */\n\nexport {\n MuseaVrtRunner,\n type VrtResult,\n type VrtSummary,\n type ExtendedVrtOptions,\n type PixelCompareOptions,\n} from \"./vrt/runner.js\";\n\nexport { generateVrtReport, generateVrtJsonReport } from \"./vrt/report.js\";\n\nimport { MuseaVrtRunner } from \"./vrt/runner.js\";\nexport default MuseaVrtRunner;\n"],"mappings":"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"}
|
package/dist/vrt.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as PixelCompareOptions, i as ExtendedVrtOptions, n as generateVrtReport, o as VrtResult, r as MuseaVrtRunner, s as VrtSummary, t as generateVrtJsonReport } from "./vrt-
|
|
2
|
-
export { ExtendedVrtOptions, MuseaVrtRunner, MuseaVrtRunner as default, PixelCompareOptions, VrtResult, VrtSummary, generateVrtJsonReport, generateVrtReport };
|
|
1
|
+
import { a as PixelCompareOptions, i as ExtendedVrtOptions, n as generateVrtReport, o as VrtResult, r as MuseaVrtRunner, s as VrtSummary, t as generateVrtJsonReport } from "./vrt-D454uyH6.mjs";
|
|
2
|
+
export { type ExtendedVrtOptions, MuseaVrtRunner, MuseaVrtRunner as default, type PixelCompareOptions, type VrtResult, type VrtSummary, generateVrtJsonReport, generateVrtReport };
|
package/dist/vrt.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as MuseaVrtRunner, n as generateVrtJsonReport, r as generateVrtReport, t as vrt_default } from "./vrt-
|
|
1
|
+
import { i as MuseaVrtRunner, n as generateVrtJsonReport, r as generateVrtReport, t as vrt_default } from "./vrt-DgVSWKGl.mjs";
|
|
2
2
|
export { MuseaVrtRunner, vrt_default as default, generateVrtJsonReport, generateVrtReport };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizejs/vite-plugin-musea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.350.2",
|
|
4
4
|
"description": "Vite plugin for Musea - Component gallery for Vue components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"component-gallery",
|
|
@@ -62,29 +62,29 @@
|
|
|
62
62
|
"access": "public"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@vizejs/native": "0.
|
|
65
|
+
"@vizejs/native": "0.350.2",
|
|
66
66
|
"pngjs": "7.0.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@mdi/js": "7.4.47",
|
|
70
|
-
"@tsdown/css": "0.22.
|
|
70
|
+
"@tsdown/css": "0.22.14",
|
|
71
71
|
"@types/node": "25.9.2",
|
|
72
72
|
"@types/pngjs": "6.0.5",
|
|
73
73
|
"@vitejs/plugin-vue": "6.0.7",
|
|
74
|
-
"@vizejs/vite-plugin": "0.
|
|
74
|
+
"@vizejs/vite-plugin": "0.350.2",
|
|
75
75
|
"highlight.js": "11.11.1",
|
|
76
|
-
"marked": "18.0.
|
|
76
|
+
"marked": "18.0.9",
|
|
77
77
|
"marked-highlight": "2.2.4",
|
|
78
|
-
"monaco-editor": "0.
|
|
79
|
-
"tsx": "4.
|
|
78
|
+
"monaco-editor": "0.56.0",
|
|
79
|
+
"tsx": "4.23.12",
|
|
80
80
|
"typescript": "6.0.3",
|
|
81
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@0.1.
|
|
82
|
-
"vite-plus": "0.1.
|
|
81
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.1.24",
|
|
82
|
+
"vite-plus": "0.1.24",
|
|
83
83
|
"vue": "3.5.35",
|
|
84
84
|
"vue-router": "4.5.1"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
|
-
"@vizejs/vite-plugin": "0.
|
|
87
|
+
"@vizejs/vite-plugin": "0.350.2",
|
|
88
88
|
"axe-core": "^4.7.0",
|
|
89
89
|
"playwright": "^1.40.0",
|
|
90
90
|
"vite": "^7.3.0 || ^8.0.0"
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/editor.main-BVHs2SSr.js","assets/index-De8nZlO8.js","assets/index-BD1UzCKA.css","assets/editor.api2-Bz2kYzUN.js","assets/editor-BnkT-hT5.css","assets/workers-Cruk8RxZ.js","assets/monaco.contribution-DurMprOc.js"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{a as e,c as t,d as n,f as r,i,l as a,n as o,o as s,r as c,s as l,t as u,u as d}from"./index-De8nZlO8.js";function f(e){return new Worker(`/__musea__/assets/editor.worker-Bd9IXS8d.js`,{name:e?.name})}function p(e){return new Worker(`/__musea__/assets/json.worker-DzV-CpCQ.js`,{name:e?.name})}function m(e){return new Worker(`/__musea__/assets/css.worker-Wv5dxAWO.js`,{name:e?.name})}function h(e){return new Worker(`/__musea__/assets/html.worker-CQP8QQsS.js`,{name:e?.name})}function g(e){return new Worker(`/__musea__/assets/ts.worker-METxwbDZ.js`,{name:e?.name})}var _=o(s({__name:`MonacoEditor`,props:{modelValue:{},language:{},theme:{},height:{},readOnly:{type:Boolean},completionItems:{}},emits:[`update:modelValue`],setup(o,{emit:s}){let _=o,v=s,{resolvedTheme:y}=c(),b=n(null),x=null,S=null,C=null,w=i(()=>y.value===`dark`?`musea-dark`:`musea-light`);t(async()=>{b.value&&(self.MonacoEnvironment={getWorker(e,t){switch(t){case`json`:return new p;case`css`:case`scss`:case`less`:return new m;case`html`:case`handlebars`:case`razor`:return new h;case`typescript`:case`javascript`:return new g;default:return new f}}},S=await u(()=>import(`./editor.main-BVHs2SSr.js`),__vite__mapDeps([0,1,2,3,4,5,6])),S.editor.defineTheme(`musea-dark`,{base:`vs-dark`,inherit:!0,rules:[],colors:{"editor.background":`#1a1a1a`,"editor.foreground":`#e5e5e5`,"editor.lineHighlightBackground":`#252525`,"editorCursor.foreground":`#e07048`,"editor.selectionBackground":`#3d3d3d`}}),S.editor.defineTheme(`musea-light`,{base:`vs`,inherit:!0,rules:[],colors:{"editor.background":`#ddd9cd`,"editor.foreground":`#121212`,"editor.lineHighlightBackground":`#d4d0c4`,"editorCursor.foreground":`#121212`,"editor.selectionBackground":`#c8c4b8`}}),_.completionItems&&_.completionItems.length>0&&T(S,_.completionItems),x=S.editor.create(b.value,{value:_.modelValue,language:_.language||`html`,theme:_.theme||w.value,minimap:{enabled:!1},fontSize:12,lineNumbers:`on`,lineNumbersMinChars:3,scrollBeyondLastLine:!1,wordWrap:`on`,automaticLayout:!0,readOnly:_.readOnly||!1,padding:{top:8,bottom:8},renderLineHighlight:`line`,scrollbar:{vertical:`auto`,horizontal:`auto`,verticalScrollbarSize:8,horizontalScrollbarSize:8},overviewRulerLanes:0,hideCursorInOverviewRuler:!0,overviewRulerBorder:!1,folding:!1,tabSize:2}),x.onDidChangeModelContent(()=>{v(`update:modelValue`,x?.getValue()||``)}))});function T(e,t){C?.dispose(),C=e.languages.registerCompletionItemProvider(`css`,{triggerCharacters:[`-`,`(`],provideCompletionItems(n,r){let i=n.getLineContent(r.lineNumber).substring(0,r.column-1),a=/var\(\s*-*$/.test(i),o=/:\s*[^;]*$/.test(i);if(!a&&!o)return{suggestions:[]};let s=n.getWordUntilPosition(r),c={startLineNumber:r.lineNumber,startColumn:s.startColumn,endLineNumber:r.lineNumber,endColumn:s.endColumn};return{suggestions:t.map(t=>{let n=`--${t.replace(/\./g,`-`)}`;return{label:n,kind:e.languages.CompletionItemKind.Variable,detail:`Design token: ${t}`,insertText:a?n:`var(${n})`,range:c,sortText:`0`+t}})}}}),C=e.languages.registerCompletionItemProvider(`html`,{triggerCharacters:[`-`,`(`],provideCompletionItems(n,r){let i=n.getLineContent(r.lineNumber).substring(0,r.column-1),a=/var\(\s*-*$/.test(i),o=/:\s*[^;]*$/.test(i);if(!a&&!o)return{suggestions:[]};let s=n.getWordUntilPosition(r),c={startLineNumber:r.lineNumber,startColumn:s.startColumn,endLineNumber:r.lineNumber,endColumn:s.endColumn};return{suggestions:t.map(t=>{let n=`--${t.replace(/\./g,`-`)}`;return{label:n,kind:e.languages.CompletionItemKind.Variable,detail:`Design token: ${t}`,insertText:a?n:`var(${n})`,range:c,sortText:`0`+t}})}}})}return l(()=>{C?.dispose(),x?.dispose()}),d(()=>_.modelValue,e=>{x&&x.getValue()!==e&&x.setValue(e)}),d(()=>_.language,e=>{if(x&&S&&e){let t=x.getModel();t&&S.editor.setModelLanguage(t,e)}}),d(()=>_.completionItems,e=>{S&&e&&e.length>0&&T(S,e)}),d(w,e=>{S&&!_.theme&&S.editor.setTheme(e)}),(t,n)=>(a(),e(`div`,{ref_key:`containerRef`,ref:b,class:`monaco-container`,style:r({height:o.height||`120px`})},null,4))}}),[[`__scopeId`,`data-v-bbc42d64`]]);export{_ as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.monaco-container[data-v-bbc42d64]{border:1px solid var(--musea-border);border-radius:var(--musea-radius-sm);overflow:hidden}
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{h as e}from"./editor.api2-Bz2kYzUN.js";import{t}from"./workers-Cruk8RxZ.js";import{_ as n,a as r,c as i,d as a,f as o,g as s,h as c,i as l,l as u,m as d,n as f,o as p,p as m,r as h,s as g,t as _,u as v,v as y}from"./lspLanguageFeatures-BAQ0wWN5.js";var b=120*1e3,x=class{constructor(e){this._defaults=e,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&=(this._worker.dispose(),null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){this._worker&&Date.now()-this._lastUsedTime>b&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||=(this._worker=t({moduleId:`vs/language/css/cssWorker`,createWorker:()=>new Worker(new URL(`/__musea__/assets/css.worker-Wv5dxAWO.js`,``+import.meta.url),{type:`module`}),label:this._defaults.languageId,createData:{options:this._defaults.options,languageId:this._defaults.languageId}}),this._worker.getProxy()),this._client}getLanguageServiceWorker(...e){let t;return this._getClient().then(e=>{t=e}).then(t=>{if(this._worker)return this._worker.withSyncedResources(e)}).then(e=>t)}};function S(t){let n=[],s=[],c=new x(t);n.push(c);let g=(...e)=>c.getLanguageServiceWorker(...e);function y(){let{languageId:n,modeConfiguration:c}=t;w(s),c.completionItems&&s.push(e.registerCompletionItemProvider(n,new _(g,[`/`,`-`,`:`]))),c.hovers&&s.push(e.registerHoverProvider(n,new a(g))),c.documentHighlights&&s.push(e.registerDocumentHighlightProvider(n,new p(g))),c.definitions&&s.push(e.registerDefinitionProvider(n,new f(g))),c.references&&s.push(e.registerReferenceProvider(n,new o(g))),c.documentSymbols&&s.push(e.registerDocumentSymbolProvider(n,new u(g))),c.rename&&s.push(e.registerRenameProvider(n,new m(g))),c.colors&&s.push(e.registerColorProvider(n,new l(g))),c.foldingRanges&&s.push(e.registerFoldingRangeProvider(n,new v(g))),c.diagnostics&&s.push(new h(n,g,t.onDidChange)),c.selectionRanges&&s.push(e.registerSelectionRangeProvider(n,new d(g))),c.documentFormattingEdits&&s.push(e.registerDocumentFormattingEditProvider(n,new r(g))),c.documentRangeFormattingEdits&&s.push(e.registerDocumentRangeFormattingEditProvider(n,new i(g)))}return y(),n.push(C(s)),C(n)}function C(e){return{dispose:()=>w(e)}}function w(e){for(;e.length;)e.pop().dispose()}export{_ as CompletionAdapter,f as DefinitionAdapter,h as DiagnosticsAdapter,l as DocumentColorAdapter,r as DocumentFormattingEditProvider,p as DocumentHighlightAdapter,g as DocumentLinkAdapter,i as DocumentRangeFormattingEditProvider,u as DocumentSymbolAdapter,v as FoldingRangeAdapter,a as HoverAdapter,o as ReferenceAdapter,m as RenameAdapter,d as SelectionRangeAdapter,x as WorkerManager,c as fromPosition,s as fromRange,S as setupMode,n as toRange,y as toTextEdit};
|