@raypx-kit/email-preview 0.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +201 -0
- package/README.md +214 -0
- package/dist/THIRD_PARTY_LICENSES.txt +1218 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.mjs +467 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ui/assets/index-BbjBen8c.css +2 -0
- package/dist/ui/assets/index-Hn03Ok93.js +15 -0
- package/dist/ui/index.html +59 -0
- package/examples/_components/footer.tsx +12 -0
- package/examples/account/password-reset.jsx +58 -0
- package/examples/static/mark.svg +5 -0
- package/examples/vite.config.ts +18 -0
- package/examples/welcome.tsx +64 -0
- package/package.json +80 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
interface EmailPreviewOptions {
|
|
3
|
+
/** Template directory relative to the Vite root. Defaults to `emails`. */
|
|
4
|
+
dir?: string;
|
|
5
|
+
/** URL prefix for the interface and its API. Defaults to `/__email`. */
|
|
6
|
+
base?: string;
|
|
7
|
+
/** Rendering timeout in milliseconds. Defaults to 10000. */
|
|
8
|
+
renderTimeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Adds the React Email preview to a Vite dev server. Builds are unaffected. */
|
|
11
|
+
export declare function emailPreview(options?: EmailPreviewOptions): Plugin;
|
|
12
|
+
export type { EmailPreviewOptions };
|
|
13
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { createRunnableDevEnvironment, isRunnableDevEnvironment } from "vite";
|
|
3
|
+
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import * as html from "prettier/plugins/html";
|
|
6
|
+
import { format } from "prettier/standalone";
|
|
7
|
+
// src/server/errors.ts
|
|
8
|
+
var PreviewRuntimeError = class extends Error {
|
|
9
|
+
code;
|
|
10
|
+
status;
|
|
11
|
+
constructor(code, message, status = 500) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "PreviewRuntimeError";
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.status = status;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function errorMessage(error) {
|
|
19
|
+
return error instanceof Error ? error.message : String(error);
|
|
20
|
+
}
|
|
21
|
+
// src/server/discovery.ts
|
|
22
|
+
function isContained(root, path) {
|
|
23
|
+
const name = relative(root, path);
|
|
24
|
+
return name === "" || !isAbsolute(name) && name !== ".." && !name.startsWith(`..${sep}`);
|
|
25
|
+
}
|
|
26
|
+
async function resolveDirectory(cwd, directory) {
|
|
27
|
+
const requested = resolve(cwd, directory);
|
|
28
|
+
try {
|
|
29
|
+
const path = await realpath(requested);
|
|
30
|
+
if (!(await stat(path)).isDirectory()) throw new Error("Not a directory");
|
|
31
|
+
return path;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
34
|
+
const parent = dirname(requested);
|
|
35
|
+
if (parent !== requested) return resolve(await resolveDirectory(parent, "."), basename(requested));
|
|
36
|
+
}
|
|
37
|
+
throw new PreviewRuntimeError("DIRECTORY_NOT_FOUND", `Email directory not found: ${directory}`, 400);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Never use an HTTP-supplied path as a filesystem path without this boundary. */
|
|
41
|
+
async function resolveContainedFile(root, name) {
|
|
42
|
+
if (!name || name.includes("\\") || name.includes("\0") || isAbsolute(name)) throw new PreviewRuntimeError("NOT_FOUND", "File not found", 404);
|
|
43
|
+
const path = resolve(root, name);
|
|
44
|
+
if (!isContained(root, path)) throw new PreviewRuntimeError("NOT_FOUND", "File not found", 404);
|
|
45
|
+
try {
|
|
46
|
+
const canonical = await realpath(path);
|
|
47
|
+
if (!isContained(root, canonical) || !(await stat(canonical)).isFile()) throw new Error("Outside directory or not a file");
|
|
48
|
+
return canonical;
|
|
49
|
+
} catch {
|
|
50
|
+
throw new PreviewRuntimeError("NOT_FOUND", "File not found", 404);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function discoverTemplates(root) {
|
|
54
|
+
const templates = [];
|
|
55
|
+
async function visit(directory) {
|
|
56
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_") || entry.name === "static") continue;
|
|
59
|
+
const path = resolve(directory, entry.name);
|
|
60
|
+
if (entry.isDirectory()) await visit(path);
|
|
61
|
+
else if (entry.isFile() && /\.(tsx|jsx)$/.test(entry.name)) {
|
|
62
|
+
const id = relative(root, path).split(sep).join("/");
|
|
63
|
+
const slash = id.lastIndexOf("/");
|
|
64
|
+
templates.push({
|
|
65
|
+
id,
|
|
66
|
+
name: entry.name.replace(/\.(tsx|jsx)$/, ""),
|
|
67
|
+
directory: slash < 0 ? "" : id.slice(0, slash)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await visit(root);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
|
|
76
|
+
}
|
|
77
|
+
return templates.sort((left, right) => left.id.localeCompare(right.id));
|
|
78
|
+
}
|
|
79
|
+
// src/server/middleware.ts
|
|
80
|
+
const noCache = {
|
|
81
|
+
"Cache-Control": "no-store",
|
|
82
|
+
"X-Content-Type-Options": "nosniff"
|
|
83
|
+
};
|
|
84
|
+
const interfaceTypes = {
|
|
85
|
+
".css": "text/css; charset=utf-8",
|
|
86
|
+
".html": "text/html; charset=utf-8",
|
|
87
|
+
".js": "text/javascript; charset=utf-8",
|
|
88
|
+
".svg": "image/svg+xml",
|
|
89
|
+
".woff2": "font/woff2"
|
|
90
|
+
};
|
|
91
|
+
function send(response, status, headers, body) {
|
|
92
|
+
response.writeHead(status, headers);
|
|
93
|
+
response.end(body);
|
|
94
|
+
}
|
|
95
|
+
function sendJson(response, status, value) {
|
|
96
|
+
send(response, status, {
|
|
97
|
+
...noCache,
|
|
98
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
99
|
+
}, JSON.stringify(value));
|
|
100
|
+
}
|
|
101
|
+
function sendError(response, error) {
|
|
102
|
+
const expected = error instanceof PreviewRuntimeError;
|
|
103
|
+
sendJson(response, expected ? error.status : 500, { error: {
|
|
104
|
+
code: expected ? error.code : "INTERNAL_ERROR",
|
|
105
|
+
message: expected ? error.message : "The preview request failed. Try again after checking the template."
|
|
106
|
+
} });
|
|
107
|
+
}
|
|
108
|
+
function openEvents(runtime, request, response) {
|
|
109
|
+
response.writeHead(200, {
|
|
110
|
+
...noCache,
|
|
111
|
+
"Content-Type": "text/event-stream",
|
|
112
|
+
Connection: "keep-alive",
|
|
113
|
+
"X-Accel-Buffering": "no"
|
|
114
|
+
});
|
|
115
|
+
const send = (event, revision) => response.write(`event: ${event}\ndata: ${JSON.stringify({ revision })}\n\n`);
|
|
116
|
+
const unsubscribe = runtime.subscribe((revision) => send("change", revision));
|
|
117
|
+
const heartbeat = setInterval(() => response.write(": heartbeat\n\n"), 15e3);
|
|
118
|
+
heartbeat.unref();
|
|
119
|
+
let untrack = () => {};
|
|
120
|
+
const close = () => {
|
|
121
|
+
clearInterval(heartbeat);
|
|
122
|
+
unsubscribe();
|
|
123
|
+
untrack();
|
|
124
|
+
if (!response.writableEnded) response.end();
|
|
125
|
+
};
|
|
126
|
+
untrack = runtime.trackStream(close);
|
|
127
|
+
request.once("close", close);
|
|
128
|
+
send("ready", runtime.revision);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Serves the prebuilt interface and its API below `base`, which ends with a slash.
|
|
132
|
+
* Requests outside `base` continue to the host server's own middleware.
|
|
133
|
+
*/
|
|
134
|
+
function previewMiddleware(base, interfaceDirectory, getRuntime) {
|
|
135
|
+
let indexHtml;
|
|
136
|
+
const readIndex = () => indexHtml ??= readFile(`${interfaceDirectory}/index.html`, "utf8").then((html) => html.replace("<head>", `<head><base href="${base}" />`), () => {
|
|
137
|
+
indexHtml = void 0;
|
|
138
|
+
throw new PreviewRuntimeError("NOT_FOUND", "The preview interface has not been built.", 404);
|
|
139
|
+
});
|
|
140
|
+
return async (request, response, next) => {
|
|
141
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
142
|
+
const mount = base.slice(0, -1);
|
|
143
|
+
if (mount && url.pathname === mount) {
|
|
144
|
+
send(response, 302, { Location: `${base}${url.search}` });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!url.pathname.startsWith(base)) return next();
|
|
148
|
+
if (request.method !== "GET" && request.method !== "HEAD") return next();
|
|
149
|
+
const path = url.pathname.slice(base.length);
|
|
150
|
+
try {
|
|
151
|
+
if (path === "api/templates") sendJson(response, 200, await (await getRuntime()).listTemplates());
|
|
152
|
+
else if (path === "api/preview") {
|
|
153
|
+
const template = url.searchParams.get("template");
|
|
154
|
+
if (!template) throw new PreviewRuntimeError("INVALID_TEMPLATE", "Select an email template.", 400);
|
|
155
|
+
sendJson(response, 200, await (await getRuntime()).renderTemplate(template));
|
|
156
|
+
} else if (path === "api/events") openEvents(await getRuntime(), request, response);
|
|
157
|
+
else if (path.startsWith("static/")) {
|
|
158
|
+
const file = await (await getRuntime()).getStaticFile(decodeURIComponent(path.slice(7)));
|
|
159
|
+
send(response, 200, {
|
|
160
|
+
...noCache,
|
|
161
|
+
"Content-Type": file.contentType,
|
|
162
|
+
"Content-Security-Policy": "sandbox"
|
|
163
|
+
}, file.data);
|
|
164
|
+
} else if (path === "" || path === "index.html") send(response, 200, {
|
|
165
|
+
...noCache,
|
|
166
|
+
"Content-Type": interfaceTypes[".html"]
|
|
167
|
+
}, await readIndex());
|
|
168
|
+
else if (path.startsWith("assets/")) {
|
|
169
|
+
const file = await resolveContainedFile(interfaceDirectory, path).catch(() => void 0);
|
|
170
|
+
if (!file) return next();
|
|
171
|
+
send(response, 200, {
|
|
172
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
173
|
+
"Content-Type": interfaceTypes[extname(file)] ?? "application/octet-stream",
|
|
174
|
+
"X-Content-Type-Options": "nosniff"
|
|
175
|
+
}, await readFile(file));
|
|
176
|
+
} else return next();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (response.headersSent) response.destroy();
|
|
179
|
+
else sendError(response, error);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// src/server/render.ts
|
|
184
|
+
/** The Vite environment that evaluates templates, separate from the application's `ssr`. */
|
|
185
|
+
const environmentName = "email_preview";
|
|
186
|
+
const entryPrefix = "virtual:email-preview-render:";
|
|
187
|
+
const resolvedEntryPrefix = `\0${entryPrefix}`;
|
|
188
|
+
const rendererPath = fileURLToPath(import.meta.resolve("@react-email/render"));
|
|
189
|
+
function renderEntryId(template) {
|
|
190
|
+
return `${entryPrefix}${template}`;
|
|
191
|
+
}
|
|
192
|
+
function resolveRenderEntry(id) {
|
|
193
|
+
return id.startsWith(entryPrefix) ? `\0${id}` : void 0;
|
|
194
|
+
}
|
|
195
|
+
/** Renders one template with its `PreviewProps`, importing React through the project. */
|
|
196
|
+
function loadRenderEntry(id) {
|
|
197
|
+
if (!id.startsWith(resolvedEntryPrefix)) return;
|
|
198
|
+
const template = id.slice(resolvedEntryPrefix.length);
|
|
199
|
+
return `import { createElement } from "react";
|
|
200
|
+
import { render, toPlainText } from ${JSON.stringify(rendererPath)};
|
|
201
|
+
import * as email from ${JSON.stringify(template)};
|
|
202
|
+
export async function renderEmail() {
|
|
203
|
+
const Component = email.default;
|
|
204
|
+
if (!Component || (typeof Component !== "function" && typeof Component !== "object")) {
|
|
205
|
+
throw new Error("The email template must default-export a React component.");
|
|
206
|
+
}
|
|
207
|
+
const html = await render(createElement(Component, Component.PreviewProps ?? {}));
|
|
208
|
+
return { html, text: toPlainText(html) };
|
|
209
|
+
}`;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Reads the project's manifest straight from disk. Bun caches failed module
|
|
213
|
+
* lookups in-process, so `require.resolve` would miss a later install.
|
|
214
|
+
*/
|
|
215
|
+
async function readProjectManifest(name, from) {
|
|
216
|
+
for (let directory = dirname(from);; directory = dirname(directory)) {
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(await readFile(join(directory, "node_modules", name, "package.json"), "utf8"));
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
221
|
+
}
|
|
222
|
+
if (dirname(directory) === directory) return {};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function assertReact(template) {
|
|
226
|
+
for (const name of ["react", "react-dom"]) if (!(await readProjectManifest(name, template)).version?.startsWith("19.")) throw new Error(`Install ${name} 19 in the project containing your email templates.`);
|
|
227
|
+
}
|
|
228
|
+
// src/server/format.ts
|
|
229
|
+
/**
|
|
230
|
+
* Indents rendered HTML for the source view. The preview, clipboard and downloads
|
|
231
|
+
* keep the original output, so markup Prettier cannot parse is shown unchanged.
|
|
232
|
+
*/
|
|
233
|
+
async function formatHtml(source) {
|
|
234
|
+
try {
|
|
235
|
+
return await format(source, {
|
|
236
|
+
parser: "html",
|
|
237
|
+
plugins: [html],
|
|
238
|
+
endOfLine: "lf",
|
|
239
|
+
tabWidth: 2,
|
|
240
|
+
bracketSameLine: true
|
|
241
|
+
});
|
|
242
|
+
} catch {
|
|
243
|
+
return source;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// src/server/static.ts
|
|
247
|
+
const contentTypes = {
|
|
248
|
+
".avif": "image/avif",
|
|
249
|
+
".css": "text/css; charset=utf-8",
|
|
250
|
+
".gif": "image/gif",
|
|
251
|
+
".ico": "image/x-icon",
|
|
252
|
+
".jpeg": "image/jpeg",
|
|
253
|
+
".jpg": "image/jpeg",
|
|
254
|
+
".png": "image/png",
|
|
255
|
+
".svg": "image/svg+xml",
|
|
256
|
+
".ttf": "font/ttf",
|
|
257
|
+
".webp": "image/webp",
|
|
258
|
+
".woff": "font/woff",
|
|
259
|
+
".woff2": "font/woff2"
|
|
260
|
+
};
|
|
261
|
+
async function readStaticFile(directory, name) {
|
|
262
|
+
let root;
|
|
263
|
+
try {
|
|
264
|
+
root = await realpath(resolve(directory, "static"));
|
|
265
|
+
} catch {
|
|
266
|
+
throw new PreviewRuntimeError("NOT_FOUND", "Static file not found", 404);
|
|
267
|
+
}
|
|
268
|
+
if (!isContained(directory, root)) throw new PreviewRuntimeError("NOT_FOUND", "Static file not found", 404);
|
|
269
|
+
const path = await resolveContainedFile(root, name);
|
|
270
|
+
return {
|
|
271
|
+
data: await readFile(path),
|
|
272
|
+
contentType: contentTypes[extname(path).toLowerCase()] ?? "application/octet-stream"
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
// src/server/runtime.ts
|
|
276
|
+
function isRenderedEmail(value) {
|
|
277
|
+
return typeof value === "object" && value !== null && "html" in value && typeof value.html === "string" && "text" in value && typeof value.text === "string";
|
|
278
|
+
}
|
|
279
|
+
var PreviewRuntime = class PreviewRuntime {
|
|
280
|
+
server;
|
|
281
|
+
directory;
|
|
282
|
+
timeoutMs;
|
|
283
|
+
listeners = /* @__PURE__ */ new Set();
|
|
284
|
+
streams = /* @__PURE__ */ new Set();
|
|
285
|
+
notification;
|
|
286
|
+
closed = false;
|
|
287
|
+
currentRevision = 0;
|
|
288
|
+
constructor(server, directory, timeoutMs) {
|
|
289
|
+
this.server = server;
|
|
290
|
+
this.directory = directory;
|
|
291
|
+
this.timeoutMs = timeoutMs;
|
|
292
|
+
}
|
|
293
|
+
static async create(server, options) {
|
|
294
|
+
if (!Number.isFinite(options.renderTimeoutMs) || options.renderTimeoutMs < 1) throw new PreviewRuntimeError("INVALID_OPTIONS", "renderTimeoutMs must be a positive number.", 400);
|
|
295
|
+
const directory = await resolveDirectory(server.config.root, options.directory);
|
|
296
|
+
const runtime = new PreviewRuntime(server, directory, options.renderTimeoutMs);
|
|
297
|
+
server.watcher.add(directory);
|
|
298
|
+
server.watcher.on("all", runtime.onFileEvent);
|
|
299
|
+
return runtime;
|
|
300
|
+
}
|
|
301
|
+
get environment() {
|
|
302
|
+
const environment = this.server.environments[environmentName];
|
|
303
|
+
if (!environment || !isRunnableDevEnvironment(environment)) throw new PreviewRuntimeError("NOT_INITIALIZED", "The email preview environment is not available.", 503);
|
|
304
|
+
return environment;
|
|
305
|
+
}
|
|
306
|
+
onFileEvent = (_event, file) => {
|
|
307
|
+
const path = resolve(file);
|
|
308
|
+
const environment = this.server.environments[environmentName];
|
|
309
|
+
if (!isContained(this.directory, path) && basename(path) !== "tsconfig.json" && !environment?.moduleGraph.getModulesByFile(path)?.size) return;
|
|
310
|
+
if (environment && isRunnableDevEnvironment(environment)) {
|
|
311
|
+
environment.moduleGraph.invalidateAll();
|
|
312
|
+
environment.runner.clearCache();
|
|
313
|
+
}
|
|
314
|
+
this.invalidate();
|
|
315
|
+
};
|
|
316
|
+
get revision() {
|
|
317
|
+
return this.currentRevision;
|
|
318
|
+
}
|
|
319
|
+
invalidate() {
|
|
320
|
+
if (this.closed) return;
|
|
321
|
+
this.currentRevision += 1;
|
|
322
|
+
if (this.notification) clearTimeout(this.notification);
|
|
323
|
+
this.notification = setTimeout(() => {
|
|
324
|
+
this.notification = void 0;
|
|
325
|
+
for (const listener of this.listeners) try {
|
|
326
|
+
listener(this.currentRevision);
|
|
327
|
+
} catch {
|
|
328
|
+
this.listeners.delete(listener);
|
|
329
|
+
}
|
|
330
|
+
}, 80);
|
|
331
|
+
}
|
|
332
|
+
assertOpen() {
|
|
333
|
+
if (this.closed) throw new PreviewRuntimeError("CLOSED", "The preview server is closing.", 503);
|
|
334
|
+
}
|
|
335
|
+
async listTemplates() {
|
|
336
|
+
this.assertOpen();
|
|
337
|
+
return {
|
|
338
|
+
directory: this.directory,
|
|
339
|
+
templates: await discoverTemplates(this.directory),
|
|
340
|
+
revision: this.revision
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async renderTemplate(id) {
|
|
344
|
+
this.assertOpen();
|
|
345
|
+
const { templates } = await this.listTemplates();
|
|
346
|
+
if (!templates.some((template) => template.id === id)) throw new PreviewRuntimeError("TEMPLATE_NOT_FOUND", "Email template not found.", 404);
|
|
347
|
+
const path = await resolveContainedFile(this.directory, id);
|
|
348
|
+
try {
|
|
349
|
+
await assertReact(path);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw new PreviewRuntimeError("DEPENDENCY_ERROR", errorMessage(error), 422);
|
|
352
|
+
}
|
|
353
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
354
|
+
this.assertOpen();
|
|
355
|
+
const revision = this.revision;
|
|
356
|
+
const result = await this.render(path);
|
|
357
|
+
if (this.revision === revision) return {
|
|
358
|
+
id,
|
|
359
|
+
revision,
|
|
360
|
+
...result,
|
|
361
|
+
formattedHtml: await formatHtml(result.html)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
throw new PreviewRuntimeError("TEMPLATE_CHANGED", "The template changed while rendering. Please try again.", 409);
|
|
365
|
+
}
|
|
366
|
+
async render(path) {
|
|
367
|
+
let entry;
|
|
368
|
+
try {
|
|
369
|
+
entry = await this.withTimeout(this.environment.runner.import(renderEntryId(path)));
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (error instanceof PreviewRuntimeError) throw error;
|
|
372
|
+
throw new PreviewRuntimeError("COMPILE_ERROR", errorMessage(error), 422);
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
if (typeof entry.renderEmail !== "function") throw new Error("Rendering failed to load.");
|
|
376
|
+
const value = await this.withTimeout(Promise.resolve(entry.renderEmail()));
|
|
377
|
+
if (!isRenderedEmail(value)) throw new Error("The template returned an invalid result.");
|
|
378
|
+
return value;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (error instanceof PreviewRuntimeError) throw error;
|
|
381
|
+
throw new PreviewRuntimeError("RENDER_ERROR", errorMessage(error), 422);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Templates run in the dev server process. A timeout reports work that never
|
|
386
|
+
* settles; it cannot interrupt a synchronous loop.
|
|
387
|
+
*/
|
|
388
|
+
withTimeout(work) {
|
|
389
|
+
let timer;
|
|
390
|
+
const timeout = new Promise((_, reject) => {
|
|
391
|
+
timer = setTimeout(() => reject(new PreviewRuntimeError("RENDER_TIMEOUT", `Template rendering exceeded ${this.timeoutMs} ms.`, 422)), this.timeoutMs);
|
|
392
|
+
});
|
|
393
|
+
return Promise.race([work, timeout]).finally(() => clearTimeout(timer));
|
|
394
|
+
}
|
|
395
|
+
subscribe(listener) {
|
|
396
|
+
this.assertOpen();
|
|
397
|
+
this.listeners.add(listener);
|
|
398
|
+
return () => this.listeners.delete(listener);
|
|
399
|
+
}
|
|
400
|
+
/** Registers an open event stream so shutdown can end it. */
|
|
401
|
+
trackStream(close) {
|
|
402
|
+
this.streams.add(close);
|
|
403
|
+
return () => this.streams.delete(close);
|
|
404
|
+
}
|
|
405
|
+
async getStaticFile(path) {
|
|
406
|
+
this.assertOpen();
|
|
407
|
+
return readStaticFile(this.directory, path);
|
|
408
|
+
}
|
|
409
|
+
close() {
|
|
410
|
+
if (this.closed) return;
|
|
411
|
+
this.closed = true;
|
|
412
|
+
if (this.notification) clearTimeout(this.notification);
|
|
413
|
+
this.server.watcher.off("all", this.onFileEvent);
|
|
414
|
+
for (const close of this.streams) close();
|
|
415
|
+
this.streams.clear();
|
|
416
|
+
this.listeners.clear();
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
// src/server/plugin.ts
|
|
420
|
+
function normalizeBase(base) {
|
|
421
|
+
const value = `/${base.replace(/^\/+|\/+$/g, "")}/`.replace(/^\/\/$/, "/");
|
|
422
|
+
if (!/^\/(?:[\w.-]+\/)*$/.test(value)) throw new Error(`Invalid email preview base: ${base}`);
|
|
423
|
+
return value;
|
|
424
|
+
}
|
|
425
|
+
/** Adds the React Email preview to a Vite dev server. Builds are unaffected. */
|
|
426
|
+
function emailPreview(options = {}) {
|
|
427
|
+
const { dir = "emails", renderTimeoutMs = 1e4, interfaceDirectory = fileURLToPath(new URL("./ui", import.meta.url)) } = options;
|
|
428
|
+
const base = normalizeBase(options.base ?? "/__email");
|
|
429
|
+
let runtime;
|
|
430
|
+
return {
|
|
431
|
+
name: "raypx-email-preview",
|
|
432
|
+
apply: "serve",
|
|
433
|
+
api: { runtime: () => runtime },
|
|
434
|
+
config() {
|
|
435
|
+
return { environments: { [environmentName]: {
|
|
436
|
+
consumer: "server",
|
|
437
|
+
resolve: {
|
|
438
|
+
dedupe: ["react", "react-dom"],
|
|
439
|
+
external: true,
|
|
440
|
+
noExternal: ["@react-email/render"]
|
|
441
|
+
},
|
|
442
|
+
dev: { createEnvironment: (name, config) => createRunnableDevEnvironment(name, config) }
|
|
443
|
+
} } };
|
|
444
|
+
},
|
|
445
|
+
resolveId(id) {
|
|
446
|
+
return resolveRenderEntry(id);
|
|
447
|
+
},
|
|
448
|
+
load(id) {
|
|
449
|
+
return loadRenderEntry(id);
|
|
450
|
+
},
|
|
451
|
+
configureServer(server) {
|
|
452
|
+
runtime = PreviewRuntime.create(server, {
|
|
453
|
+
directory: dir,
|
|
454
|
+
renderTimeoutMs
|
|
455
|
+
});
|
|
456
|
+
runtime.catch(() => {});
|
|
457
|
+
server.middlewares.use(previewMiddleware(base, interfaceDirectory, () => runtime));
|
|
458
|
+
server.httpServer?.once("close", () => void runtime?.then((value) => value.close()));
|
|
459
|
+
},
|
|
460
|
+
async buildEnd() {
|
|
461
|
+
(await runtime?.catch(() => void 0))?.close();
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
export { emailPreview };
|
|
466
|
+
|
|
467
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/server/errors.ts","../src/server/discovery.ts","../src/server/middleware.ts","../src/server/render.ts","../src/server/format.ts","../src/server/static.ts","../src/server/runtime.ts","../src/server/plugin.ts"],"sourcesContent":["export class PreviewRuntimeError extends Error {\n readonly code: string;\n readonly status: number;\n\n constructor(code: string, message: string, status = 500) {\n super(message);\n this.name = \"PreviewRuntimeError\";\n this.code = code;\n this.status = status;\n }\n}\n\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n","import { readdir, realpath, stat } from \"node:fs/promises\";\nimport { basename, dirname, isAbsolute, relative, resolve, sep } from \"node:path\";\nimport type { Template } from \"../types.js\";\nimport { PreviewRuntimeError } from \"./errors.js\";\n\nexport function isContained(root: string, path: string): boolean {\n const name = relative(root, path);\n return name === \"\" || (!isAbsolute(name) && name !== \"..\" && !name.startsWith(`..${sep}`));\n}\n\nexport async function resolveDirectory(cwd: string, directory: string): Promise<string> {\n const requested = resolve(cwd, directory);\n try {\n const path = await realpath(requested);\n if (!(await stat(path)).isDirectory()) throw new Error(\"Not a directory\");\n return path;\n } catch (error) {\n if (error instanceof Error && \"code\" in error && error.code === \"ENOENT\") {\n // A new project can start the preview before its emails folder exists.\n // Resolve existing ancestors so future path checks use one canonical root.\n const parent = dirname(requested);\n if (parent !== requested)\n return resolve(await resolveDirectory(parent, \".\"), basename(requested));\n }\n throw new PreviewRuntimeError(\n \"DIRECTORY_NOT_FOUND\",\n `Email directory not found: ${directory}`,\n 400,\n );\n }\n}\n\n/** Never use an HTTP-supplied path as a filesystem path without this boundary. */\nexport async function resolveContainedFile(root: string, name: string): Promise<string> {\n if (!name || name.includes(\"\\\\\") || name.includes(\"\\0\") || isAbsolute(name)) {\n throw new PreviewRuntimeError(\"NOT_FOUND\", \"File not found\", 404);\n }\n const path = resolve(root, name);\n if (!isContained(root, path)) throw new PreviewRuntimeError(\"NOT_FOUND\", \"File not found\", 404);\n try {\n const canonical = await realpath(path);\n if (!isContained(root, canonical) || !(await stat(canonical)).isFile()) {\n throw new Error(\"Outside directory or not a file\");\n }\n return canonical;\n } catch {\n throw new PreviewRuntimeError(\"NOT_FOUND\", \"File not found\", 404);\n }\n}\n\nexport async function discoverTemplates(root: string): Promise<Template[]> {\n const templates: Template[] = [];\n async function visit(directory: string): Promise<void> {\n const entries = await readdir(directory, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\") || entry.name.startsWith(\"_\") || entry.name === \"static\")\n continue;\n const path = resolve(directory, entry.name);\n if (entry.isDirectory()) {\n await visit(path);\n } else if (entry.isFile() && /\\.(tsx|jsx)$/.test(entry.name)) {\n const id = relative(root, path).split(sep).join(\"/\");\n const slash = id.lastIndexOf(\"/\");\n templates.push({\n id,\n name: entry.name.replace(/\\.(tsx|jsx)$/, \"\"),\n directory: slash < 0 ? \"\" : id.slice(0, slash),\n });\n }\n }\n }\n try {\n await visit(root);\n } catch (error) {\n if (!(error instanceof Error) || !(\"code\" in error) || error.code !== \"ENOENT\") throw error;\n }\n return templates.sort((left, right) => left.id.localeCompare(right.id));\n}\n","import { readFile } from \"node:fs/promises\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { extname } from \"node:path\";\nimport { resolveContainedFile } from \"./discovery.js\";\nimport { PreviewRuntimeError } from \"./errors.js\";\nimport type { PreviewRuntime } from \"./runtime.js\";\n\ntype Next = (error?: unknown) => void;\n\nconst noCache = { \"Cache-Control\": \"no-store\", \"X-Content-Type-Options\": \"nosniff\" };\n\nconst interfaceTypes: Readonly<Record<string, string>> = {\n \".css\": \"text/css; charset=utf-8\",\n \".html\": \"text/html; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".svg\": \"image/svg+xml\",\n \".woff2\": \"font/woff2\",\n};\n\nfunction send(\n response: ServerResponse,\n status: number,\n headers: Record<string, string>,\n body?: string | Uint8Array,\n) {\n response.writeHead(status, headers);\n response.end(body);\n}\n\nfunction sendJson(response: ServerResponse, status: number, value: unknown) {\n send(\n response,\n status,\n { ...noCache, \"Content-Type\": \"application/json; charset=utf-8\" },\n JSON.stringify(value),\n );\n}\n\nfunction sendError(response: ServerResponse, error: unknown) {\n const expected = error instanceof PreviewRuntimeError;\n sendJson(response, expected ? error.status : 500, {\n error: {\n code: expected ? error.code : \"INTERNAL_ERROR\",\n message: expected\n ? error.message\n : \"The preview request failed. Try again after checking the template.\",\n },\n });\n}\n\nfunction openEvents(runtime: PreviewRuntime, request: IncomingMessage, response: ServerResponse) {\n response.writeHead(200, {\n ...noCache,\n \"Content-Type\": \"text/event-stream\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\",\n });\n const send = (event: string, revision: number) =>\n response.write(`event: ${event}\\ndata: ${JSON.stringify({ revision })}\\n\\n`);\n const unsubscribe = runtime.subscribe((revision) => send(\"change\", revision));\n const heartbeat = setInterval(() => response.write(\": heartbeat\\n\\n\"), 15000);\n heartbeat.unref();\n let untrack = () => {};\n const close = () => {\n clearInterval(heartbeat);\n unsubscribe();\n untrack();\n if (!response.writableEnded) response.end();\n };\n untrack = runtime.trackStream(close);\n request.once(\"close\", close);\n send(\"ready\", runtime.revision);\n}\n\n/**\n * Serves the prebuilt interface and its API below `base`, which ends with a slash.\n * Requests outside `base` continue to the host server's own middleware.\n */\nexport function previewMiddleware(\n base: string,\n interfaceDirectory: string,\n getRuntime: () => Promise<PreviewRuntime>,\n) {\n let indexHtml: Promise<string> | undefined;\n const readIndex = () =>\n (indexHtml ??= readFile(`${interfaceDirectory}/index.html`, \"utf8\").then(\n // Relative asset and API URLs resolve against the mount point.\n (html) => html.replace(\"<head>\", `<head><base href=\"${base}\" />`),\n () => {\n indexHtml = undefined;\n throw new PreviewRuntimeError(\n \"NOT_FOUND\",\n \"The preview interface has not been built.\",\n 404,\n );\n },\n ));\n\n return async (request: IncomingMessage, response: ServerResponse, next: Next) => {\n const url = new URL(request.url ?? \"/\", \"http://localhost\");\n const mount = base.slice(0, -1);\n if (mount && url.pathname === mount) {\n send(response, 302, { Location: `${base}${url.search}` });\n return;\n }\n if (!url.pathname.startsWith(base)) return next();\n if (request.method !== \"GET\" && request.method !== \"HEAD\") return next();\n const path = url.pathname.slice(base.length);\n try {\n if (path === \"api/templates\") {\n sendJson(response, 200, await (await getRuntime()).listTemplates());\n } else if (path === \"api/preview\") {\n const template = url.searchParams.get(\"template\");\n if (!template) {\n throw new PreviewRuntimeError(\"INVALID_TEMPLATE\", \"Select an email template.\", 400);\n }\n sendJson(response, 200, await (await getRuntime()).renderTemplate(template));\n } else if (path === \"api/events\") {\n openEvents(await getRuntime(), request, response);\n } else if (path.startsWith(\"static/\")) {\n const file = await (await getRuntime()).getStaticFile(decodeURIComponent(path.slice(7)));\n send(\n response,\n 200,\n { ...noCache, \"Content-Type\": file.contentType, \"Content-Security-Policy\": \"sandbox\" },\n file.data,\n );\n } else if (path === \"\" || path === \"index.html\") {\n send(\n response,\n 200,\n { ...noCache, \"Content-Type\": interfaceTypes[\".html\"]! },\n await readIndex(),\n );\n } else if (path.startsWith(\"assets/\")) {\n const file = await resolveContainedFile(interfaceDirectory, path).catch(() => undefined);\n // Unknown paths belong to the host server, which matters when `base` is `/`.\n if (!file) return next();\n send(\n response,\n 200,\n {\n \"Cache-Control\": \"public, max-age=31536000, immutable\",\n \"Content-Type\": interfaceTypes[extname(file)] ?? \"application/octet-stream\",\n \"X-Content-Type-Options\": \"nosniff\",\n },\n await readFile(file),\n );\n } else {\n return next();\n }\n } catch (error) {\n if (response.headersSent) response.destroy();\n else sendError(response, error);\n }\n };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/** The Vite environment that evaluates templates, separate from the application's `ssr`. */\nexport const environmentName = \"email_preview\";\n\nconst entryPrefix = \"virtual:email-preview-render:\";\nconst resolvedEntryPrefix = `\\0${entryPrefix}`;\n// Resolved from this package, which depends on the renderer; the project may not.\n// The generated entry imports it by path inside the project's Vite environment, so\n// its `react` import resolves to the same React as the template.\nconst rendererPath = fileURLToPath(import.meta.resolve(\"@react-email/render\"));\n\nexport function renderEntryId(template: string): string {\n return `${entryPrefix}${template}`;\n}\n\nexport function resolveRenderEntry(id: string): string | undefined {\n return id.startsWith(entryPrefix) ? `\\0${id}` : undefined;\n}\n\n/** Renders one template with its `PreviewProps`, importing React through the project. */\nexport function loadRenderEntry(id: string): string | undefined {\n if (!id.startsWith(resolvedEntryPrefix)) return;\n const template = id.slice(resolvedEntryPrefix.length);\n return `import { createElement } from \"react\";\nimport { render, toPlainText } from ${JSON.stringify(rendererPath)};\nimport * as email from ${JSON.stringify(template)};\nexport async function renderEmail() {\n const Component = email.default;\n if (!Component || (typeof Component !== \"function\" && typeof Component !== \"object\")) {\n throw new Error(\"The email template must default-export a React component.\");\n }\n const html = await render(createElement(Component, Component.PreviewProps ?? {}));\n return { html, text: toPlainText(html) };\n}`;\n}\n\n/**\n * Reads the project's manifest straight from disk. Bun caches failed module\n * lookups in-process, so `require.resolve` would miss a later install.\n */\nasync function readProjectManifest(name: string, from: string): Promise<{ version?: string }> {\n for (let directory = dirname(from); ; directory = dirname(directory)) {\n try {\n return JSON.parse(\n await readFile(join(directory, \"node_modules\", name, \"package.json\"), \"utf8\"),\n );\n } catch (error) {\n if (!(error instanceof Error && \"code\" in error && error.code === \"ENOENT\")) throw error;\n }\n if (dirname(directory) === directory) return {};\n }\n}\n\nexport async function assertReact(template: string): Promise<void> {\n for (const name of [\"react\", \"react-dom\"]) {\n const manifest = await readProjectManifest(name, template);\n if (!manifest.version?.startsWith(\"19.\")) {\n throw new Error(`Install ${name} 19 in the project containing your email templates.`);\n }\n }\n}\n","import * as html from \"prettier/plugins/html\";\nimport { format } from \"prettier/standalone\";\n\n/**\n * Indents rendered HTML for the source view. The preview, clipboard and downloads\n * keep the original output, so markup Prettier cannot parse is shown unchanged.\n */\nexport async function formatHtml(source: string): Promise<string> {\n try {\n return await format(source, {\n parser: \"html\",\n plugins: [html],\n endOfLine: \"lf\",\n tabWidth: 2,\n bracketSameLine: true,\n });\n } catch {\n return source;\n }\n}\n","import { readFile, realpath } from \"node:fs/promises\";\nimport { extname, resolve } from \"node:path\";\nimport { isContained, resolveContainedFile } from \"./discovery.js\";\nimport { PreviewRuntimeError } from \"./errors.js\";\n\nconst contentTypes: Readonly<Record<string, string>> = {\n \".avif\": \"image/avif\",\n \".css\": \"text/css; charset=utf-8\",\n \".gif\": \"image/gif\",\n \".ico\": \"image/x-icon\",\n \".jpeg\": \"image/jpeg\",\n \".jpg\": \"image/jpeg\",\n \".png\": \"image/png\",\n \".svg\": \"image/svg+xml\",\n \".ttf\": \"font/ttf\",\n \".webp\": \"image/webp\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n};\n\nexport async function readStaticFile(\n directory: string,\n name: string,\n): Promise<{ data: Uint8Array<ArrayBuffer>; contentType: string }> {\n let root: string;\n try {\n root = await realpath(resolve(directory, \"static\"));\n } catch {\n throw new PreviewRuntimeError(\"NOT_FOUND\", \"Static file not found\", 404);\n }\n if (!isContained(directory, root)) {\n throw new PreviewRuntimeError(\"NOT_FOUND\", \"Static file not found\", 404);\n }\n const path = await resolveContainedFile(root, name);\n return {\n data: await readFile(path),\n contentType: contentTypes[extname(path).toLowerCase()] ?? \"application/octet-stream\",\n };\n}\n","import { basename, resolve } from \"node:path\";\nimport { isRunnableDevEnvironment } from \"vite\";\nimport type { RunnableDevEnvironment, ViteDevServer } from \"vite\";\nimport type { PreviewResult, TemplateList } from \"../types.js\";\nimport {\n discoverTemplates,\n isContained,\n resolveContainedFile,\n resolveDirectory,\n} from \"./discovery.js\";\nimport { errorMessage, PreviewRuntimeError } from \"./errors.js\";\nimport { formatHtml } from \"./format.js\";\nimport { assertReact, environmentName, renderEntryId } from \"./render.js\";\nimport { readStaticFile } from \"./static.js\";\n\nexport interface RuntimeOptions {\n /** Template directory, relative to the Vite root unless absolute. */\n directory: string;\n renderTimeoutMs: number;\n}\n\ntype RenderedEmail = Pick<PreviewResult, \"html\" | \"text\">;\n\nfunction isRenderedEmail(value: unknown): value is RenderedEmail {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"html\" in value &&\n typeof value.html === \"string\" &&\n \"text\" in value &&\n typeof value.text === \"string\"\n );\n}\n\nexport class PreviewRuntime {\n private readonly listeners = new Set<(revision: number) => void>();\n private readonly streams = new Set<() => void>();\n private notification: ReturnType<typeof setTimeout> | undefined;\n private closed = false;\n private currentRevision = 0;\n\n private constructor(\n private readonly server: ViteDevServer,\n readonly directory: string,\n private readonly timeoutMs: number,\n ) {}\n\n static async create(server: ViteDevServer, options: RuntimeOptions): Promise<PreviewRuntime> {\n if (!Number.isFinite(options.renderTimeoutMs) || options.renderTimeoutMs < 1) {\n throw new PreviewRuntimeError(\n \"INVALID_OPTIONS\",\n \"renderTimeoutMs must be a positive number.\",\n 400,\n );\n }\n const directory = await resolveDirectory(server.config.root, options.directory);\n const runtime = new PreviewRuntime(server, directory, options.renderTimeoutMs);\n // The template directory may sit outside the Vite root, for example in a sibling package.\n server.watcher.add(directory);\n server.watcher.on(\"all\", runtime.onFileEvent);\n return runtime;\n }\n\n private get environment(): RunnableDevEnvironment {\n const environment = this.server.environments[environmentName];\n if (!environment || !isRunnableDevEnvironment(environment)) {\n throw new PreviewRuntimeError(\n \"NOT_INITIALIZED\",\n \"The email preview environment is not available.\",\n 503,\n );\n }\n return environment;\n }\n\n private readonly onFileEvent = (_event: string, file: string): void => {\n const path = resolve(file);\n const environment = this.server.environments[environmentName];\n if (\n !isContained(this.directory, path) &&\n basename(path) !== \"tsconfig.json\" &&\n !environment?.moduleGraph.getModulesByFile(path)?.size\n ) {\n return;\n }\n // Vite invalidates changed modules asynchronously. Template graphs are small,\n // so drop them now and the next render can never observe stale code.\n if (environment && isRunnableDevEnvironment(environment)) {\n environment.moduleGraph.invalidateAll();\n environment.runner.clearCache();\n }\n this.invalidate();\n };\n\n get revision(): number {\n return this.currentRevision;\n }\n\n private invalidate(): void {\n if (this.closed) return;\n this.currentRevision += 1;\n if (this.notification) clearTimeout(this.notification);\n this.notification = setTimeout(() => {\n this.notification = undefined;\n for (const listener of this.listeners) {\n try {\n listener(this.currentRevision);\n } catch {\n this.listeners.delete(listener);\n }\n }\n }, 80);\n }\n\n private assertOpen(): void {\n if (this.closed) throw new PreviewRuntimeError(\"CLOSED\", \"The preview server is closing.\", 503);\n }\n\n async listTemplates(): Promise<TemplateList> {\n this.assertOpen();\n return {\n directory: this.directory,\n templates: await discoverTemplates(this.directory),\n revision: this.revision,\n };\n }\n\n async renderTemplate(id: string): Promise<PreviewResult> {\n this.assertOpen();\n const { templates } = await this.listTemplates();\n if (!templates.some((template) => template.id === id)) {\n throw new PreviewRuntimeError(\"TEMPLATE_NOT_FOUND\", \"Email template not found.\", 404);\n }\n const path = await resolveContainedFile(this.directory, id);\n try {\n await assertReact(path);\n } catch (error) {\n throw new PreviewRuntimeError(\"DEPENDENCY_ERROR\", errorMessage(error), 422);\n }\n for (let attempt = 0; attempt < 3; attempt += 1) {\n this.assertOpen();\n const revision = this.revision;\n const result = await this.render(path);\n if (this.revision === revision) {\n return { id, revision, ...result, formattedHtml: await formatHtml(result.html) };\n }\n }\n throw new PreviewRuntimeError(\n \"TEMPLATE_CHANGED\",\n \"The template changed while rendering. Please try again.\",\n 409,\n );\n }\n\n private async render(path: string): Promise<RenderedEmail> {\n let entry: { renderEmail?: unknown };\n try {\n entry = await this.withTimeout(this.environment.runner.import(renderEntryId(path)));\n } catch (error) {\n if (error instanceof PreviewRuntimeError) throw error;\n throw new PreviewRuntimeError(\"COMPILE_ERROR\", errorMessage(error), 422);\n }\n try {\n if (typeof entry.renderEmail !== \"function\") throw new Error(\"Rendering failed to load.\");\n const value: unknown = await this.withTimeout(Promise.resolve(entry.renderEmail()));\n if (!isRenderedEmail(value)) throw new Error(\"The template returned an invalid result.\");\n return value;\n } catch (error) {\n if (error instanceof PreviewRuntimeError) throw error;\n throw new PreviewRuntimeError(\"RENDER_ERROR\", errorMessage(error), 422);\n }\n }\n\n /**\n * Templates run in the dev server process. A timeout reports work that never\n * settles; it cannot interrupt a synchronous loop.\n */\n private withTimeout<T>(work: Promise<T>): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(\n () =>\n reject(\n new PreviewRuntimeError(\n \"RENDER_TIMEOUT\",\n `Template rendering exceeded ${this.timeoutMs} ms.`,\n 422,\n ),\n ),\n this.timeoutMs,\n );\n });\n return Promise.race([work, timeout]).finally(() => clearTimeout(timer));\n }\n\n subscribe(listener: (revision: number) => void): () => void {\n this.assertOpen();\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n /** Registers an open event stream so shutdown can end it. */\n trackStream(close: () => void): () => void {\n this.streams.add(close);\n return () => this.streams.delete(close);\n }\n\n async getStaticFile(\n path: string,\n ): Promise<{ data: Uint8Array<ArrayBuffer>; contentType: string }> {\n this.assertOpen();\n return readStaticFile(this.directory, path);\n }\n\n close(): void {\n if (this.closed) return;\n this.closed = true;\n if (this.notification) clearTimeout(this.notification);\n this.server.watcher.off(\"all\", this.onFileEvent);\n for (const close of this.streams) close();\n this.streams.clear();\n this.listeners.clear();\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport { createRunnableDevEnvironment } from \"vite\";\nimport type { Plugin } from \"vite\";\nimport { previewMiddleware } from \"./middleware.js\";\nimport { environmentName, loadRenderEntry, resolveRenderEntry } from \"./render.js\";\nimport { PreviewRuntime } from \"./runtime.js\";\n\nexport interface EmailPreviewOptions {\n /** Template directory relative to the Vite root. Defaults to `emails`. */\n dir?: string;\n /** URL prefix for the interface and its API. Defaults to `/__email`. */\n base?: string;\n /** Rendering timeout in milliseconds. Defaults to 10000. */\n renderTimeoutMs?: number;\n}\n\n/** @internal Lets source tests and the package's own development serve another build. */\nexport interface InternalOptions extends EmailPreviewOptions {\n interfaceDirectory?: string;\n}\n\nfunction normalizeBase(base: string): string {\n const value = `/${base.replace(/^\\/+|\\/+$/g, \"\")}/`.replace(/^\\/\\/$/, \"/\");\n if (!/^\\/(?:[\\w.-]+\\/)*$/.test(value)) {\n throw new Error(`Invalid email preview base: ${base}`);\n }\n return value;\n}\n\n/** Adds the React Email preview to a Vite dev server. Builds are unaffected. */\nexport function emailPreview(options: EmailPreviewOptions = {}): Plugin {\n const {\n dir = \"emails\",\n renderTimeoutMs = 10_000,\n interfaceDirectory = fileURLToPath(new URL(\"./ui\", import.meta.url)),\n } = options as InternalOptions;\n const base = normalizeBase(options.base ?? \"/__email\");\n let runtime: Promise<PreviewRuntime> | undefined;\n\n return {\n name: \"raypx-email-preview\",\n apply: \"serve\",\n // Lets tests and cooperating plugins reach the running preview.\n api: { runtime: () => runtime },\n config() {\n return {\n environments: {\n // Templates get their own module graph, so rendering never loads into or\n // invalidates the application's SSR environment.\n [environmentName]: {\n consumer: \"server\",\n resolve: {\n dedupe: [\"react\", \"react-dom\"],\n external: true,\n // Bundled through Vite so the renderer resolves the project's React.\n noExternal: [\"@react-email/render\"],\n },\n dev: {\n createEnvironment: (name, config) => createRunnableDevEnvironment(name, config),\n },\n },\n },\n };\n },\n resolveId(id) {\n return resolveRenderEntry(id);\n },\n load(id) {\n return loadRenderEntry(id);\n },\n configureServer(server) {\n runtime = PreviewRuntime.create(server, { directory: dir, renderTimeoutMs });\n // Keep a failed start observable through each request instead of crashing.\n runtime.catch(() => {});\n server.middlewares.use(previewMiddleware(base, interfaceDirectory, () => runtime!));\n server.httpServer?.once(\"close\", () => void runtime?.then((value) => value.close()));\n },\n async buildEnd() {\n (await runtime?.catch(() => undefined))?.close();\n },\n };\n}\n"],"mappings":"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"}
|