Skip to content
1 change: 1 addition & 0 deletions packages/create-email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"create-email": "src/index.js"
},
"devDependencies": {
"nypm": "0.6.0",
"react": "19.0.0",
"tsconfig": "workspace:*",
"typescript": "5.8.3"
Expand Down
42 changes: 15 additions & 27 deletions packages/create-email/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { spawnSync } from 'node:child_process';
import { existsSync, promises as fs } from 'node:fs';
import path from 'node:path';
import { installDependencies, runScript } from 'nypm';

describe('automatic setup', () => {
const starterPath = path.resolve(__dirname, '../.test');
const starterPath = path.resolve(import.meta.dirname, '../.test');
test.sequential('creation', async () => {
if (existsSync(starterPath)) {
await fs.rm(starterPath, { recursive: true });
}

const createEmailProcess = spawnSync(
'node',
[path.resolve(__dirname, './index.js'), '.test'],
process.execPath,
[path.resolve(import.meta.dirname, './index.js'), '.test'],
{
shell: true,
cwd: path.resolve(__dirname, '../'),
cwd: path.resolve(import.meta.dirname, '../'),
stdio: 'pipe',
},
);
Expand All @@ -26,36 +26,24 @@ describe('automatic setup', () => {
);
});

test.sequential('install', { timeout: 40_000 }, () => {
const installProcess = spawnSync('npm', ['install'], {
shell: true,
cwd: path.resolve(starterPath),
stdio: 'pipe',
test.sequential('install', { timeout: 40_000 }, async () => {
await installDependencies({
cwd: starterPath,
packageManager: 'npm',
});
if (installProcess.stderr) {
console.log(installProcess.stderr.toString());
}
expect(installProcess.status, 'starter npm install should return 0').toBe(
0,
);
});

test.sequential('export', () => {
const exportProcess = spawnSync('npm', ['run export'], {
shell: true,
test.sequential('export', async () => {
await runScript('export', {
cwd: starterPath,
stdio: 'pipe',
packageManager: 'npm',
});
if (exportProcess.stderr) {
console.log(exportProcess.stderr.toString());
}
expect(exportProcess.status, 'export should return status code 0').toBe(0);
});

test.sequential('type checking', { timeout: 10_000 }, () => {
const typecheckingProcess = spawnSync('npx', ['tsc'], {
shell: true,
test.sequential('type checking', { timeout: 10_000 }, async () => {
const typecheckingProcess = spawnSync('npx tsc', {
cwd: starterPath,
shell: true,
stdio: 'pipe',
});
if (typecheckingProcess.stderr) {
Expand Down
2 changes: 2 additions & 0 deletions packages/create-email/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["dist", "build", "node_modules", ".test"],
"compilerOptions": {
"moduleResolution": "nodenext",
"module": "nodenext",
"noEmit": true,
"types": ["vitest/globals"]
}
Expand Down
10 changes: 3 additions & 7 deletions packages/preview-server/scripts/build-preview-server.mts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import url from 'node:url';

const filename = url.fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);

const nextBuildProcess = spawn('pnpm', ['next', 'build'], {
const nextBuildProcess = spawn('pnpm next build', {
detached: true,
shell: true,
stdio: 'inherit',
cwd: path.resolve(dirname, '../'),
cwd: path.resolve(import.meta.dirname, '../'),
});

process.on('SIGINT', () => {
Expand All @@ -23,7 +19,7 @@ nextBuildProcess.on('exit', (code) => {
process.exit(code);
}

fs.rmSync(path.resolve(dirname, '../.next/cache'), {
fs.rmSync(path.resolve(import.meta.dirname, '../.next/cache'), {
recursive: true,
});
});
2 changes: 1 addition & 1 deletion packages/preview-server/scripts/dev.mts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT=true`,
'utf8',
);

const webServerProcess = child_process.spawn('next', ['dev'], {
const webServerProcess = child_process.spawn('pnpm next dev', {
cwd: previewServerRoot,
shell: true,
stdio: 'inherit',
Expand Down
3 changes: 2 additions & 1 deletion packages/preview-server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"noEmit": true,
"strict": false,
"target": "ESNext",
"module": "CommonJS",
"module": "esnext",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"types": ["vitest/globals"],
Expand All @@ -39,6 +39,7 @@
"tailwind-internals.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"next.config.mjs"
Expand Down
71 changes: 11 additions & 60 deletions packages/react-email/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import logSymbols from 'log-symbols';
import { installDependencies, type PackageManagerName, runScript } from 'nypm';
import ora from 'ora';
import {
type EmailsDirectory,
Expand All @@ -12,65 +12,9 @@ import { registerSpinnerAutostopping } from '../utils/register-spinner-autostopp

interface Args {
dir: string;
packageManager: string;
packageManager: PackageManagerName;
}

const buildPreviewApp = (absoluteDirectory: string) => {
return new Promise<void>((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}`,
),
);
}
});
});
};

const npmInstall = async (
builtPreviewAppPath: string,
packageManager: string,
) => {
return new Promise<void>((resolve, reject) => {
const childProc = spawn(
packageManager,
[
'install',
packageManager === 'deno' ? '' : '--include=dev',
packageManager === 'deno' ? '--quiet' : '--silent',
],
{
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}`,
),
);
}
});
});
};

const setNextEnvironmentVariablesForBuild = async (
emailsDirRelativePath: string,
builtPreviewAppPath: string,
Expand Down Expand Up @@ -283,14 +227,21 @@ export const build = async ({
await updatePackageJson(builtPreviewAppPath);

spinner.text = 'Installing dependencies on `.react-email`';
await npmInstall(builtPreviewAppPath, packageManager);
await installDependencies({
cwd: builtPreviewAppPath,
silent: true,
packageManager,
});

spinner.stopAndPersist({
text: 'Successfully prepared `.react-email` for `next build`',
symbol: logSymbols.success,
});

await buildPreviewApp(builtPreviewAppPath);
await runScript('build', {
packageManager,
cwd: builtPreviewAppPath,
});
} catch (error) {
console.log(error);
process.exit(1);
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading