import { createServer } from "node:http"; import { readFile, stat, writeFile, mkdir } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import puppeteer from "puppeteer"; import { PUBLIC_ROUTES, NOT_FOUND_ROUTE } from "./routes.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const distDir = path.resolve(__dirname, "../dist"); const routes = PUBLIC_ROUTES.filter((route) => route.prerender).map( (route) => route.path ); const mimeTypes = { ".html": "text/html", ".js": "application/javascript", ".css": "text/css", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".avif": "image/avif", ".webp": "image/webp", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".txt": "text/plain", ".xml": "application/xml", ".map": "application/json", ".gz": "application/gzip", ".br": "application/brotli", }; function serveStaticFile(res, filePath) { const ext = path.extname(filePath).toLowerCase(); const contentType = mimeTypes[ext] ?? "application/octet-stream"; res.statusCode = 200; res.setHeader("Content-Type", contentType); return readFile(filePath).then((data) => res.end(data)); } function createStaticServer() { const server = createServer(async (req, res) => { try { const requestUrl = new URL(req.url ?? "/", "http://localhost"); let pathname = decodeURIComponent(requestUrl.pathname); const hasExtension = path.extname(pathname) !== ""; if (pathname.endsWith("/")) { pathname = pathname.slice(0, -1); } let filePath = path.join(distDir, pathname); try { const fileStat = await stat(filePath); if (fileStat.isDirectory()) { filePath = path.join(filePath, "index.html"); } await serveStaticFile(res, filePath); return; } catch (error) { if (hasExtension) { res.statusCode = 404; res.end("Not found"); return; } filePath = path.join(distDir, "index.html"); await serveStaticFile(res, filePath); } } catch (error) { res.statusCode = 500; res.end(`Server error: ${error instanceof Error ? error.message : error}`); } }); return new Promise((resolve) => { server.listen(0, () => { const address = server.address(); if (address && typeof address === "object") { resolve({ server, port: address.port }); } else { resolve({ server, port: 4173 }); } }); }); } const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function renderRoute(browser, baseUrl, route, outputFile) { const page = await browser.newPage(); const targetUrl = `${baseUrl}${route}`; try { await page.goto(targetUrl, { waitUntil: ["networkidle0", "domcontentloaded"], timeout: 60000, }); } catch (error) { console.warn( `[prerender] Navigation to ${targetUrl} failed: ${ error instanceof Error ? error.message : error }` ); } try { // Donne un léger délai pour laisser React terminer le rendu asynchrone await delay(1500); const html = await page.content(); const outputPath = outputFile ?? (route === "/" ? path.join(distDir, "index.html") : path.join(distDir, route.replace(/^\//, ""), "index.html")); await mkdir(path.dirname(outputPath), { recursive: true }); await writeFile(outputPath, html, "utf8"); console.log( `✔ prerendered ${route} -> ${path.relative(distDir, outputPath) || "index.html"}` ); } finally { await page.close(); } } async function main() { const { server, port } = await createStaticServer(); const baseUrl = `http://127.0.0.1:${port}`; const browser = await puppeteer.launch({ headless: true }); try { for (const route of routes) { await renderRoute(browser, baseUrl, route); } await renderRoute(browser, baseUrl, NOT_FOUND_ROUTE, path.join(distDir, "404.html")); } finally { await browser.close(); server.close(); } } main().catch((error) => { console.error("[prerender] Failed to prerender routes:", error); process.exit(1); });