Skip to content

Commit

Permalink
fix(react-email): Preview server crashing without React 19 (#1861)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielmfern authored Jan 9, 2025
1 parent 3b8aada commit c6fcd94
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 135 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-apes-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-email": patch
---

Fix preview server crashing without React 19
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use server';

import type { EmailsDirectory } from '../utils/get-emails-directory-metadata';
import { getEmailsDirectoryMetadata } from '../utils/get-emails-directory-metadata';

export const getEmailsDirectoryMetadataAction = async (
absolutePathToEmailsDirectory: string,
keepFileExtensions = false,
isSubDirectory = false,

baseDirectoryPath = absolutePathToEmailsDirectory,
): Promise<EmailsDirectory | undefined> => {
return getEmailsDirectoryMetadata(
absolutePathToEmailsDirectory,
keepFileExtensions,
isSubDirectory,
baseDirectoryPath,
);
};
123 changes: 0 additions & 123 deletions packages/react-email/src/actions/get-emails-directory-metadata.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/react-email/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from 'next';
import './globals.css';
import { getEmailsDirectoryMetadata } from '../actions/get-emails-directory-metadata';
import { emailsDirectoryAbsolutePath } from '../utils/emails-directory-absolute-path';
import { EmailsProvider } from '../contexts/emails';
import { getEmailsDirectoryMetadata } from '../utils/get-emails-directory-metadata';
import { inter } from './inter';

export const metadata: Metadata = {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-email/src/app/preview/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import path from 'node:path';
import { Suspense } from 'react';
import { redirect } from 'next/navigation';
import { getEmailPathFromSlug } from '../../../actions/get-email-path-from-slug';
import { getEmailsDirectoryMetadata } from '../../../actions/get-emails-directory-metadata';
import { renderEmailByPath } from '../../../actions/render-email-by-path';
import { emailsDirectoryAbsolutePath } from '../../../utils/emails-directory-absolute-path';
import Home from '../../page';
import { getEmailsDirectoryMetadata } from '../../../utils/get-emails-directory-metadata';
import Preview from './preview';

export const dynamicParams = true;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-email/src/cli/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { spawn } from 'node:child_process';
import {
type EmailsDirectory,
getEmailsDirectoryMetadata,
} from '../../actions/get-emails-directory-metadata';
} from '../../utils/get-emails-directory-metadata';
import { cliPacakgeLocation } from '../utils';
import { registerSpinnerAutostopping } from '../../utils/register-spinner-autostopping';
import logSymbols from 'log-symbols';
Expand Down
2 changes: 1 addition & 1 deletion packages/react-email/src/cli/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { tree } from '../utils';
import {
EmailsDirectory,
getEmailsDirectoryMetadata,
} from '../../actions/get-emails-directory-metadata';
} from '../../utils/get-emails-directory-metadata';
import { renderingUtilitiesExporter } from '../../utils/esbuild/renderring-utilities-exporter';

const getEmailTemplatesFromDirectory = (emailDirectory: EmailsDirectory) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
import * as Collapsible from '@radix-ui/react-collapsible';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import type { EmailsDirectory } from '../../actions/get-emails-directory-metadata';
import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
import { emailsDirectoryAbsolutePath } from '../../utils/emails-directory-absolute-path';
import { cn } from '../../utils';
import { IconFile } from '../icons/icon-file';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as Collapsible from '@radix-ui/react-collapsible';
import * as React from 'react';
import { cn } from '../../utils';
import { type EmailsDirectory } from '../../actions/get-emails-directory-metadata';
import { type EmailsDirectory } from '../../utils/get-emails-directory-metadata';
import { Heading } from '../heading';
import { IconFolder } from '../icons/icon-folder';
import { IconFolderOpen } from '../icons/icon-folder-open';
Expand Down
8 changes: 3 additions & 5 deletions packages/react-email/src/contexts/emails.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
'use client';
import { createContext, useContext, useState } from 'react';
import {
getEmailsDirectoryMetadata,
type EmailsDirectory,
} from '../actions/get-emails-directory-metadata';
import { getEmailsDirectoryMetadataAction } from '../actions/get-emails-directory-metadata-action';
import { useHotreload } from '../hooks/use-hot-reload';
import type { EmailsDirectory } from '../utils/get-emails-directory-metadata';

const EmailsContext = createContext<
| {
Expand Down Expand Up @@ -37,7 +35,7 @@ export const EmailsProvider = (props: {
// the rules of hooks
// eslint-disable-next-line react-hooks/rules-of-hooks
useHotreload(async () => {
const metadata = await getEmailsDirectoryMetadata(
const metadata = await getEmailsDirectoryMetadataAction(
props.initialEmailsDirectoryMetadata.absolutePath,
);
if (metadata) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getEmailsDirectoryMetadata } from './get-emails-directory-metadata';
test('getEmailsDirectoryMetadata on demo emails', async () => {
const emailsDirectoryPath = path.resolve(
__dirname,
'../../../../apps/demo/emails/',
'../../../../apps/demo/emails',
);
expect(await getEmailsDirectoryMetadata(emailsDirectoryPath)).toEqual({
absolutePath: emailsDirectoryPath,
Expand Down
119 changes: 119 additions & 0 deletions packages/react-email/src/utils/get-emails-directory-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import fs from 'node:fs';
import path from 'node:path';

const isFileAnEmail = (fullPath: string): boolean => {
const stat = fs.statSync(fullPath);

if (stat.isDirectory()) return false;

const { ext } = path.parse(fullPath);

if (!['.js', '.tsx', '.jsx'].includes(ext)) return false;

// This is to avoid a possible race condition where the file doesn't exist anymore
// once we are checking if it is an actual email, this couuld cause issues that
// would be very hard to debug and find out the why of it happening.
if (!fs.existsSync(fullPath)) {
return false;
}

// check with a heuristic to see if the file has at least
// a default export
const fileContents = fs.readFileSync(fullPath, 'utf8');

return /\bexport\s+default\b/gm.test(fileContents);
};

export interface EmailsDirectory {
absolutePath: string;
relativePath: string;
directoryName: string;
emailFilenames: string[];
subDirectories: EmailsDirectory[];
}

const mergeDirectoriesWithSubDirectories = (
emailsDirectoryMetadata: EmailsDirectory,
): EmailsDirectory => {
let currentResultingMergedDirectory: EmailsDirectory =
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;
};

export const getEmailsDirectoryMetadata = async (
absolutePathToEmailsDirectory: string,
keepFileExtensions = false,
isSubDirectory = false,

baseDirectoryPath = absolutePathToEmailsDirectory,
): Promise<EmailsDirectory | undefined> => {
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,
) as Promise<EmailsDirectory>;
}),
);

const emailsMetadata = {
absolutePath: absolutePathToEmailsDirectory,
relativePath: path.relative(
baseDirectoryPath,
absolutePathToEmailsDirectory,
),
directoryName: absolutePathToEmailsDirectory.split(path.sep).pop()!,
emailFilenames,
subDirectories,
} satisfies EmailsDirectory;

return isSubDirectory
? mergeDirectoriesWithSubDirectories(emailsMetadata)
: emailsMetadata;
};

1 comment on commit c6fcd94

@vercel
Copy link

@vercel vercel bot commented on c6fcd94 Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

react-email-demo – ./apps/demo

react-email-demo.vercel.app
react-email-demo-resend.vercel.app
react-email-demo-git-main-resend.vercel.app
demo.react.email

Please sign in to comment.