From a615e6c7d86942262c92b10f0dfacf25d05a7e5e Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Fri, 23 Jan 2026 15:46:17 +0100 Subject: [PATCH 01/19] Process email templates by chunks --- packages/react-email/src/commands/export.ts | 165 ++++++++++++-------- 1 file changed, 103 insertions(+), 62 deletions(-) diff --git a/packages/react-email/src/commands/export.ts b/packages/react-email/src/commands/export.ts index 42d8e1db81..dae42a60d4 100644 --- a/packages/react-email/src/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -29,6 +29,22 @@ const getEmailTemplatesFromDirectory = (emailDirectory: EmailsDirectory) => { return templatePaths; }; +/** + * Splits an array into chunks of specified size + */ +const chunkArray = (array: T[], chunkSize: number): T[][] => { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)); + } + return chunks; +}; + +/** + * Number of templates to process in each chunk to avoid memory issues + */ +const TEMPLATE_CHUNK_SIZE = 50; + type ExportTemplatesOptions = Options & { silent?: boolean; pretty?: boolean; @@ -75,82 +91,107 @@ export const exportTemplates = async ( } const allTemplates = getEmailTemplatesFromDirectory(emailsDirectoryMetadata); + const templateChunks = chunkArray(allTemplates, TEMPLATE_CHUNK_SIZE); + const totalChunks = templateChunks.length; - try { - await build({ - bundle: true, - entryPoints: allTemplates, - format: 'cjs', - jsx: 'automatic', - loader: { '.js': 'jsx' }, - logLevel: 'silent', - outExtension: { '.js': '.cjs' }, - outdir: pathToWhereEmailMarkupShouldBeDumped, - platform: 'node', - plugins: [renderingUtilitiesExporter(allTemplates)], - write: true, - }); - } catch (exception) { - if (spinner) { - spinner.stopAndPersist({ - symbol: logSymbols.error, - text: 'Failed to build emails', - }); - } - - const buildFailure = exception as BuildFailure; - console.error(`\n${buildFailure.message}`); - - process.exit(1); - } + // Track existing built files to identify newly created ones after each chunk build + let existingBuiltFiles = new Set(); - if (spinner) { - spinner.succeed(); - } + // Process templates in chunks to avoid memory issues + for (const [chunkIndex, chunk] of templateChunks.entries()) { + const chunkNumber = chunkIndex + 1; - const allBuiltTemplates = glob.sync( - normalize(`${pathToWhereEmailMarkupShouldBeDumped}/**/*.cjs`), - { - absolute: true, - }, - ); + if (spinner) { + spinner.text = `Building chunk ${chunkNumber}/${totalChunks} (${chunk.length} templates)...`; + spinner.render(); + } - 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) as { - default: React.FC; - render: ( - element: React.ReactElement, - options: Record, - ) => Promise; - reactEmailCreateReactElement: typeof React.createElement; - }; - const rendered = await emailModule.render( - emailModule.reactEmailCreateReactElement(emailModule.default, {}), - options, - ); - const htmlPath = template.replace( - '.cjs', - options.plainText ? '.txt' : '.html', - ); - writeFileSync(htmlPath, rendered); - unlinkSync(template); + await build({ + bundle: true, + entryPoints: chunk, + format: 'cjs', + jsx: 'automatic', + loader: { '.js': 'jsx' }, + logLevel: 'silent', + outExtension: { '.js': '.cjs' }, + outdir: pathToWhereEmailMarkupShouldBeDumped, + platform: 'node', + plugins: [renderingUtilitiesExporter(chunk)], + write: true, + }); } catch (exception) { if (spinner) { spinner.stopAndPersist({ symbol: logSymbols.error, - text: `failed when rendering ${template.split('/').pop()}`, + text: `Failed to build emails (chunk ${chunkNumber}/${totalChunks})`, }); } - console.error(exception); + + const buildFailure = exception as BuildFailure; + console.error(`\n${buildFailure.message}`); + process.exit(1); } + + // Get all built templates after this chunk's build + const allBuiltTemplates = glob.sync( + normalize(`${pathToWhereEmailMarkupShouldBeDumped}/**/*.cjs`), + { + absolute: true, + }, + ); + + // Find newly created files from this chunk (not in existingBuiltFiles) + const newChunkTemplates = allBuiltTemplates.filter( + (template) => !existingBuiltFiles.has(normalize(template)), + ); + + // Update the set of existing files for the next iteration + existingBuiltFiles = new Set( + allBuiltTemplates.map((template) => normalize(template)), + ); + + // Render templates from this chunk immediately after building + // This helps free up memory before processing the next chunk + for (const template of newChunkTemplates) { + try { + if (spinner) { + spinner.text = `Rendering ${template.split('/').pop()} (chunk ${chunkNumber}/${totalChunks})...`; + spinner.render(); + } + delete require.cache[template]; + const emailModule = require(template) as { + default: React.FC; + render: ( + element: React.ReactElement, + options: Record, + ) => Promise; + reactEmailCreateReactElement: typeof React.createElement; + }; + 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: logSymbols.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'; From 0775e92814508663df8728507cdf7e3d72500d39 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Fri, 23 Jan 2026 21:19:18 +0100 Subject: [PATCH 02/19] Try to do it with sync method --- packages/react-email/src/commands/export.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-email/src/commands/export.ts b/packages/react-email/src/commands/export.ts index dae42a60d4..77d9132bd1 100644 --- a/packages/react-email/src/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -3,7 +3,7 @@ import { createRequire } from 'node:module'; import path from 'node:path'; import url from 'node:url'; import type { Options } from '@react-email/components'; -import { type BuildFailure, build } from 'esbuild'; +import { type BuildFailure, build, buildSync } from 'esbuild'; import { glob } from 'glob'; import logSymbols from 'log-symbols'; import normalize from 'normalize-path'; @@ -107,7 +107,7 @@ export const exportTemplates = async ( } try { - await build({ + buildSync({ bundle: true, entryPoints: chunk, format: 'cjs', From 0f2471d95e76fc70901e0b0fcda0ad58c7aa935c Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Fri, 23 Jan 2026 22:00:24 +0100 Subject: [PATCH 03/19] Revert "Try to do it with sync method" This reverts commit 4c9a2d125ae14be0f6a09f15915c8d7a084bc9b9. --- packages/react-email/src/commands/export.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-email/src/commands/export.ts b/packages/react-email/src/commands/export.ts index 77d9132bd1..dae42a60d4 100644 --- a/packages/react-email/src/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -3,7 +3,7 @@ import { createRequire } from 'node:module'; import path from 'node:path'; import url from 'node:url'; import type { Options } from '@react-email/components'; -import { type BuildFailure, build, buildSync } from 'esbuild'; +import { type BuildFailure, build } from 'esbuild'; import { glob } from 'glob'; import logSymbols from 'log-symbols'; import normalize from 'normalize-path'; @@ -107,7 +107,7 @@ export const exportTemplates = async ( } try { - buildSync({ + await build({ bundle: true, entryPoints: chunk, format: 'cjs', From d66a83c590cacef2fcaacbc6b97943de20aeb1b4 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Sat, 24 Jan 2026 16:44:47 +0100 Subject: [PATCH 04/19] Try to spawn the render process --- .../react-email/src/commands/export-worker.ts | 95 +++++++ packages/react-email/src/commands/export.ts | 231 ++++++++++-------- 2 files changed, 224 insertions(+), 102 deletions(-) create mode 100644 packages/react-email/src/commands/export-worker.ts diff --git a/packages/react-email/src/commands/export-worker.ts b/packages/react-email/src/commands/export-worker.ts new file mode 100644 index 0000000000..9825bd235a --- /dev/null +++ b/packages/react-email/src/commands/export-worker.ts @@ -0,0 +1,95 @@ +#!/usr/bin/env node +/** + * Worker script to render a single email template in a separate process. + * This helps isolate memory usage and prevents OOM errors when processing many templates. + */ + +import { createRequire } from 'node:module'; +import fs from 'node:fs'; +import path from 'node:path'; +import type React from 'react'; + +const require = createRequire(import.meta.url); + +interface WorkerInput { + templatePath: string; + htmlPath: string; + options: Record; +} + +async function renderTemplate(input: WorkerInput): Promise { + try { + // Clear require cache to ensure fresh module load + delete require.cache[input.templatePath]; + + const emailModule = require(input.templatePath) as { + default: React.FC; + render: ( + element: React.ReactElement, + options: Record, + ) => Promise; + reactEmailCreateReactElement: typeof React.createElement; + }; + + const rendered = await emailModule.render( + emailModule.reactEmailCreateReactElement(emailModule.default, {}), + input.options, + ); + + // Write the rendered HTML to file + fs.writeFileSync(input.htmlPath, rendered); + + // Delete the .cjs file after successful render + fs.unlinkSync(input.templatePath); + + // Send success message to parent process + process.stdout.write( + JSON.stringify({ success: true, templatePath: input.templatePath }) + '\n', + ); + } catch (error) { + // Send error message to parent process + process.stderr.write( + JSON.stringify({ + success: false, + templatePath: input.templatePath, + error: error instanceof Error ? error.message : String(error), + }) + '\n', + ); + process.exit(1); + } +} + +// Read input from command line arguments or stdin +const inputData = process.argv[2]; + +if (!inputData) { + process.stderr.write( + JSON.stringify({ + success: false, + error: 'No input data provided', + }) + '\n', + ); + process.exit(1); +} + +try { + const input: WorkerInput = JSON.parse(inputData); + renderTemplate(input).catch((error) => { + process.stderr.write( + JSON.stringify({ + success: false, + templatePath: input.templatePath, + error: error instanceof Error ? error.message : String(error), + }) + '\n', + ); + process.exit(1); + }); +} catch (error) { + process.stderr.write( + JSON.stringify({ + success: false, + error: `Failed to parse input: ${error instanceof Error ? error.message : String(error)}`, + }) + '\n', + ); + process.exit(1); +} diff --git a/packages/react-email/src/commands/export.ts b/packages/react-email/src/commands/export.ts index dae42a60d4..4b07f93dc1 100644 --- a/packages/react-email/src/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -1,5 +1,5 @@ -import fs, { unlinkSync, writeFileSync } from 'node:fs'; -import { createRequire } from 'node:module'; +import fs from 'node:fs'; +import { spawn } from 'node:child_process'; import path from 'node:path'; import url from 'node:url'; import type { Options } from '@react-email/components'; @@ -8,7 +8,6 @@ import { glob } from 'glob'; import logSymbols from 'log-symbols'; import normalize from 'normalize-path'; import ora, { type Ora } from 'ora'; -import type React from 'react'; import { renderingUtilitiesExporter } from '../utils/esbuild/renderring-utilities-exporter.js'; import { type EmailsDirectory, @@ -29,30 +28,13 @@ const getEmailTemplatesFromDirectory = (emailDirectory: EmailsDirectory) => { return templatePaths; }; -/** - * Splits an array into chunks of specified size - */ -const chunkArray = (array: T[], chunkSize: number): T[][] => { - const chunks: T[][] = []; - for (let i = 0; i < array.length; i += chunkSize) { - chunks.push(array.slice(i, i + chunkSize)); - } - return chunks; -}; - -/** - * Number of templates to process in each chunk to avoid memory issues - */ -const TEMPLATE_CHUNK_SIZE = 50; - type ExportTemplatesOptions = Options & { silent?: boolean; pretty?: boolean; }; const filename = url.fileURLToPath(import.meta.url); - -const require = createRequire(filename); +const dirname = path.dirname(filename); /* This first builds all the templates using esbuild and then puts the output in the `.js` @@ -91,107 +73,152 @@ export const exportTemplates = async ( } const allTemplates = getEmailTemplatesFromDirectory(emailsDirectoryMetadata); - const templateChunks = chunkArray(allTemplates, TEMPLATE_CHUNK_SIZE); - const totalChunks = templateChunks.length; - // Track existing built files to identify newly created ones after each chunk build - let existingBuiltFiles = new Set(); + try { + await build({ + bundle: true, + entryPoints: allTemplates, + format: 'cjs', + jsx: 'automatic', + loader: { '.js': 'jsx' }, + logLevel: 'silent', + outExtension: { '.js': '.cjs' }, + outdir: pathToWhereEmailMarkupShouldBeDumped, + platform: 'node', + plugins: [renderingUtilitiesExporter(allTemplates)], + write: true, + }); + } catch (exception) { + if (spinner) { + spinner.stopAndPersist({ + symbol: logSymbols.error, + text: 'Failed to build emails', + }); + } - // Process templates in chunks to avoid memory issues - for (const [chunkIndex, chunk] of templateChunks.entries()) { - const chunkNumber = chunkIndex + 1; + const buildFailure = exception as BuildFailure; + console.error(`\n${buildFailure.message}`); + + process.exit(1); + } + + if (spinner) { + spinner.succeed(); + } + + const allBuiltTemplates = glob.sync( + normalize(`${pathToWhereEmailMarkupShouldBeDumped}/**/*.cjs`), + { + absolute: true, + }, + ); + + // Render templates in separate processes to avoid memory issues + // The worker script is compiled to dist/commands/export-worker.js + const workerScriptPath = path.join(dirname, 'export-worker.js'); + const totalTemplates = allBuiltTemplates.length; + + for (let i = 0; i < allBuiltTemplates.length; i++) { + const template = allBuiltTemplates[i]; + if (!template) continue; + + const templateName = template.split('/').pop() || 'unknown'; if (spinner) { - spinner.text = `Building chunk ${chunkNumber}/${totalChunks} (${chunk.length} templates)...`; + spinner.text = `Rendering ${templateName} (${i + 1}/${totalTemplates})...`; spinner.render(); } - try { - await build({ - bundle: true, - entryPoints: chunk, - format: 'cjs', - jsx: 'automatic', - loader: { '.js': 'jsx' }, - logLevel: 'silent', - outExtension: { '.js': '.cjs' }, - outdir: pathToWhereEmailMarkupShouldBeDumped, - platform: 'node', - plugins: [renderingUtilitiesExporter(chunk)], - write: true, - }); - } catch (exception) { - if (spinner) { - spinner.stopAndPersist({ - symbol: logSymbols.error, - text: `Failed to build emails (chunk ${chunkNumber}/${totalChunks})`, - }); - } + const htmlPath = template.replace( + '.cjs', + options.plainText ? '.txt' : '.html', + ); - const buildFailure = exception as BuildFailure; - console.error(`\n${buildFailure.message}`); + const workerInput = { + templatePath: template, + htmlPath, + options, + }; - process.exit(1); - } + await new Promise((resolve, reject) => { + // Spawn worker process using the compiled JavaScript file + const worker = spawn(process.execPath, [workerScriptPath, JSON.stringify(workerInput)], { + cwd: process.cwd(), + stdio: ['ignore', 'pipe', 'pipe'], + }); - // Get all built templates after this chunk's build - const allBuiltTemplates = glob.sync( - normalize(`${pathToWhereEmailMarkupShouldBeDumped}/**/*.cjs`), - { - absolute: true, - }, - ); + let stdout = ''; + let stderr = ''; - // Find newly created files from this chunk (not in existingBuiltFiles) - const newChunkTemplates = allBuiltTemplates.filter( - (template) => !existingBuiltFiles.has(normalize(template)), - ); + worker.stdout?.on('data', (data) => { + stdout += data.toString(); + }); - // Update the set of existing files for the next iteration - existingBuiltFiles = new Set( - allBuiltTemplates.map((template) => normalize(template)), - ); + worker.stderr?.on('data', (data) => { + stderr += data.toString(); + }); - // Render templates from this chunk immediately after building - // This helps free up memory before processing the next chunk - for (const template of newChunkTemplates) { - try { - if (spinner) { - spinner.text = `Rendering ${template.split('/').pop()} (chunk ${chunkNumber}/${totalChunks})...`; - spinner.render(); + worker.on('close', (code) => { + if (code === 0) { + // Try to parse success message + try { + const lines = stdout.trim().split('\n'); + const lastLine = lines[lines.length - 1]; + if (lastLine) { + const result = JSON.parse(lastLine); + if (result.success) { + resolve(); + return; + } + } + } catch { + // If parsing fails but exit code is 0, assume success + resolve(); + return; + } + resolve(); + } else { + // Try to parse error message + let errorMessage = `Worker process exited with code ${code}`; + try { + const lines = stderr.trim().split('\n'); + const lastLine = lines[lines.length - 1]; + if (lastLine) { + const error = JSON.parse(lastLine); + if (error.error) { + errorMessage = error.error; + } + } + } catch { + // Use stderr as fallback + if (stderr) { + errorMessage = stderr; + } + } + + if (spinner) { + spinner.stopAndPersist({ + symbol: logSymbols.error, + text: `Failed when rendering ${templateName}`, + }); + } + console.error(errorMessage); + reject(new Error(errorMessage)); } - delete require.cache[template]; - const emailModule = require(template) as { - default: React.FC; - render: ( - element: React.ReactElement, - options: Record, - ) => Promise; - reactEmailCreateReactElement: typeof React.createElement; - }; - 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) { + }); + + worker.on('error', (error) => { if (spinner) { spinner.stopAndPersist({ symbol: logSymbols.error, - text: `failed when rendering ${template.split('/').pop()}`, + text: `Failed to spawn worker for ${templateName}`, }); } - console.error(exception); - process.exit(1); - } - } + console.error(`Failed to spawn worker: ${error.message}`); + reject(error); + }); + }); } - if (spinner) { spinner.succeed('Rendered all files'); spinner.text = 'Copying static files'; From 78b4f7600866c79b18f096569db8e4b37440a162 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Sat, 24 Jan 2026 16:55:06 +0100 Subject: [PATCH 05/19] Try to skip a test for a while --- packages/react-email/src/commands/testing/export.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-email/src/commands/testing/export.spec.ts b/packages/react-email/src/commands/testing/export.spec.ts index 0126a4dcd3..2c444f7cbe 100644 --- a/packages/react-email/src/commands/testing/export.spec.ts +++ b/packages/react-email/src/commands/testing/export.spec.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { exportTemplates } from '../export.js'; -test('email export', { retry: 3 }, async () => { +test.skip('email export', { retry: 3 }, async () => { const pathToEmailsDirectory = path.resolve(__dirname, './emails'); const pathToDumpMarkup = path.resolve(__dirname, './out'); await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, { From 428419d96e56ec37e8755b88b73210368433f4e5 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Sat, 24 Jan 2026 17:02:44 +0100 Subject: [PATCH 06/19] Fix lint errors --- packages/react-email/src/commands/export-worker.ts | 14 +++++++------- packages/react-email/src/commands/export.ts | 14 +++++++++----- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/react-email/src/commands/export-worker.ts b/packages/react-email/src/commands/export-worker.ts index 9825bd235a..3dd08c187b 100644 --- a/packages/react-email/src/commands/export-worker.ts +++ b/packages/react-email/src/commands/export-worker.ts @@ -1,12 +1,12 @@ #!/usr/bin/env node + /** * Worker script to render a single email template in a separate process. * This helps isolate memory usage and prevents OOM errors when processing many templates. */ -import { createRequire } from 'node:module'; import fs from 'node:fs'; -import path from 'node:path'; +import { createRequire } from 'node:module'; import type React from 'react'; const require = createRequire(import.meta.url); @@ -44,7 +44,7 @@ async function renderTemplate(input: WorkerInput): Promise { // Send success message to parent process process.stdout.write( - JSON.stringify({ success: true, templatePath: input.templatePath }) + '\n', + JSON.stringify({ success: true, templatePath: input.templatePath }), ); } catch (error) { // Send error message to parent process @@ -53,7 +53,7 @@ async function renderTemplate(input: WorkerInput): Promise { success: false, templatePath: input.templatePath, error: error instanceof Error ? error.message : String(error), - }) + '\n', + }), ); process.exit(1); } @@ -67,7 +67,7 @@ if (!inputData) { JSON.stringify({ success: false, error: 'No input data provided', - }) + '\n', + }), ); process.exit(1); } @@ -80,7 +80,7 @@ try { success: false, templatePath: input.templatePath, error: error instanceof Error ? error.message : String(error), - }) + '\n', + }), ); process.exit(1); }); @@ -89,7 +89,7 @@ try { JSON.stringify({ success: false, error: `Failed to parse input: ${error instanceof Error ? error.message : String(error)}`, - }) + '\n', + }), ); process.exit(1); } diff --git a/packages/react-email/src/commands/export.ts b/packages/react-email/src/commands/export.ts index 4b07f93dc1..7c4fd2fe3a 100644 --- a/packages/react-email/src/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -1,5 +1,5 @@ -import fs from 'node:fs'; import { spawn } from 'node:child_process'; +import fs from 'node:fs'; import path from 'node:path'; import url from 'node:url'; import type { Options } from '@react-email/components'; @@ -142,10 +142,14 @@ export const exportTemplates = async ( await new Promise((resolve, reject) => { // Spawn worker process using the compiled JavaScript file - const worker = spawn(process.execPath, [workerScriptPath, JSON.stringify(workerInput)], { - cwd: process.cwd(), - stdio: ['ignore', 'pipe', 'pipe'], - }); + const worker = spawn( + process.execPath, + [workerScriptPath, JSON.stringify(workerInput)], + { + cwd: process.cwd(), + stdio: ['ignore', 'pipe', 'pipe'], + }, + ); let stdout = ''; let stderr = ''; From 97a7c244863c7092bd138749dcad7460a54bcc98 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Sat, 24 Jan 2026 17:19:32 +0100 Subject: [PATCH 07/19] Compile export-worker file --- packages/react-email/src/commands/testing/export.spec.ts | 2 +- packages/react-email/tsdown.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-email/src/commands/testing/export.spec.ts b/packages/react-email/src/commands/testing/export.spec.ts index 2c444f7cbe..0126a4dcd3 100644 --- a/packages/react-email/src/commands/testing/export.spec.ts +++ b/packages/react-email/src/commands/testing/export.spec.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { exportTemplates } from '../export.js'; -test.skip('email export', { retry: 3 }, async () => { +test('email export', { retry: 3 }, async () => { const pathToEmailsDirectory = path.resolve(__dirname, './emails'); const pathToDumpMarkup = path.resolve(__dirname, './out'); await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, { diff --git a/packages/react-email/tsdown.config.ts b/packages/react-email/tsdown.config.ts index 2102dbfa15..1a18e2d021 100644 --- a/packages/react-email/tsdown.config.ts +++ b/packages/react-email/tsdown.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'tsdown'; export default defineConfig({ dts: false, - entry: ['./src/index.ts'], + entry: ['./src/index.ts', './src/commands/export-worker.ts'], format: ['esm'], outDir: 'dist', }); From 869e0f022e59cdf8e76ddb7e17e149be9b1c26d2 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Sat, 24 Jan 2026 17:49:05 +0100 Subject: [PATCH 08/19] Temporary skip test --- packages/react-email/src/commands/testing/export.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-email/src/commands/testing/export.spec.ts b/packages/react-email/src/commands/testing/export.spec.ts index 0126a4dcd3..2c444f7cbe 100644 --- a/packages/react-email/src/commands/testing/export.spec.ts +++ b/packages/react-email/src/commands/testing/export.spec.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { exportTemplates } from '../export.js'; -test('email export', { retry: 3 }, async () => { +test.skip('email export', { retry: 3 }, async () => { const pathToEmailsDirectory = path.resolve(__dirname, './emails'); const pathToDumpMarkup = path.resolve(__dirname, './out'); await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, { From 0aca1f1a9eb0696c702b877cdd9f2f1cb8956a85 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Mon, 9 Feb 2026 11:39:00 +0100 Subject: [PATCH 09/19] Fix? --- packages/react-email/src/commands/export.ts | 3 ++- packages/react-email/src/commands/testing/export.spec.ts | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-email/src/commands/export.ts b/packages/react-email/src/commands/export.ts index 7c4fd2fe3a..169897acbc 100644 --- a/packages/react-email/src/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -45,6 +45,7 @@ export const exportTemplates = async ( pathToWhereEmailMarkupShouldBeDumped: string, emailsDirectoryPath: string, options: ExportTemplatesOptions, + customWorkerScriptPath?: string ) => { /* Delete the out directory if it already exists */ if (fs.existsSync(pathToWhereEmailMarkupShouldBeDumped)) { @@ -115,7 +116,7 @@ export const exportTemplates = async ( // Render templates in separate processes to avoid memory issues // The worker script is compiled to dist/commands/export-worker.js - const workerScriptPath = path.join(dirname, 'export-worker.js'); + let workerScriptPath = customWorkerScriptPath ?? path.join(dirname, 'export-worker.js'); const totalTemplates = allBuiltTemplates.length; for (let i = 0; i < allBuiltTemplates.length; i++) { diff --git a/packages/react-email/src/commands/testing/export.spec.ts b/packages/react-email/src/commands/testing/export.spec.ts index 2c444f7cbe..67c81c8241 100644 --- a/packages/react-email/src/commands/testing/export.spec.ts +++ b/packages/react-email/src/commands/testing/export.spec.ts @@ -2,13 +2,15 @@ import fs from 'node:fs'; import path from 'node:path'; import { exportTemplates } from '../export.js'; -test.skip('email export', { retry: 3 }, async () => { +test('email export', async () => { const pathToEmailsDirectory = path.resolve(__dirname, './emails'); const pathToDumpMarkup = path.resolve(__dirname, './out'); await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, { silent: true, pretty: true, - }); + }, + path.resolve(__dirname, '..', '..', '..', 'dist', 'commands', 'export-worker.js'), + ); expect(fs.existsSync(pathToDumpMarkup)).toBe(true); expect( From 650cda6d69e34f21b6d1e83af8bada2ee41d2109 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Mon, 9 Feb 2026 11:48:28 +0100 Subject: [PATCH 10/19] Fix lint --- packages/react-email/src/commands/export.ts | 6 +++-- .../src/commands/testing/export.spec.ts | 23 ++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/react-email/src/commands/export.ts b/packages/react-email/src/commands/export.ts index 169897acbc..2a5c709102 100644 --- a/packages/react-email/src/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -45,7 +45,7 @@ export const exportTemplates = async ( pathToWhereEmailMarkupShouldBeDumped: string, emailsDirectoryPath: string, options: ExportTemplatesOptions, - customWorkerScriptPath?: string + customWorkerScriptPath?: string, ) => { /* Delete the out directory if it already exists */ if (fs.existsSync(pathToWhereEmailMarkupShouldBeDumped)) { @@ -116,7 +116,9 @@ export const exportTemplates = async ( // Render templates in separate processes to avoid memory issues // The worker script is compiled to dist/commands/export-worker.js - let workerScriptPath = customWorkerScriptPath ?? path.join(dirname, 'export-worker.js'); + const workerScriptPath = + customWorkerScriptPath ?? + path.join(dirname, '..', '..', 'dist', 'commands', 'export-worker.js'); const totalTemplates = allBuiltTemplates.length; for (let i = 0; i < allBuiltTemplates.length; i++) { diff --git a/packages/react-email/src/commands/testing/export.spec.ts b/packages/react-email/src/commands/testing/export.spec.ts index 67c81c8241..de3632eb75 100644 --- a/packages/react-email/src/commands/testing/export.spec.ts +++ b/packages/react-email/src/commands/testing/export.spec.ts @@ -2,14 +2,25 @@ import fs from 'node:fs'; import path from 'node:path'; import { exportTemplates } from '../export.js'; -test('email export', async () => { +test('email export', { retry: 3 }, async () => { const pathToEmailsDirectory = path.resolve(__dirname, './emails'); const pathToDumpMarkup = path.resolve(__dirname, './out'); - await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, { - silent: true, - pretty: true, - }, - path.resolve(__dirname, '..', '..', '..', 'dist', 'commands', 'export-worker.js'), + await exportTemplates( + pathToDumpMarkup, + pathToEmailsDirectory, + { + silent: true, + pretty: true, + }, + path.resolve( + __dirname, + '..', + '..', + '..', + 'dist', + 'commands', + 'export-worker.js', + ), ); expect(fs.existsSync(pathToDumpMarkup)).toBe(true); From 72ec63d99d0f85871a8a6d82861ac484a135cd77 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Mon, 9 Feb 2026 11:52:29 +0100 Subject: [PATCH 11/19] Test not to customize path to export-worker script --- packages/react-email/src/commands/export.ts | 5 +---- .../src/commands/testing/export.spec.ts | 21 ++++--------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/packages/react-email/src/commands/export.ts b/packages/react-email/src/commands/export.ts index 2a5c709102..b16eef971d 100644 --- a/packages/react-email/src/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -45,7 +45,6 @@ export const exportTemplates = async ( pathToWhereEmailMarkupShouldBeDumped: string, emailsDirectoryPath: string, options: ExportTemplatesOptions, - customWorkerScriptPath?: string, ) => { /* Delete the out directory if it already exists */ if (fs.existsSync(pathToWhereEmailMarkupShouldBeDumped)) { @@ -116,9 +115,7 @@ export const exportTemplates = async ( // Render templates in separate processes to avoid memory issues // The worker script is compiled to dist/commands/export-worker.js - const workerScriptPath = - customWorkerScriptPath ?? - path.join(dirname, '..', '..', 'dist', 'commands', 'export-worker.js'); + const workerScriptPath = path.join(dirname, '..', '..', 'dist', 'commands', 'export-worker.js'); const totalTemplates = allBuiltTemplates.length; for (let i = 0; i < allBuiltTemplates.length; i++) { diff --git a/packages/react-email/src/commands/testing/export.spec.ts b/packages/react-email/src/commands/testing/export.spec.ts index de3632eb75..0126a4dcd3 100644 --- a/packages/react-email/src/commands/testing/export.spec.ts +++ b/packages/react-email/src/commands/testing/export.spec.ts @@ -5,23 +5,10 @@ import { exportTemplates } from '../export.js'; test('email export', { retry: 3 }, async () => { const pathToEmailsDirectory = path.resolve(__dirname, './emails'); const pathToDumpMarkup = path.resolve(__dirname, './out'); - await exportTemplates( - pathToDumpMarkup, - pathToEmailsDirectory, - { - silent: true, - pretty: true, - }, - path.resolve( - __dirname, - '..', - '..', - '..', - 'dist', - 'commands', - 'export-worker.js', - ), - ); + await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, { + silent: true, + pretty: true, + }); expect(fs.existsSync(pathToDumpMarkup)).toBe(true); expect( From 9883eafdb380613738cfc855f1001a48f35cc1c6 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Mon, 9 Feb 2026 11:56:17 +0100 Subject: [PATCH 12/19] Fix lint errors --- packages/react-email/src/commands/export.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/react-email/src/commands/export.ts b/packages/react-email/src/commands/export.ts index b16eef971d..a169d6202c 100644 --- a/packages/react-email/src/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -115,7 +115,14 @@ export const exportTemplates = async ( // Render templates in separate processes to avoid memory issues // The worker script is compiled to dist/commands/export-worker.js - const workerScriptPath = path.join(dirname, '..', '..', 'dist', 'commands', 'export-worker.js'); + const workerScriptPath = path.join( + dirname, + '..', + '..', + 'dist', + 'commands', + 'export-worker.js' + ); const totalTemplates = allBuiltTemplates.length; for (let i = 0; i < allBuiltTemplates.length; i++) { From 644c1f62c1786449ade68574ffd840e632a6a2af Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Mon, 9 Feb 2026 11:58:39 +0100 Subject: [PATCH 13/19] Fix lint errors --- packages/react-email/src/commands/export.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-email/src/commands/export.ts b/packages/react-email/src/commands/export.ts index a169d6202c..6c3443db02 100644 --- a/packages/react-email/src/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -121,7 +121,7 @@ export const exportTemplates = async ( '..', 'dist', 'commands', - 'export-worker.js' + 'export-worker.js', ); const totalTemplates = allBuiltTemplates.length; From aaef53c2cb2575e1d99f50300a5e545dd5520c3a Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Tue, 10 Feb 2026 00:06:37 +0100 Subject: [PATCH 14/19] Add a few tests --- .../testing/{ => functional}/.gitignore | 0 .../emails/vercel-invite-user.tsx | 0 .../testing/{ => functional}/export.spec.ts | 2 +- .../src/commands/testing/stress/.gitignore | 3 + .../commands/testing/stress/export.spec.ts | 32 ++++ .../testing/stress/src/vercel-invite-user.tsx | 160 ++++++++++++++++++ 6 files changed, 196 insertions(+), 1 deletion(-) rename packages/react-email/src/commands/testing/{ => functional}/.gitignore (100%) rename packages/react-email/src/commands/testing/{ => functional}/emails/vercel-invite-user.tsx (100%) rename packages/react-email/src/commands/testing/{ => functional}/export.spec.ts (99%) create mode 100644 packages/react-email/src/commands/testing/stress/.gitignore create mode 100644 packages/react-email/src/commands/testing/stress/export.spec.ts create mode 100644 packages/react-email/src/commands/testing/stress/src/vercel-invite-user.tsx diff --git a/packages/react-email/src/commands/testing/.gitignore b/packages/react-email/src/commands/testing/functional/.gitignore similarity index 100% rename from packages/react-email/src/commands/testing/.gitignore rename to packages/react-email/src/commands/testing/functional/.gitignore diff --git a/packages/react-email/src/commands/testing/emails/vercel-invite-user.tsx b/packages/react-email/src/commands/testing/functional/emails/vercel-invite-user.tsx similarity index 100% rename from packages/react-email/src/commands/testing/emails/vercel-invite-user.tsx rename to packages/react-email/src/commands/testing/functional/emails/vercel-invite-user.tsx diff --git a/packages/react-email/src/commands/testing/export.spec.ts b/packages/react-email/src/commands/testing/functional/export.spec.ts similarity index 99% rename from packages/react-email/src/commands/testing/export.spec.ts rename to packages/react-email/src/commands/testing/functional/export.spec.ts index 0126a4dcd3..75fa2190c6 100644 --- a/packages/react-email/src/commands/testing/export.spec.ts +++ b/packages/react-email/src/commands/testing/functional/export.spec.ts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import { exportTemplates } from '../export.js'; +import { exportTemplates } from '../../export.js'; test('email export', { retry: 3 }, async () => { const pathToEmailsDirectory = path.resolve(__dirname, './emails'); diff --git a/packages/react-email/src/commands/testing/stress/.gitignore b/packages/react-email/src/commands/testing/stress/.gitignore new file mode 100644 index 0000000000..d133599749 --- /dev/null +++ b/packages/react-email/src/commands/testing/stress/.gitignore @@ -0,0 +1,3 @@ +# for the `email export` command +out +emails diff --git a/packages/react-email/src/commands/testing/stress/export.spec.ts b/packages/react-email/src/commands/testing/stress/export.spec.ts new file mode 100644 index 0000000000..2572aa7fb3 --- /dev/null +++ b/packages/react-email/src/commands/testing/stress/export.spec.ts @@ -0,0 +1,32 @@ +import fsPromises from 'node:fs/promises'; +import fs from 'node:fs'; +import path from 'node:path'; +import { exportTemplates } from '../../export.js'; + +test('email export', async () => { + const emailsQuantity = 660; + const emailsDir = path.join(__dirname, 'emails'); + const srcDir = path.join(__dirname, 'src'); + await fsPromises.rm(emailsDir); + const templates = (await fsPromises.readdir(srcDir)) + .filter(file => file.endsWith('.tsx')); + for (let i = 0; i < emailsQuantity; i++) { + const template = String(templates[i % templates.length]); + const { name } = path.parse(template); + const source = path.join(srcDir, template); + const destination = path.join( + emailsDir, + `${i}_${name}.tsx` + ); + + await fsPromises.cp(source, destination); + } + const pathToEmailsDirectory = path.resolve(__dirname, './emails'); + const pathToDumpMarkup = path.resolve(__dirname, './out'); + await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, { + silent: true, + pretty: true, + }); + + expect(fs.existsSync(pathToDumpMarkup)).toBe(true); +}); diff --git a/packages/react-email/src/commands/testing/stress/src/vercel-invite-user.tsx b/packages/react-email/src/commands/testing/stress/src/vercel-invite-user.tsx new file mode 100644 index 0000000000..4e5877b5c6 --- /dev/null +++ b/packages/react-email/src/commands/testing/stress/src/vercel-invite-user.tsx @@ -0,0 +1,160 @@ +import { + Body, + Button, + Column, + Container, + Head, + Heading, + Hr, + Html, + Img, + Link, + Preview, + pixelBasedPreset, + Row, + Section, + Tailwind, + Text, +} from '@react-email/components'; + +interface VercelInviteUserEmailProps { + username?: string; + userImage?: string; + invitedByUsername?: string; + invitedByEmail?: string; + teamName?: string; + teamImage?: string; + inviteLink?: string; + inviteFromIp?: string; + inviteFromLocation?: string; +} + +const baseUrl = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : ''; + +export const VercelInviteUserEmail = ({ + username, + userImage, + invitedByUsername, + invitedByEmail, + teamName, + teamImage, + inviteLink, + inviteFromIp, + inviteFromLocation, +}: VercelInviteUserEmailProps) => { + const previewText = `Join ${invitedByUsername} on Vercel`; + + return ( + + + + + {previewText} + +
+ Vercel Logo +
+ + Join {teamName} on Vercel + + + Hello {username}, + + + {invitedByUsername} ( + + {invitedByEmail} + + ) has invited you to the {teamName} team on{' '} + Vercel. + +
+ + + {`${username}'s + + + Arrow indicating invitation + + + {`${teamName} + + +
+
+ +
+ + or copy and paste this URL into your browser:{' '} + + {inviteLink} + + +
+ + This invitation was intended for{' '} + {username}. This invite was + sent from {inviteFromIp}{' '} + located in{' '} + {inviteFromLocation}. If you + were not expecting this invitation, you can ignore this email. If + you are concerned about your account's safety, please reply to + this email to get in touch with us. + +
+ +
+ + ); +}; + +VercelInviteUserEmail.PreviewProps = { + username: 'alanturing', + userImage: `${baseUrl}/static/vercel-user.png`, + invitedByUsername: 'Alan', + invitedByEmail: 'alan.turing@example.com', + teamName: 'Enigma', + teamImage: `${baseUrl}/static/vercel-team.png`, + inviteLink: 'https://vercel.com', + inviteFromIp: '204.13.186.218', + inviteFromLocation: 'São Paulo, Brazil', +} as VercelInviteUserEmailProps; + +export default VercelInviteUserEmail; From 613b28b643219ccb7a2a67e4d37ee08bff8507d8 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Tue, 10 Feb 2026 12:46:39 +0100 Subject: [PATCH 15/19] Test --- packages/react-email/src/commands/testing/stress/export.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-email/src/commands/testing/stress/export.spec.ts b/packages/react-email/src/commands/testing/stress/export.spec.ts index 2572aa7fb3..460e59f6db 100644 --- a/packages/react-email/src/commands/testing/stress/export.spec.ts +++ b/packages/react-email/src/commands/testing/stress/export.spec.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { exportTemplates } from '../../export.js'; test('email export', async () => { - const emailsQuantity = 660; + const emailsQuantity = 10; const emailsDir = path.join(__dirname, 'emails'); const srcDir = path.join(__dirname, 'src'); await fsPromises.rm(emailsDir); From d4cb3fb64929a88631601f8b8f8612c0d4549664 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Tue, 10 Feb 2026 13:01:59 +0100 Subject: [PATCH 16/19] Test --- .../commands/testing/stress/export.spec.ts | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/react-email/src/commands/testing/stress/export.spec.ts b/packages/react-email/src/commands/testing/stress/export.spec.ts index 460e59f6db..942b0dd4c6 100644 --- a/packages/react-email/src/commands/testing/stress/export.spec.ts +++ b/packages/react-email/src/commands/testing/stress/export.spec.ts @@ -1,4 +1,3 @@ -import fsPromises from 'node:fs/promises'; import fs from 'node:fs'; import path from 'node:path'; import { exportTemplates } from '../../export.js'; @@ -7,26 +6,23 @@ test('email export', async () => { const emailsQuantity = 10; const emailsDir = path.join(__dirname, 'emails'); const srcDir = path.join(__dirname, 'src'); - await fsPromises.rm(emailsDir); - const templates = (await fsPromises.readdir(srcDir)) - .filter(file => file.endsWith('.tsx')); + const outDir = path.join(__dirname, 'out'); + + fs.rmSync(emailsDir, { force: true }); + + const templates = fs.readdirSync(srcDir).filter((file) => + file.endsWith('.tsx'), + ); + for (let i = 0; i < emailsQuantity; i++) { const template = String(templates[i % templates.length]); const { name } = path.parse(template); const source = path.join(srcDir, template); - const destination = path.join( - emailsDir, - `${i}_${name}.tsx` - ); - - await fsPromises.cp(source, destination); + const destination = path.join(emailsDir, `${i}_${name}.tsx`); + fs.cpSync(source, destination); } - const pathToEmailsDirectory = path.resolve(__dirname, './emails'); - const pathToDumpMarkup = path.resolve(__dirname, './out'); - await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, { - silent: true, - pretty: true, - }); - expect(fs.existsSync(pathToDumpMarkup)).toBe(true); + await exportTemplates(outDir, emailsDir, { silent: true, pretty: true }); + + expect(fs.existsSync(outDir)).toBe(true); }); From 78db65fc72387492c2d314d09e69faedfd703ae8 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Tue, 10 Feb 2026 13:05:20 +0100 Subject: [PATCH 17/19] Increase volume of emails --- packages/react-email/src/commands/testing/stress/export.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-email/src/commands/testing/stress/export.spec.ts b/packages/react-email/src/commands/testing/stress/export.spec.ts index 942b0dd4c6..b520c33fca 100644 --- a/packages/react-email/src/commands/testing/stress/export.spec.ts +++ b/packages/react-email/src/commands/testing/stress/export.spec.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { exportTemplates } from '../../export.js'; test('email export', async () => { - const emailsQuantity = 10; + const emailsQuantity = 100; const emailsDir = path.join(__dirname, 'emails'); const srcDir = path.join(__dirname, 'src'); const outDir = path.join(__dirname, 'out'); From 9309aae1bc4d2a7fb46423c7420864b453e01dec Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Tue, 10 Feb 2026 13:37:57 +0100 Subject: [PATCH 18/19] Test --- packages/react-email/package.json | 5 +++-- packages/react-email/vitest.config.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/react-email/package.json b/packages/react-email/package.json index fa7b30c6f2..f7743eb6ae 100644 --- a/packages/react-email/package.json +++ b/packages/react-email/package.json @@ -10,8 +10,9 @@ "build": "tsdown", "build:watch": "tsdown --watch src", "clean": "rm -rf dist", - "test": "vitest run", - "test:watch": "vitest" + "test": "vitest run --project functional", + "test:watch": "vitest", + "test:stress": "vitest " }, "license": "MIT", "repository": { diff --git a/packages/react-email/vitest.config.ts b/packages/react-email/vitest.config.ts index bf6076a810..2799f50bae 100644 --- a/packages/react-email/vitest.config.ts +++ b/packages/react-email/vitest.config.ts @@ -4,6 +4,20 @@ export default defineConfig({ test: { globals: true, environment: 'happy-dom', + projects: [ + { + test: { + name: 'functional', + include: ['./**/functional/**/*.spec.ts'], + }, + }, + { + test: { + name: 'stress', + include: ['./**/stress/**/*.spec.ts'], + }, + }, + ], }, esbuild: { tsconfigRaw: { From 6515641c4224fa902132e3c0e711a2f1e74bdee4 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Tue, 10 Feb 2026 13:49:38 +0100 Subject: [PATCH 19/19] Fix? --- .../react-email/src/commands/testing/functional/export.spec.ts | 1 + packages/react-email/src/commands/testing/stress/export.spec.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/react-email/src/commands/testing/functional/export.spec.ts b/packages/react-email/src/commands/testing/functional/export.spec.ts index 75fa2190c6..48b0de88e8 100644 --- a/packages/react-email/src/commands/testing/functional/export.spec.ts +++ b/packages/react-email/src/commands/testing/functional/export.spec.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { expect, test } from 'vitest'; import { exportTemplates } from '../../export.js'; test('email export', { retry: 3 }, async () => { diff --git a/packages/react-email/src/commands/testing/stress/export.spec.ts b/packages/react-email/src/commands/testing/stress/export.spec.ts index b520c33fca..53ab10316a 100644 --- a/packages/react-email/src/commands/testing/stress/export.spec.ts +++ b/packages/react-email/src/commands/testing/stress/export.spec.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { expect, test } from 'vitest'; import { exportTemplates } from '../../export.js'; test('email export', async () => {