UNPKG

react-email

Version:

A live preview of your emails right in your browser.

1,273 lines (1,245 loc) 42.2 kB
#!/usr/bin/env node 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();