import { existsSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import type { ReportData } from "./data"; const here = path.dirname(fileURLToPath(import.meta.url)); const web = path.resolve(here, "../../../web"); const modules = path.resolve(process.env.E2E_WEB_MODULES ?? path.join(web, "node_modules")); const fonts = path.join(web, "static"); interface Resolver { resolve: (id: string, importer: string | undefined, options: object) => Promise; } const bare = /^(?![./])(@[\w.-]+\/)?[\w.-]+(\/|$)/; const [input, out] = process.argv.slice(2); if (!existsSync(path.join(modules, "vite"))) { console.error( `${modules} has no vite, run pnpm install in web/ or pass --web-modules=` ); process.exit(1); } const require = createRequire(path.join(modules, "noop.js")); const load = (name: string, from = require) => import(pathToFileURL(from.resolve(name)).href); // tailwind only searches up from web/src, so it misses packages installed elsewhere // take the esm entry, cjs gives tailwind a module object instead of the plugin const entry = (condition: "style" | "import") => async (id: string) => { if (!bare.test(id)) return undefined; const manifest = path.join(modules, id, "package.json"); const pkg = JSON.parse(await readFile(manifest, "utf8")); const fallback = condition === "style" ? pkg.style : (pkg.module ?? pkg.main); return path.join(path.dirname(manifest), pkg.exports?.["."]?.[condition] ?? fallback); }; const renderHtml = async (data: ReportData) => { const { createServer } = await load("vite"); const { svelte } = await load("@sveltejs/vite-plugin-svelte"); const { default: icons } = await load("unplugin-icons/vite"); const server = await createServer({ configFile: false, root: web, logLevel: "error", // vite's default cache would land in web/node_modules cacheDir: path.join(tmpdir(), "e2e-report-vite"), appType: "custom", server: { middlewareMode: true, hmr: false, ws: false, watch: null }, optimizeDeps: { noDiscovery: true, include: [] }, ssr: { noExternal: true }, // the tsconfig extends .svelte-kit, which only exists after svelte-kit sync oxc: { tsconfig: false }, resolve: { alias: [ { find: /^\$lib\//, replacement: `${web}/src/lib/` }, { find: /^\$icon\//, replacement: "~icons/lucide/" } ] }, plugins: [ // web/ may have no node_modules, so bare imports fall back to ours { name: "web-modules", enforce: "pre", async resolveId( this: Resolver, id: string, importer: string | undefined, options: object ) { if (!bare.test(id)) return null; const own = await this.resolve(id, importer, { ...options, skipSelf: true }); const fallback = path.join(modules, "noop.js"); return own ?? this.resolve(id, fallback, { ...options, skipSelf: true }); } }, svelte({ configFile: false, compilerOptions: { css: "injected", runes: true, experimental: { async: true } } }), icons({ compiler: "svelte", collectionsNodeResolvePath: path.dirname(modules) }) ] }); try { const { render } = await server.ssrLoadModule("svelte/server"); const { default: Report } = await server.ssrLoadModule(path.join(here, "Report.svelte")); return await render(Report, { props: { data } }); } finally { await server.close(); } }; const buildCss = async (html: string) => { const node = await load( "@tailwindcss/node", createRequire(require.resolve("@tailwindcss/vite")) ); const compiler = await node.compile(`@import "${web}/src/app.css";`, { base: path.join(web, "src"), onDependency: () => {}, customCssResolver: entry("style"), customJsResolver: entry("import") }); const candidates = new Set(); for (const [, list] of html.matchAll(/class="([^"]*)"/g)) for (const name of list.split(/\s+/)) if (name) candidates.add(name); return inlineFonts(compiler.build([...candidates])); }; // the report opens from disk so /fonts urls miss, and italics are cut for size const inlineFonts = async (css: string) => { const faces = css.match(/@font-face\s*{[^}]*}/g) ?? []; for (const face of faces) { const url = face.match(/url\("?(\/fonts\/[^")]+)"?\)/)?.[1]; const file = url && path.join(fonts, url); if (/font-style:\s*italic/.test(face) || !file || !existsSync(file)) { css = css.replace(face, ""); continue; } const data = `data:font/woff2;base64,${(await readFile(file)).toString("base64")}`; css = css.replace(face, face.replace(url, data)); } return css; }; const data: ReportData = JSON.parse(await readFile(input, "utf8")); const { head, body } = await renderHtml(data); const css = await buildCss(body); const script = await readFile(path.join(here, "search.js"), "utf8"); await writeFile( out, ` e2e report ${head} ${body} ` );