fix(render): lazy-load prettier so it's only loaded when pretty is used

Merged
AM
11h ago
+78-30

Fixes #3781

Problem

pretty.ts imported prettier/standalone and prettier/plugins/html at module top level, so every render() loaded prettier, even without pretty: true.

Next.js always externalizes prettier. In next dev, Turbopack loads externals through a hashed alias symlink (.next/dev/node_modules/prettier-<hash>) that it creates after the server process has started. Bun's resolver doesn't see a node_modules symlink created after the directory was first resolved (oven-sh/bun#42776, oven-sh/bun#25370), so the import fails with Cannot find package 'prettier-<hash>'. Node resolves it fine, and next build + next start on Bun also works because the symlink already exists at startup.

Fix

Import prettier dynamically the first time pretty() is called (cached afterwards), the same way render already loads react-dom/server. pretty was already async, so the public API is unchanged. As a side effect, render() without pretty no longer pays prettier's load cost.

Verification

Packed this build and installed it into the reproduction (https://github.com/pthneo/react-email-turbopack-bun-prettier-bug, Next 16.4.0, Bun 1.4.2):

  • render() without pretty: works on Turbopack + Bun (previously failed), Turbopack + Node, webpack + Bun, webpack + Node.
  • render(..., { pretty: true }): works on Turbopack + Node, webpack + Bun and webpack + Node. On Turbopack + Bun in dev it still hits the Bun resolver bug; transpilePackages: ["prettier"] in next.config works around it.

Added a test that prettier isn't loaded until pretty is called. @react-email/render tests pass (90/90) and biome check is clean.

šŸ¤– Generated with Claude Code


Summary by cubic

Lazy-loads prettier so render no longer loads it unless pretty: true is used.

Previously pretty.ts imported prettier/standalone and prettier/plugins/html at module load time, so every render() call loaded prettier. In Next.js dev with Turbopack, prettier is externalized and resolved through a hashed symlink Bun can't pick up after startup, which made render() fail with "Failed to load external module prettier". Prettier is now imported dynamically on the first pretty() call, keeping the public API unchanged.

  • Fixes render() failing in Next.js dev with Turbopack on Bun.
  • render() without pretty no longer pays prettier's load cost.
  • Adds a test verifying prettier isn't loaded until pretty is called.