/** * Local React-Email preview server. * * Run via `pnpm --filter @toolctrl/mail preview` and visit * http://localhost:3001 — the index lists every transactional * template; clicking a name renders the template with the sample * props from `previews.ts` so designers can iterate without * triggering live email sends. * * Intentionally tiny — uses Node's built-in `http` server so the * mail package keeps a small dep footprint. For production-grade * preview workflows (visual-regression diffing, screenshot capture) * mount the React-Email components in Storybook (Phase 08). */ import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { reactTransactionalTemplateNames, renderTransactionalEmail } from "../src/templates-react/render.js"; import { samplePropsByTemplate } from "./previews.js"; const PORT = Number.parseInt(process.env.MAIL_PREVIEW_PORT ?? "3001", 10); function escapeHtml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function renderIndex(): string { const names = reactTransactionalTemplateNames(); const items = names .map((name) => `
  • ${escapeHtml(name)} · text
  • `) .join(""); return `Mail Preview

    Mail template preview

    Browsing ${names.length} transactional templates. Each entry links to the rendered HTML and a plain-text fallback.

    `; } function handle(req: IncomingMessage, res: ServerResponse): void { const url = new URL(req.url ?? "/", `http://localhost:${PORT}`); if (url.pathname === "/" || url.pathname === "/preview") { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(renderIndex()); return; } const match = /^\/preview\/(.+)$/.exec(url.pathname); if (!match) { res.writeHead(404, { "content-type": "text/plain" }); res.end("not found"); return; } const templateName = match[1] as ReturnType[number]; const sample = samplePropsByTemplate[templateName]; if (!sample) { res.writeHead(404, { "content-type": "text/plain" }); res.end(`unknown template: ${templateName}`); return; } try { const rendered = renderTransactionalEmail(templateName, sample); if (url.searchParams.get("format") === "text") { res.writeHead(200, { "content-type": "text/plain; charset=utf-8" }); res.end(rendered.text); return; } res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(rendered.html); } catch (error) { res.writeHead(500, { "content-type": "text/plain" }); res.end(`render failed: ${(error as Error).message}`); } } const server = createServer(handle); server.listen(PORT, () => { // eslint-disable-next-line no-console console.log(`📧 Mail preview server listening on http://localhost:${PORT}`); });