react-email
Version:
A live preview of your emails right in your browser.
1,273 lines (1,245 loc) • 42.2 kB
JavaScript
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined")
return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/cli/index.ts
import { program } from "commander";
// package.json
var package_default = {
name: "react-email",
version: "3.0.7",
description: "A live preview of your emails right in your browser.",
bin: {
email: "./dist/cli/index.js"
},
scripts: {
build: "tsup-node && node build-preview-server.mjs",
dev: "tsup-node --watch",
test: "vitest run",
"test:watch": "vitest",
clean: "rm -rf dist"
},
license: "MIT",
repository: {
type: "git",
url: "https://github.com/resend/react-email.git",
directory: "packages/react-email"
},
keywords: ["react", "email"],
engines: {
node: ">=18.0.0"
},
dependencies: {
"@babel/core": "7.24.5",
"@babel/parser": "7.24.5",
chalk: "4.1.2",
chokidar: "4.0.3",
commander: "11.1.0",
debounce: "2.0.0",
esbuild: "0.23.0",
glob: "10.3.4",
"log-symbols": "4.1.0",
"mime-types": "2.1.35",
next: "15.1.2",
"normalize-path": "3.0.0",
ora: "5.4.1",
"socket.io": "4.8.1"
},
devDependencies: {
"@radix-ui/colors": "1.0.1",
"@radix-ui/react-collapsible": "1.1.0",
"@radix-ui/react-popover": "1.1.1",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-toggle-group": "1.1.0",
"@radix-ui/react-tooltip": "1.1.2",
"@react-email/render": "workspace:*",
"@swc/core": "1.4.15",
"@types/babel__core": "7.20.5",
"@types/fs-extra": "11.0.1",
"@types/mime-types": "2.1.4",
"@types/node": "22.10.2",
"@types/normalize-path": "3.0.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/webpack": "5.28.5",
"@vercel/style-guide": "5.1.0",
autoprefixer: "10.4.20",
clsx: "2.1.0",
"framer-motion": "12.0.0-alpha.2",
postcss: "8.4.40",
"prism-react-renderer": "2.1.0",
"module-punycode": "npm:punycode@2.3.1",
react: "^19",
"react-dom": "^19",
sharp: "0.33.3",
"socket.io-client": "4.8.0",
sonner: "1.7.1",
"source-map-js": "1.0.2",
"stacktrace-parser": "0.1.10",
"tailwind-merge": "2.2.0",
tailwindcss: "3.4.0",
tsup: "7.2.0",
tsx: "4.9.0",
typescript: "5.1.6",
vitest: "1.1.3"
}
};
// src/cli/commands/build.ts
import { spawn } from "node:child_process";
import fs5 from "node:fs";
import path8 from "node:path";
import logSymbols3 from "log-symbols";
import ora2 from "ora";
// src/utils/get-emails-directory-metadata.ts
import fs from "node:fs";
import path from "node:path";
var isFileAnEmail = (fullPath) => {
const stat = fs.statSync(fullPath);
if (stat.isDirectory())
return false;
const { ext } = path.parse(fullPath);
if (![".js", ".tsx", ".jsx"].includes(ext))
return false;
if (!fs.existsSync(fullPath)) {
return false;
}
const fileContents = fs.readFileSync(fullPath, "utf8");
return /\bexport\s+default\b/gm.test(fileContents);
};
var mergeDirectoriesWithSubDirectories = (emailsDirectoryMetadata) => {
let currentResultingMergedDirectory = emailsDirectoryMetadata;
while (currentResultingMergedDirectory.emailFilenames.length === 0 && currentResultingMergedDirectory.subDirectories.length === 1) {
const onlySubDirectory = currentResultingMergedDirectory.subDirectories[0];
currentResultingMergedDirectory = {
...onlySubDirectory,
directoryName: path.join(
currentResultingMergedDirectory.directoryName,
onlySubDirectory.directoryName
)
};
}
return currentResultingMergedDirectory;
};
var getEmailsDirectoryMetadata = async (absolutePathToEmailsDirectory, keepFileExtensions = false, isSubDirectory = false, baseDirectoryPath = absolutePathToEmailsDirectory) => {
if (!fs.existsSync(absolutePathToEmailsDirectory))
return;
const dirents = await fs.promises.readdir(absolutePathToEmailsDirectory, {
withFileTypes: true
});
const emailFilenames = dirents.filter(
(dirent) => isFileAnEmail(path.join(absolutePathToEmailsDirectory, dirent.name))
).map(
(dirent) => keepFileExtensions ? dirent.name : dirent.name.replace(path.extname(dirent.name), "")
);
const subDirectories = await Promise.all(
dirents.filter(
(dirent) => dirent.isDirectory() && !dirent.name.startsWith("_") && dirent.name !== "static"
).map((dirent) => {
const direntAbsolutePath = path.join(
absolutePathToEmailsDirectory,
dirent.name
);
return getEmailsDirectoryMetadata(
direntAbsolutePath,
keepFileExtensions,
true,
baseDirectoryPath
);
})
);
const emailsMetadata = {
absolutePath: absolutePathToEmailsDirectory,
relativePath: path.relative(
baseDirectoryPath,
absolutePathToEmailsDirectory
),
directoryName: absolutePathToEmailsDirectory.split(path.sep).pop(),
emailFilenames,
subDirectories
};
return isSubDirectory ? mergeDirectoriesWithSubDirectories(emailsMetadata) : emailsMetadata;
};
// src/utils/register-spinner-autostopping.ts
import logSymbols from "log-symbols";
var spinners = /* @__PURE__ */ new Set();
process.on("SIGINT", () => {
spinners.forEach((spinner) => {
if (spinner.isSpinning) {
spinner.stop();
}
});
});
process.on("exit", (code) => {
if (code !== 0) {
spinners.forEach((spinner) => {
if (spinner.isSpinning) {
spinner.stopAndPersist({
symbol: logSymbols.error
});
}
});
}
});
var registerSpinnerAutostopping = (spinner) => {
spinners.add(spinner);
};
// src/cli/utils/tree.ts
import { promises as fs2 } from "node:fs";
import os from "node:os";
import path2 from "node:path";
var SYMBOLS = {
BRANCH: "\u251C\u2500\u2500 ",
EMPTY: "",
INDENT: " ",
LAST_BRANCH: "\u2514\u2500\u2500 ",
VERTICAL: "\u2502 "
};
var getTreeLines = async (dirPath, depth, currentDepth = 0) => {
const base = process.cwd();
const dirFullpath = path2.resolve(base, dirPath);
const dirname = path2.basename(dirFullpath);
let lines = [dirname];
const dirStat = await fs2.stat(dirFullpath);
if (dirStat.isDirectory() && currentDepth < depth) {
const childDirents = await fs2.readdir(dirFullpath, { withFileTypes: true });
childDirents.sort((a, b) => {
if (a.isDirectory() && b.isFile()) {
return -1;
}
if (a.isFile() && b.isDirectory()) {
return 1;
}
return b.name > a.name ? -1 : 1;
});
for (let i = 0; i < childDirents.length; i++) {
const dirent = childDirents[i];
const isLast = i === childDirents.length - 1;
const branchingSymbol = isLast ? SYMBOLS.LAST_BRANCH : SYMBOLS.BRANCH;
const verticalSymbol = isLast ? SYMBOLS.INDENT : SYMBOLS.VERTICAL;
if (dirent.isFile()) {
lines.push(`${branchingSymbol}${dirent.name}`);
} else {
const pathToDirectory = path2.join(dirFullpath, dirent.name);
const treeLinesForSubDirectory = await getTreeLines(
pathToDirectory,
depth,
currentDepth + 1
);
lines = lines.concat(
treeLinesForSubDirectory.map(
(line, index) => index === 0 ? `${branchingSymbol}${line}` : `${verticalSymbol}${line}`
)
);
}
}
}
return lines;
};
var tree = async (dirPath, depth) => {
const lines = await getTreeLines(dirPath, depth);
return lines.join(os.EOL);
};
// src/cli/utils/preview/hot-reloading/setup-hot-reloading.ts
import path7 from "node:path";
import { watch } from "chokidar";
import debounce from "debounce";
import { Server as SocketServer } from "socket.io";
// src/cli/utils/preview/hot-reloading/create-dependency-graph.ts
import { promises as fs4, existsSync, statSync } from "node:fs";
import path6 from "node:path";
// src/cli/utils/preview/start-dev-server.ts
import http from "node:http";
import path5 from "node:path";
import url from "node:url";
import chalk from "chalk";
import logSymbols2 from "log-symbols";
import next from "next";
import ora from "ora";
// src/cli/utils/preview/get-env-variables-for-preview-app.ts
import path3 from "node:path";
var getEnvVariablesForPreviewApp = (relativePathToEmailsDirectory, cwd) => {
return {
EMAILS_DIR_RELATIVE_PATH: relativePathToEmailsDirectory,
EMAILS_DIR_ABSOLUTE_PATH: path3.resolve(cwd, relativePathToEmailsDirectory),
USER_PROJECT_LOCATION: cwd
};
};
// src/cli/utils/preview/serve-static-file.ts
import { promises as fs3 } from "node:fs";
import path4 from "node:path";
import { lookup } from "mime-types";
var serveStaticFile = async (res, parsedUrl, staticDirRelativePath) => {
const staticBaseDir = path4.join(process.cwd(), staticDirRelativePath);
const pathname = parsedUrl.pathname;
const ext = path4.parse(pathname).ext;
const fileAbsolutePath = path4.join(staticBaseDir, pathname);
const fileHandle = await fs3.open(fileAbsolutePath, "r");
try {
const fileData = await fs3.readFile(fileHandle);
res.setHeader("Content-type", lookup(ext) || "text/plain");
res.end(fileData);
} catch (exception) {
console.error(
`Could not read file at ${fileAbsolutePath} to be served, here's the exception:`,
exception
);
res.statusCode = 500;
res.end(
"Could not read file to be served! Check your terminal for more information."
);
} finally {
fileHandle.close();
}
};
// src/cli/utils/preview/start-dev-server.ts
var devServer;
var safeAsyncServerListen = (server, port) => {
return new Promise((resolve) => {
server.listen(port, () => {
resolve({ portAlreadyInUse: false });
});
server.on("error", (e) => {
if (e.code === "EADDRINUSE") {
resolve({ portAlreadyInUse: true });
}
});
});
};
var isDev = !__filename.endsWith(path5.join("cli", "index.js"));
var cliPacakgeLocation = isDev ? path5.resolve(__dirname, "../../../..") : path5.resolve(__dirname, "../..");
var previewServerLocation = isDev ? path5.resolve(__dirname, "../../../..") : path5.resolve(__dirname, "../preview");
var startDevServer = async (emailsDirRelativePath, staticBaseDirRelativePath, port) => {
devServer = http.createServer((req, res) => {
if (!req.url) {
res.end(404);
return;
}
const parsedUrl = url.parse(req.url, true);
res.setHeader(
"Cache-Control",
"no-cache, max-age=0, must-revalidate, no-store"
);
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "-1");
try {
if (parsedUrl.path?.includes("static/") && !parsedUrl.path.includes("_next/static/")) {
void serveStaticFile(res, parsedUrl, staticBaseDirRelativePath);
} else if (!isNextReady) {
void nextReadyPromise.then(
() => nextHandleRequest?.(req, res, parsedUrl)
);
} else {
void nextHandleRequest?.(req, res, parsedUrl);
}
} catch (e) {
console.error("caught error", e);
res.writeHead(500);
res.end();
}
});
const { portAlreadyInUse } = await safeAsyncServerListen(devServer, port);
if (!portAlreadyInUse) {
console.log(chalk.greenBright(` React Email ${package_default.version}`));
console.log(` Running preview at: http://localhost:${port}
`);
} else {
const nextPortToTry = port + 1;
console.warn(
` ${logSymbols2.warning} Port ${port} is already in use, trying ${nextPortToTry}`
);
return startDevServer(
emailsDirRelativePath,
staticBaseDirRelativePath,
nextPortToTry
);
}
devServer.on("close", async () => {
await app.close();
});
devServer.on("error", (e) => {
console.error(
` ${logSymbols2.error} preview server error: `,
JSON.stringify(e)
);
process.exit(1);
});
const spinner = ora({
text: "Getting react-email preview server ready...\n",
prefixText: " "
}).start();
registerSpinnerAutostopping(spinner);
const timeBeforeNextReady = performance.now();
process.env = {
NODE_ENV: "development",
...process.env,
...getEnvVariablesForPreviewApp(
// If we don't do normalization here, stuff like https://github.com/resend/react-email/issues/1354 happens.
path5.normalize(emailsDirRelativePath),
process.cwd()
)
};
const app = next({
// passing in env here does not get the environment variables there
dev: isDev,
conf: {
images: {
// This is to avoid the warning with sharp
unoptimized: true
}
},
hostname: "localhost",
port,
dir: previewServerLocation
});
let isNextReady = false;
const nextReadyPromise = app.prepare();
await nextReadyPromise;
isNextReady = true;
const nextHandleRequest = app.getRequestHandler();
const secondsToNextReady = ((performance.now() - timeBeforeNextReady) / 1e3).toFixed(1);
spinner.stopAndPersist({
text: `Ready in ${secondsToNextReady}s
`,
symbol: logSymbols2.success
});
return devServer;
};
var makeExitHandler = (options) => (_codeOrSignal) => {
if (typeof devServer !== "undefined") {
console.log("\nshutting down dev server");
devServer.close();
devServer = void 0;
}
if (options?.shouldKillProcess) {
process.exit(options.killWithErrorCode ? 1 : 0);
}
};
process.on("exit", makeExitHandler());
process.on(
"SIGINT",
makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false })
);
process.on(
"SIGUSR1",
makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false })
);
process.on(
"SIGUSR2",
makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false })
);
process.on(
"uncaughtException",
makeExitHandler({ shouldKillProcess: true, killWithErrorCode: true })
);
// src/cli/utils/preview/hot-reloading/get-imported-modules.ts
import { traverse } from "@babel/core";
import { parse } from "@babel/parser";
var getImportedModules = (contents) => {
const importedPaths = [];
const parsedContents = parse(contents, {
sourceType: "unambiguous",
strictMode: false,
errorRecovery: true,
plugins: ["jsx", "typescript", "decorators"]
});
traverse(parsedContents, {
ImportDeclaration({ node }) {
importedPaths.push(node.source.value);
},
ExportAllDeclaration({ node }) {
importedPaths.push(node.source.value);
},
ExportNamedDeclaration({ node }) {
if (node.source) {
importedPaths.push(node.source.value);
}
},
CallExpression({ node }) {
if ("name" in node.callee && node.callee.name === "require") {
if (node.arguments.length === 1) {
const importPathNode = node.arguments[0];
if (importPathNode.type === "StringLiteral") {
importedPaths.push(importPathNode.value);
}
}
}
}
});
return importedPaths;
};
// src/cli/utils/preview/hot-reloading/create-dependency-graph.ts
var readAllFilesInsideDirectory = async (directory) => {
let allFilePaths = [];
const topLevelDirents = await fs4.readdir(directory, { withFileTypes: true });
for await (const dirent of topLevelDirents) {
const pathToDirent = path6.join(directory, dirent.name);
if (dirent.isDirectory()) {
allFilePaths = allFilePaths.concat(
await readAllFilesInsideDirectory(pathToDirent)
);
} else {
allFilePaths.push(pathToDirent);
}
}
return allFilePaths;
};
var isJavascriptModule = (filePath) => {
const extensionName = path6.extname(filePath);
return [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"].includes(extensionName);
};
var checkFileExtensionsUntilItExists = (pathWithoutExtension) => {
if (existsSync(`${pathWithoutExtension}.ts`)) {
return `${pathWithoutExtension}.ts`;
}
if (existsSync(`${pathWithoutExtension}.tsx`)) {
return `${pathWithoutExtension}.tsx`;
}
if (existsSync(`${pathWithoutExtension}.js`)) {
return `${pathWithoutExtension}.js`;
}
if (existsSync(`${pathWithoutExtension}.jsx`)) {
return `${pathWithoutExtension}.jsx`;
}
if (existsSync(`${pathWithoutExtension}.mjs`)) {
return `${pathWithoutExtension}.mjs`;
}
if (existsSync(`${pathWithoutExtension}.cjs`)) {
return `${pathWithoutExtension}.cjs`;
}
};
var createDependencyGraph = async (directory) => {
const filePaths = await readAllFilesInsideDirectory(directory);
const modulePaths = filePaths.filter(isJavascriptModule);
const graph = Object.fromEntries(
modulePaths.map((path12) => [
path12,
{
path: path12,
dependencyPaths: [],
dependentPaths: [],
moduleDependencies: []
}
])
);
const getDependencyPaths = async (filePath) => {
const contents = await fs4.readFile(filePath, "utf8");
const importedPaths = getImportedModules(contents);
const importedPathsRelativeToDirectory = importedPaths.map(
(dependencyPath) => {
const isModulePath = !dependencyPath.startsWith(".");
if (isModulePath || path6.isAbsolute(dependencyPath)) {
return dependencyPath;
}
let pathToDependencyFromDirectory = path6.resolve(
/*
path.resolve resolves paths differently from what imports on javascript do.
So if we wouldn't do this, for an email at "/path/to/email.tsx" with a dependecy path of "./other-email"
would end up going into /path/to/email.tsx/other-email instead of /path/to/other-email which is the
one the import is meant to go to
*/
path6.dirname(filePath),
dependencyPath
);
let isDirectory = false;
try {
isDirectory = statSync(pathToDependencyFromDirectory).isDirectory();
} catch (_) {
}
if (isDirectory) {
const pathToSubDirectory = pathToDependencyFromDirectory;
const pathWithExtension = checkFileExtensionsUntilItExists(
`${pathToSubDirectory}/index`
);
if (pathWithExtension) {
pathToDependencyFromDirectory = pathWithExtension;
} else if (isDev) {
console.warn(
`Could not find index file for directory at ${pathToDependencyFromDirectory}. This is probably going to cause issues with both hot reloading and your code.`
);
}
}
if (!isJavascriptModule(pathToDependencyFromDirectory)) {
const pathWithExtension = checkFileExtensionsUntilItExists(
pathToDependencyFromDirectory
);
if (pathWithExtension) {
pathToDependencyFromDirectory = pathWithExtension;
} else if (isDev) {
console.warn(
`Could not determine the file extension for the file at ${pathToDependencyFromDirectory}`
);
}
}
return pathToDependencyFromDirectory;
}
);
const moduleDependencies = importedPathsRelativeToDirectory.filter(
(dependencyPath) => !dependencyPath.startsWith(".") && !path6.isAbsolute(dependencyPath)
);
const nonNodeModuleImportPathsRelativeToDirectory = importedPathsRelativeToDirectory.filter(
(dependencyPath) => dependencyPath.startsWith(".") || path6.isAbsolute(dependencyPath)
);
return {
dependencyPaths: nonNodeModuleImportPathsRelativeToDirectory,
moduleDependencies
};
};
const updateModuleDependenciesInGraph = async (moduleFilePath) => {
const module = graph[moduleFilePath] ?? {
path: moduleFilePath,
dependencyPaths: [],
dependentPaths: [],
moduleDependencies: []
};
const { moduleDependencies, dependencyPaths: newDependencyPaths } = await getDependencyPaths(moduleFilePath);
module.moduleDependencies = moduleDependencies;
for (const dependencyPath of module.dependencyPaths) {
if (newDependencyPaths.includes(dependencyPath))
continue;
const dependencyModule = graph[dependencyPath];
if (dependencyModule !== void 0) {
dependencyModule.dependentPaths = dependencyModule.dependentPaths.filter(
(dependentPath) => dependentPath !== moduleFilePath
);
}
}
module.dependencyPaths = newDependencyPaths;
for (const dependencyPath of newDependencyPaths) {
const dependencyModule = graph[dependencyPath];
if (dependencyModule !== void 0 && !dependencyModule.dependentPaths.includes(moduleFilePath)) {
dependencyModule.dependentPaths.push(moduleFilePath);
} else {
graph[dependencyPath] = {
path: dependencyPath,
moduleDependencies: [],
dependencyPaths: [],
dependentPaths: [moduleFilePath]
};
}
}
graph[moduleFilePath] = module;
};
for (const filePath of modulePaths) {
await updateModuleDependenciesInGraph(filePath);
}
const removeModuleFromGraph = (filePath) => {
const module = graph[filePath];
if (module) {
for (const dependencyPath of module.dependencyPaths) {
if (graph[dependencyPath]) {
graph[dependencyPath].dependentPaths = graph[dependencyPath].dependentPaths.filter(
(dependentPath) => dependentPath !== filePath
);
}
}
delete graph[filePath];
}
};
return [
graph,
async (event, pathToModified) => {
switch (event) {
case "change":
if (isJavascriptModule(pathToModified)) {
await updateModuleDependenciesInGraph(pathToModified);
}
break;
case "add":
if (isJavascriptModule(pathToModified)) {
await updateModuleDependenciesInGraph(pathToModified);
}
break;
case "addDir": {
const filesInsideAddedDirectory = await readAllFilesInsideDirectory(pathToModified);
const modulesInsideAddedDirectory = filesInsideAddedDirectory.filter(isJavascriptModule);
for await (const filePath of modulesInsideAddedDirectory) {
await updateModuleDependenciesInGraph(filePath);
}
break;
}
case "unlink":
if (isJavascriptModule(pathToModified)) {
removeModuleFromGraph(pathToModified);
}
break;
case "unlinkDir": {
const filesInsideDeletedDirectory = await readAllFilesInsideDirectory(pathToModified);
const modulesInsideDeletedDirectory = filesInsideDeletedDirectory.filter(isJavascriptModule);
for await (const filePath of modulesInsideDeletedDirectory) {
removeModuleFromGraph(filePath);
}
break;
}
}
},
{
resolveDependentsOf: function resolveDependentsOf(pathToModule) {
const moduleEntry = graph[pathToModule];
const dependentPaths = [];
if (moduleEntry) {
for (const dependentPath of moduleEntry.dependentPaths) {
const dependentsOfDependent = resolveDependentsOf(dependentPath);
dependentPaths.push(...dependentsOfDependent);
dependentPaths.push(dependentPath);
}
}
return dependentPaths;
}
}
];
};
// src/cli/utils/preview/hot-reloading/setup-hot-reloading.ts
var setupHotreloading = async (devServer2, emailDirRelativePath) => {
let clients = [];
const io = new SocketServer(devServer2);
io.on("connection", (client) => {
clients.push(client);
client.on("disconnect", () => {
clients = clients.filter((item) => item !== client);
});
});
let changes = [];
const reload = debounce(() => {
clients.forEach((client) => {
client.emit("reload", changes);
});
changes = [];
}, 150);
const absolutePathToEmailsDirectory = path7.resolve(
process.cwd(),
emailDirRelativePath
);
const [dependencyGraph, updateDependencyGraph, { resolveDependentsOf }] = await createDependencyGraph(absolutePathToEmailsDirectory);
const watcher = watch("", {
ignoreInitial: true,
cwd: absolutePathToEmailsDirectory
});
const getFilesOutsideEmailsDirectory = () => Object.keys(dependencyGraph).filter(
(p) => path7.relative(absolutePathToEmailsDirectory, p).startsWith("..")
);
let filesOutsideEmailsDirectory = getFilesOutsideEmailsDirectory();
for (const p of filesOutsideEmailsDirectory) {
watcher.add(p);
}
const exit = async () => {
await watcher.close();
};
process.on("SIGINT", exit);
process.on("uncaughtException", exit);
watcher.on("all", async (event, relativePathToChangeTarget) => {
const file = relativePathToChangeTarget.split(path7.sep);
if (file.length === 0) {
return;
}
const pathToChangeTarget = path7.resolve(
absolutePathToEmailsDirectory,
relativePathToChangeTarget
);
await updateDependencyGraph(event, pathToChangeTarget);
const newFilesOutsideEmailsDirectory = getFilesOutsideEmailsDirectory();
for (const p of filesOutsideEmailsDirectory) {
if (!newFilesOutsideEmailsDirectory.includes(p)) {
watcher.unwatch(p);
}
}
for (const p of newFilesOutsideEmailsDirectory) {
if (!filesOutsideEmailsDirectory.includes(p)) {
watcher.add(p);
}
}
filesOutsideEmailsDirectory = newFilesOutsideEmailsDirectory;
changes.push({
event,
filename: relativePathToChangeTarget
});
for (const dependentPath of resolveDependentsOf(pathToChangeTarget)) {
changes.push({
event: "change",
filename: path7.relative(absolutePathToEmailsDirectory, dependentPath)
});
}
reload();
});
return watcher;
};
// src/cli/commands/build.ts
var buildPreviewApp = (absoluteDirectory) => {
return new Promise((resolve, reject) => {
const nextBuild = spawn("npm", ["run", "build"], {
cwd: absoluteDirectory,
shell: true
});
nextBuild.stdout.pipe(process.stdout);
nextBuild.stderr.pipe(process.stderr);
nextBuild.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(
new Error(
`Unable to build the Next app and it exited with code: ${code}`
)
);
}
});
});
};
var setNextEnvironmentVariablesForBuild = async (emailsDirRelativePath, builtPreviewAppPath) => {
const nextConfigContents = `
const path = require('path');
const emailsDirRelativePath = path.normalize('${emailsDirRelativePath}');
const userProjectLocation = path.resolve(process.cwd(), '../');
/** @type {import('next').NextConfig} */
module.exports = {
env: {
NEXT_PUBLIC_IS_BUILDING: 'true',
EMAILS_DIR_RELATIVE_PATH: emailsDirRelativePath,
EMAILS_DIR_ABSOLUTE_PATH: path.resolve(userProjectLocation, emailsDirRelativePath),
USER_PROJECT_LOCATION: userProjectLocation
},
// this is needed so that the code for building emails works properly
webpack: (
/** @type {import('webpack').Configuration & { externals: string[] }} */
config,
{ isServer }
) => {
if (isServer) {
config.externals.push('esbuild');
}
return config;
},
typescript: {
ignoreBuildErrors: true
},
eslint: {
ignoreDuringBuilds: true
},
experimental: {
webpackBuildWorker: true
},
}`;
await fs5.promises.writeFile(
path8.resolve(builtPreviewAppPath, "./next.config.js"),
nextConfigContents,
"utf8"
);
};
var getEmailSlugsFromEmailDirectory = (emailDirectory, emailsDirectoryAbsolutePath) => {
const directoryPathRelativeToEmailsDirectory = emailDirectory.absolutePath.replace(emailsDirectoryAbsolutePath, "").trim();
const slugs = [];
emailDirectory.emailFilenames.forEach(
(filename) => slugs.push(
path8.join(directoryPathRelativeToEmailsDirectory, filename).split(path8.sep).filter((segment) => segment.length > 0)
)
);
emailDirectory.subDirectories.forEach((directory) => {
slugs.push(
...getEmailSlugsFromEmailDirectory(
directory,
emailsDirectoryAbsolutePath
)
);
});
return slugs;
};
var forceSSGForEmailPreviews = async (emailsDirPath, builtPreviewAppPath) => {
const emailDirectoryMetadata = (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await getEmailsDirectoryMetadata(emailsDirPath)
);
const parameters = getEmailSlugsFromEmailDirectory(
emailDirectoryMetadata,
emailsDirPath
).map((slug) => ({ slug }));
const removeForceDynamic = async (filePath) => {
const contents = await fs5.promises.readFile(filePath, "utf8");
await fs5.promises.writeFile(
filePath,
contents.replace("export const dynamic = 'force-dynamic';", ""),
"utf8"
);
};
await removeForceDynamic(
path8.resolve(builtPreviewAppPath, "./src/app/layout.tsx")
);
await removeForceDynamic(
path8.resolve(builtPreviewAppPath, "./src/app/preview/[...slug]/page.tsx")
);
await fs5.promises.appendFile(
path8.resolve(builtPreviewAppPath, "./src/app/preview/[...slug]/page.tsx"),
`
export function generateStaticParams() {
return Promise.resolve(
${JSON.stringify(parameters)}
);
}`,
"utf8"
);
};
var updatePackageJson = async (builtPreviewAppPath) => {
const packageJsonPath = path8.resolve(builtPreviewAppPath, "./package.json");
const packageJson = JSON.parse(
await fs5.promises.readFile(packageJsonPath, "utf8")
);
packageJson.scripts.build = "next build";
packageJson.scripts.start = "next start";
packageJson.name = "preview-server";
delete packageJson.devDependencies["@react-email/render"];
delete packageJson.devDependencies["@react-email/components"];
await fs5.promises.writeFile(
packageJsonPath,
JSON.stringify(packageJson),
"utf8"
);
};
var npmInstall = async (builtPreviewAppPath, packageManager) => {
return new Promise((resolve, reject) => {
const childProc = spawn(
packageManager,
["install", "--silent", "--include=dev"],
{
cwd: builtPreviewAppPath,
shell: true
}
);
childProc.stdout.pipe(process.stdout);
childProc.stderr.pipe(process.stderr);
childProc.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(
new Error(
`Unable to install the dependencies and it exited with code: ${code}`
)
);
}
});
});
};
var build = async ({
dir: emailsDirRelativePath,
packageManager
}) => {
try {
const spinner = ora2({
text: "Starting build process...",
prefixText: " "
}).start();
registerSpinnerAutostopping(spinner);
spinner.text = `Checking if ${emailsDirRelativePath} folder exists`;
if (!fs5.existsSync(emailsDirRelativePath)) {
process.exit(1);
}
const emailsDirPath = path8.join(process.cwd(), emailsDirRelativePath);
const staticPath = path8.join(emailsDirPath, "static");
const builtPreviewAppPath = path8.join(process.cwd(), ".react-email");
if (fs5.existsSync(builtPreviewAppPath)) {
spinner.text = "Deleting pre-existing `.react-email` folder";
await fs5.promises.rm(builtPreviewAppPath, { recursive: true });
}
spinner.text = "Copying preview app from CLI to `.react-email`";
await fs5.promises.cp(cliPacakgeLocation, builtPreviewAppPath, {
recursive: true,
filter: (source) => {
return !/(\/|\\)cli(\/|\\)?/.test(source) && !/(\/|\\)\.next(\/|\\)?/.test(source) && !/(\/|\\)\.turbo(\/|\\)?/.test(source) && !/(\/|\\)node_modules(\/|\\)?$/.test(source);
}
});
if (fs5.existsSync(staticPath)) {
spinner.text = "Copying `static` folder into `.react-email/public/static`";
const builtStaticDirectory = path8.resolve(
builtPreviewAppPath,
"./public/static"
);
await fs5.promises.cp(staticPath, builtStaticDirectory, {
recursive: true
});
}
spinner.text = "Setting Next environment variables for preview app to work properly";
await setNextEnvironmentVariablesForBuild(
emailsDirRelativePath,
builtPreviewAppPath
);
spinner.text = "Setting server side generation for the email preview pages";
await forceSSGForEmailPreviews(emailsDirPath, builtPreviewAppPath);
spinner.text = "Updating package.json's build and start scripts";
await updatePackageJson(builtPreviewAppPath);
spinner.text = "Installing dependencies on `.react-email`";
await npmInstall(builtPreviewAppPath, packageManager);
spinner.stopAndPersist({
text: "Successfully prepared `.react-email` for `next build`",
symbol: logSymbols3.success
});
await buildPreviewApp(builtPreviewAppPath);
} catch (error) {
console.log(error);
process.exit(1);
}
};
// src/cli/commands/dev.ts
import fs6 from "node:fs";
var dev = async ({ dir: emailsDirRelativePath, port }) => {
try {
if (!fs6.existsSync(emailsDirRelativePath)) {
console.error(`Missing ${emailsDirRelativePath} folder`);
process.exit(1);
}
const devServer2 = await startDevServer(
emailsDirRelativePath,
emailsDirRelativePath,
// defaults to ./emails/static for the static files that are served to the preview
Number.parseInt(port)
);
await setupHotreloading(devServer2, emailsDirRelativePath);
} catch (error) {
console.log(error);
process.exit(1);
}
};
// src/cli/commands/export.ts
import fs8, { unlinkSync, writeFileSync } from "node:fs";
import path10 from "node:path";
import { build as build2 } from "esbuild";
import { glob } from "glob";
import logSymbols4 from "log-symbols";
import normalize from "normalize-path";
import ora3 from "ora";
// src/utils/esbuild/renderring-utilities-exporter.ts
import { promises as fs7 } from "node:fs";
import path9 from "node:path";
// src/utils/esbuild/escape-string-for-regex.ts
function escapeStringForRegex(string) {
return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
}
// src/utils/esbuild/renderring-utilities-exporter.ts
var renderingUtilitiesExporter = (emailTemplates) => ({
name: "rendering-utilities-exporter",
setup: (b) => {
b.onLoad(
{
filter: new RegExp(
emailTemplates.map((emailPath) => escapeStringForRegex(emailPath)).join("|")
)
},
async ({ path: pathToFile }) => {
return {
contents: `${await fs7.readFile(pathToFile, "utf8")};
export { render } from 'react-email-module-that-will-export-render'
export { createElement as reactEmailCreateReactElement } from 'react';
`,
loader: path9.extname(pathToFile).slice(1)
};
}
);
b.onResolve(
{ filter: /^react-email-module-that-will-export-render$/ },
async (args) => {
const options = {
kind: "import-statement",
importer: args.importer,
resolveDir: args.resolveDir,
namespace: args.namespace
};
let result = await b.resolve("@react-email/render", options);
if (result.errors.length === 0) {
return result;
}
result = await b.resolve("@react-email/components", options);
if (result.errors.length > 0 && result.errors[0]) {
result.errors[0].text = "Failed trying to import `render` from either `@react-email/render` or `@react-email/components` to be able to render your email template.\n Maybe you don't have either of them installed?";
}
return result;
}
);
}
});
// src/cli/commands/export.ts
var getEmailTemplatesFromDirectory = (emailDirectory) => {
const templatePaths = [];
emailDirectory.emailFilenames.forEach(
(filename) => templatePaths.push(path10.join(emailDirectory.absolutePath, filename))
);
emailDirectory.subDirectories.forEach((directory) => {
templatePaths.push(...getEmailTemplatesFromDirectory(directory));
});
return templatePaths;
};
var exportTemplates = async (pathToWhereEmailMarkupShouldBeDumped, emailsDirectoryPath, options) => {
if (fs8.existsSync(pathToWhereEmailMarkupShouldBeDumped)) {
fs8.rmSync(pathToWhereEmailMarkupShouldBeDumped, { recursive: true });
}
let spinner;
if (!options.silent) {
spinner = ora3("Preparing files...\n").start();
registerSpinnerAutostopping(spinner);
}
const emailsDirectoryMetadata = await getEmailsDirectoryMetadata(
path10.resolve(process.cwd(), emailsDirectoryPath),
true
);
if (typeof emailsDirectoryMetadata === "undefined") {
if (spinner) {
spinner.stopAndPersist({
symbol: logSymbols4.error,
text: `Could not find the directory at ${emailsDirectoryPath}`
});
}
return;
}
const allTemplates = getEmailTemplatesFromDirectory(emailsDirectoryMetadata);
try {
await build2({
bundle: true,
entryPoints: allTemplates,
plugins: [renderingUtilitiesExporter(allTemplates)],
platform: "node",
format: "cjs",
loader: { ".js": "jsx" },
outExtension: { ".js": ".cjs" },
jsx: "transform",
write: true,
outdir: pathToWhereEmailMarkupShouldBeDumped
});
} catch (exception) {
const buildFailure = exception;
if (spinner) {
spinner.stopAndPersist({
symbol: logSymbols4.error,
text: "Failed to build emails"
});
}
process.exit(1);
}
if (spinner) {
spinner.succeed();
}
const allBuiltTemplates = glob.sync(
normalize(`${pathToWhereEmailMarkupShouldBeDumped}/**/*.cjs`),
{
absolute: true
}
);
for await (const template of allBuiltTemplates) {
try {
if (spinner) {
spinner.text = `rendering ${template.split("/").pop()}`;
spinner.render();
}
delete __require.cache[template];
const emailModule = __require(template);
const rendered = await emailModule.render(
emailModule.reactEmailCreateReactElement(emailModule.default, {}),
options
);
const htmlPath = template.replace(
".cjs",
options.plainText ? ".txt" : ".html"
);
writeFileSync(htmlPath, rendered);
unlinkSync(template);
} catch (exception) {
if (spinner) {
spinner.stopAndPersist({
symbol: logSymbols4.error,
text: `failed when rendering ${template.split("/").pop()}`
});
}
console.error(exception);
process.exit(1);
}
}
if (spinner) {
spinner.succeed("Rendered all files");
spinner.text = "Copying static files";
spinner.render();
}
const staticDirectoryPath = path10.join(emailsDirectoryPath, "static");
if (fs8.existsSync(staticDirectoryPath)) {
const pathToDumpStaticFilesInto = path10.join(
pathToWhereEmailMarkupShouldBeDumped,
"static"
);
if (fs8.existsSync(pathToDumpStaticFilesInto))
await fs8.promises.rm(pathToDumpStaticFilesInto, { recursive: true });
try {
await fs8.promises.cp(staticDirectoryPath, pathToDumpStaticFilesInto, {
recursive: true
});
} catch (exception) {
console.error(exception);
if (spinner) {
spinner.stopAndPersist({
symbol: logSymbols4.error,
text: "Failed to copy static files"
});
}
console.error(
`Something went wrong while copying the file to ${pathToWhereEmailMarkupShouldBeDumped}/static, ${exception}`
);
process.exit(1);
}
}
if (spinner && !options.silent) {
spinner.succeed();
const fileTree = await tree(pathToWhereEmailMarkupShouldBeDumped, 4);
console.log(fileTree);
spinner.stopAndPersist({
symbol: logSymbols4.success,
text: "Successfully exported emails"
});
}
};
// src/cli/commands/start.ts
import { spawn as spawn2 } from "node:child_process";
import fs9 from "node:fs";
import path11 from "node:path";
var start = async () => {
try {
const usersProjectLocation = process.cwd();
const builtPreviewPath = path11.resolve(
usersProjectLocation,
"./.react-email"
);
if (!fs9.existsSync(builtPreviewPath)) {
console.error(
"Could not find .react-email, maybe you haven't ran email build?"
);
process.exit(1);
}
const nextStart = spawn2("npm", ["start"], {
cwd: builtPreviewPath,
stdio: "inherit"
});
process.on("SIGINT", () => {
nextStart.kill("SIGINT");
});
nextStart.on("exit", (code) => {
process.exit(code ?? 0);
});
} catch (error) {
console.log(error);
process.exit(1);
}
};
// src/cli/index.ts
var PACKAGE_NAME = "react-email";
program.name(PACKAGE_NAME).description("A live preview of your emails right in your browser").version(package_default.version);
program.command("dev").description("Starts the preview email development app").option("-d, --dir <path>", "Directory with your email templates", "./emails").option("-p --port <port>", "Port to run dev server on", "3000").action(dev);
program.command("build").description("Copies the preview app for onto .react-email and builds it").option("-d, --dir <path>", "Directory with your email templates", "./emails").option(
"-p --packageManager <name>",
"Package name to use on installation on `.react-email`",
"npm"
).action(build);
program.command("start").description('Runs the built preview app that is inside of ".react-email"').action(start);
program.command("export").description("Build the templates to the `out` directory").option("--outDir <path>", "Output directory", "out").option("-p, --pretty", "Pretty print the output", false).option("-t, --plainText", "Set output format as plain text", false).option("-d, --dir <path>", "Directory with your email templates", "./emails").option(
"-s, --silent",
"To, or not to show a spinner with process information",
false
).action(
({ outDir, pretty, plainText, silent, dir: srcDir }) => exportTemplates(outDir, srcDir, { pretty, silent, plainText })
);
program.parse();