Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cbf4f3b
feat(demo): use Tailwind v4 on all templates (#2487)
bukinoshita Oct 16, 2025
59c58eb
fix: lockfile
gabrielmfern Oct 17, 2025
4197fff
move preview-server to react-email/ui
gabrielmfern Oct 16, 2025
8cf8dac
Revert "move preview-server to react-email/ui"
gabrielmfern Oct 16, 2025
519c525
add code to download the preview server from npm, and install next to it
gabrielmfern Oct 16, 2025
001a9f9
add spinners to preview server installation
gabrielmfern Oct 17, 2025
fbef658
add directory location to UI installation success message
gabrielmfern Oct 17, 2025
e73e883
add error message when emails directory doesn't exist in build
gabrielmfern Oct 17, 2025
31cafff
simplify
gabrielmfern Oct 17, 2025
7fdd5b5
update `email build` to work with new strategy
gabrielmfern Oct 17, 2025
68af13c
remove dependency on nypm
gabrielmfern Oct 17, 2025
221ea23
lint
gabrielmfern Oct 17, 2025
b191c86
update snaps, and make test more reliable
gabrielmfern Oct 17, 2025
3cb975b
make tree run in a sub directory where no other tests can cause race …
gabrielmfern Oct 17, 2025
0e7ae4c
feat(demo): use Tailwind v4 on all templates (#2487)
bukinoshita Oct 16, 2025
0a7d210
fix: lockfile
gabrielmfern Oct 17, 2025
ddd37a4
feat(tailwind): update to tailwind v4 (#2425)
gabrielmfern Oct 17, 2025
19de23f
chore(demo): use local tailwind version
gabrielmfern Oct 17, 2025
9360e39
chore: remove preview version and add changeset
gabrielmfern Oct 17, 2025
26ee920
chore: update lockfile
gabrielmfern Oct 17, 2025
3c8938f
chore(root): version packages (canary) (#2578)
github-actions[bot] Oct 17, 2025
3e5ea15
fix success message always showing
gabrielmfern Oct 20, 2025
b134d7d
Merge branch 'canary' into feat/new-preview-server-strategy
gabrielmfern Oct 20, 2025
5871d86
fix lock
gabrielmfern Oct 20, 2025
678be46
Merge branch 'canary' into feat/new-preview-server-strategy
gabrielmfern Oct 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/react-email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
"log-symbols": "^7.0.0",
"mime-types": "^3.0.0",
"normalize-path": "^3.0.0",
"nypm": "0.6.0",
"ora": "^8.0.0",
"prompts": "2.4.2",
"socket.io": "^4.8.1",
"tar": "^7.5.1",
"tsconfig-paths": "4.2.0"
},
"devDependencies": {
Expand Down
23 changes: 14 additions & 9 deletions packages/react-email/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ export function generateStaticParams() {
};

const updatePackageJson = async (builtPreviewAppPath: string) => {
/* @see ../utils/get-preview-server-location.ts */
await fs.promises.rm(path.resolve(builtPreviewAppPath, './package.json'));
await fs.promises.rename(
path.resolve(builtPreviewAppPath, './package.source.json'),
path.resolve(builtPreviewAppPath, './package.json'),
);
const packageJsonPath = path.resolve(builtPreviewAppPath, './package.json');
const packageJson = JSON.parse(
await fs.promises.readFile(packageJsonPath, 'utf8'),
Expand Down Expand Up @@ -239,6 +245,10 @@ export const build = async ({

spinner.text = `Checking if ${emailsDirRelativePath} folder exists`;
if (!fs.existsSync(emailsDirRelativePath)) {
spinner.stopAndPersist({
symbol: logSymbols.error,
text: `Directory does not exist: ${emailsDirRelativePath}`,
});
process.exit(1);
}

Expand All @@ -255,15 +265,10 @@ export const build = async ({
spinner.text = 'Copying preview app from CLI to `.react-email`';
await fs.promises.cp(previewServerLocation, builtPreviewAppPath, {
recursive: true,
filter: (source: string) => {
// do not copy the CLI files
return (
!/(\/|\\)cli(\/|\\)?/.test(source) &&
!/(\/|\\)\.next(\/|\\)?/.test(source) &&
!/(\/|\\)\.turbo(\/|\\)?/.test(source) &&
!/(\/|\\)node_modules(\/|\\)?$/.test(source)
);
},
filter: (source: string) =>
!/(\/|\\)\.next(\/|\\)?/.test(source) &&
!/(\/|\\)\.turbo(\/|\\)?/.test(source) &&
!/(\/|\\)node_modules(\/|\\)?$/.test(source),
});

if (fs.existsSync(staticPath)) {
Expand Down
1 change: 1 addition & 0 deletions packages/react-email/src/utils/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.test
36 changes: 14 additions & 22 deletions packages/react-email/src/utils/__snapshots__/tree.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`tree(__dirname, 2) 1`] = `
"utils
├── __snapshots__
│ └── tree.spec.ts.snap
├── esbuild
│ ├── escape-string-for-regex.ts
│ └── renderring-utilities-exporter.ts
├── preview
│ ├── hot-reloading
│ ├── get-env-variables-for-preview-app.ts
│ ├── index.ts
│ ├── serve-static-file.ts
│ └── start-dev-server.ts
├── types
│ ├── hot-reload-change.ts
│ └── hot-reload-event.ts
├── get-emails-directory-metadata.spec.ts
├── get-emails-directory-metadata.ts
├── get-preview-server-location.ts
"preview
├── hot-reloading
│ ├── __snapshots__
│ ├── test
│ ├── create-dependency-graph.spec.ts
│ ├── create-dependency-graph.ts
│ ├── get-imported-modules.spec.ts
│ ├── get-imported-modules.ts
│ ├── resolve-path-aliases.spec.ts
│ ├── resolve-path-aliases.ts
│ └── setup-hot-reloading.ts
├── get-env-variables-for-preview-app.ts
├── index.ts
├── packageJson.ts
├── register-spinner-autostopping.ts
├── tree.spec.ts
└── tree.ts"
├── serve-static-file.ts
└── start-dev-server.ts"
`;
17 changes: 17 additions & 0 deletions packages/react-email/src/utils/get-preview-server-location.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import fs from 'node:fs';
import path from 'node:path';
import { installPreviewServer } from './get-preview-server-location.js';
import { packageJson } from './packageJson.js';

test.sequential('installPreviewServer()', { timeout: 10_000 }, async () => {
const testDirectory = path.join(import.meta.dirname, '.test');
await installPreviewServer(testDirectory, packageJson.version);
expect(fs.existsSync(testDirectory)).toBe(true);

// @ts-ignore The directory should exist at this point
const importedModule = await import(path.join(testDirectory, 'index.mjs'));
expect({ ...importedModule }).toEqual({
version: packageJson.version,
});
await fs.promises.rm(testDirectory, { recursive: true, force: true });
});
139 changes: 102 additions & 37 deletions packages/react-email/src/utils/get-preview-server-location.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,115 @@
import child_process from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import url from 'node:url';
import { createJiti } from 'jiti';
import { addDevDependency } from 'nypm';
import prompts from 'prompts';
import logSymbols from 'log-symbols';
import ora from 'ora';
import { extract } from 'tar';
import { packageJson } from './packageJson.js';
import { registerSpinnerAutostopping } from './register-spinner-autostopping.js';

const ensurePreviewServerInstalled = async (
message: string,
): Promise<never> => {
const response = await prompts({
type: 'confirm',
name: 'installPreviewServer',
message,
initial: true,
export async function installPreviewServer(directory: string, version: string) {
const spinner = ora({
text: 'Installing UI',
prefixText: ' ',
});
if (response.installPreviewServer) {
console.log('Installing "@react-email/preview-server"');
await addDevDependency(
`@react-email/preview-server@${packageJson.version}`,
);
process.exit(0);
} else {
process.exit(0);
}
};
spinner.start();

export const getPreviewServerLocation = async () => {
const usersProject = createJiti(process.cwd());
let previewServerLocation!: string;
registerSpinnerAutostopping(spinner);
if (fs.existsSync(directory)) {
await fs.promises.rm(directory, { recursive: true });
}
await fs.promises.mkdir(directory);
// Download and unpack the package
const tempDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), 'react-email-'),
);
try {
previewServerLocation = path.dirname(
url.fileURLToPath(usersProject.esmResolve('@react-email/preview-server')),
// Download package using npm pack
child_process.execSync(`npm pack @react-email/preview-server@${version}`, {
cwd: tempDir,
stdio: 'ignore',
});

// Find the downloaded tarball
const files = await fs.promises.readdir(tempDir);
const tarball = files.find(
(file) =>
file.startsWith('react-email-preview-server-') && file.endsWith('.tgz'),
);

if (!tarball) {
spinner.stopAndPersist({
symbol: logSymbols.error,
text: 'Failed to install UI',
});
throw new Error('Failed to find tarball for UI', {
cause: { tempDir, files },
});
}

// Extract tarball to directory using tar package
const tarballPath = path.join(tempDir, tarball);
try {
await extract({
cwd: directory,
strip: 1,
file: tarballPath,
z: true,
});
} catch (exception) {
spinner.stopAndPersist({
symbol: logSymbols.error,
text: 'Failed to install UI',
});
throw new Error('Failed to extract UI package', {
cause: exception,
});
}

const packageJsonPath = path.resolve(directory, './package.json');
await fs.promises.cp(
packageJsonPath,
path.resolve(directory, './package.source.json'),
);
const packageJson = JSON.parse(
await fs.promises.readFile(packageJsonPath, 'utf8'),
);
} catch (_exception) {
await ensurePreviewServerInstalled(
'To run the preview server, the package "@react-email/preview-server" must be installed. Would you like to install it?',
packageJson.dependencies = {
next: packageJson.dependencies.next,
};
packageJson.devDependencies = {};
await fs.promises.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
'utf8',
);
child_process.execSync('npm install --silent', {
stdio: 'ignore',
cwd: directory,
});
} finally {
// Clean up temp directory
await fs.promises.rm(tempDir, { recursive: true, force: true });
}
const { version } = await usersProject.import<{
spinner.stopAndPersist({
symbol: logSymbols.success,
text: `UI installed successfully (${directory})\n`,
});
}

export async function getPreviewServerLocation() {
const directory = path.join(os.homedir(), '.react-email');
if (!fs.existsSync(directory)) {
await installPreviewServer(directory, packageJson.version);
}

const { version } = (await import(path.join(directory, 'index.mjs'))) as {
version: string;
}>('@react-email/preview-server');
};
if (version !== packageJson.version) {
await ensurePreviewServerInstalled(
`To run the preview server, the version of "@react-email/preview-server" must match the version of "react-email" (${packageJson.version}). Would you like to install it?`,
);
await installPreviewServer(directory, packageJson.version);
}

return previewServerLocation;
};
return directory;
}
5 changes: 4 additions & 1 deletion packages/react-email/src/utils/tree.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import path from 'node:path';
import { tree } from './tree.js';

test('tree(__dirname, 2)', async () => {
expect(await tree(__dirname, 2)).toMatchSnapshot();
expect(
await tree(path.resolve(import.meta.dirname, './preview'), 2),
).toMatchSnapshot();
});
Loading
Loading