Skip to content

Commit f356e81

Browse files
fix(render): lazy-load prettier so it's only loaded when pretty is used (#3790)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent cf6ab43 commit f356e81

3 files changed

Lines changed: 78 additions & 30 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-email/render": patch
3+
---
4+
5+
`render` now only loads `prettier` when `pretty: true` is used, so rendering without it no longer fails with "Failed to load external module prettier" in Next.js dev with Turbopack on Bun

‎packages/render/src/shared/utils/pretty.spec.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,25 @@ describe('pretty', () => {
184184
"
185185
`);
186186
});
187+
188+
it('only loads prettier once pretty is called', async () => {
189+
vi.resetModules();
190+
const loadStandalone = vi.fn();
191+
vi.doMock('prettier/standalone', async (importOriginal) => {
192+
loadStandalone();
193+
return importOriginal();
194+
});
195+
196+
try {
197+
const { pretty: lazyPretty } = await import('./pretty');
198+
expect(loadStandalone).not.toHaveBeenCalled();
199+
200+
expect(await lazyPretty('<div><p>Hello</p></div>')).toBe(
201+
'<div><p>Hello</p></div>\n',
202+
);
203+
expect(loadStandalone).toHaveBeenCalledOnce();
204+
} finally {
205+
vi.doUnmock('prettier/standalone');
206+
}
207+
});
187208
});

‎packages/render/src/shared/utils/pretty.ts‎

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { Options, Plugin } from 'prettier';
22
import type { builders } from 'prettier/doc';
3-
import * as html from 'prettier/plugins/html';
4-
import { format } from 'prettier/standalone';
53

64
interface HtmlNode {
75
type?: 'element' | 'text' | 'ieConditionalComment';
@@ -86,46 +84,70 @@ function recursivelyMapDoc(
8684
return callback(doc);
8785
}
8886

89-
const modifiedHtml = { ...html } as Plugin;
90-
if (modifiedHtml.printers) {
91-
const previousPrint = modifiedHtml.printers.html.print;
92-
modifiedHtml.printers.html.print = (path, options, print, args) => {
93-
const node = getHtmlNode(
94-
path as Parameters<NonNullable<Plugin['printers']>['html']['print']>[0],
95-
);
96-
97-
const rawPrintingResult = previousPrint(path, options, print, args);
98-
99-
if (
100-
node?.type === 'ieConditionalComment' ||
101-
node?.kind === 'ieConditionalComment'
102-
) {
103-
const printingResult = recursivelyMapDoc(rawPrintingResult, (doc) => {
104-
if (typeof doc === 'object' && doc.type === 'line') {
105-
return doc.soft ? '' : ' ';
106-
}
107-
108-
return doc;
109-
});
110-
111-
return printingResult;
112-
}
87+
function createModifiedHtmlPlugin(html: Plugin): Plugin {
88+
const modifiedHtml = { ...html };
89+
if (modifiedHtml.printers) {
90+
const previousPrint = modifiedHtml.printers.html.print;
91+
modifiedHtml.printers.html.print = (path, options, print, args) => {
92+
const node = getHtmlNode(
93+
path as Parameters<NonNullable<Plugin['printers']>['html']['print']>[0],
94+
);
95+
96+
const rawPrintingResult = previousPrint(path, options, print, args);
97+
98+
if (
99+
node?.type === 'ieConditionalComment' ||
100+
node?.kind === 'ieConditionalComment'
101+
) {
102+
const printingResult = recursivelyMapDoc(rawPrintingResult, (doc) => {
103+
if (typeof doc === 'object' && doc.type === 'line') {
104+
return doc.soft ? '' : ' ';
105+
}
106+
107+
return doc;
108+
});
109+
110+
return printingResult;
111+
}
113112

114-
return rawPrintingResult;
115-
};
113+
return rawPrintingResult;
114+
};
115+
}
116+
117+
return modifiedHtml;
116118
}
117119

120+
const importPrettier = async () => {
121+
const [{ format }, html] = await Promise.all([
122+
import('prettier/standalone'),
123+
import('prettier/plugins/html'),
124+
]);
125+
126+
return { format, html: createModifiedHtmlPlugin(html as Plugin) };
127+
};
128+
129+
/**
130+
* Prettier is only loaded the first time `pretty` is called, so importing
131+
* `render` doesn't load it when `pretty` is never used. This also keeps
132+
* bundlers that externalize `prettier` (e.g. Next.js with Turbopack) from
133+
* trying to resolve it at runtime unless it's actually needed.
134+
*/
135+
let prettierPromise: ReturnType<typeof importPrettier> | undefined;
136+
118137
const defaults: Options = {
119138
endOfLine: 'lf',
120139
tabWidth: 2,
121-
plugins: [modifiedHtml],
122140
bracketSameLine: true,
123141
parser: 'html',
124142
};
125143

126-
export const pretty = (str: string, options: Options = {}) => {
144+
export const pretty = async (str: string, options: Options = {}) => {
145+
prettierPromise ??= importPrettier();
146+
const { format, html } = await prettierPromise;
147+
127148
return format(str.replaceAll('\0', ''), {
128149
...defaults,
150+
plugins: [html],
129151
...options,
130152
});
131153
};

0 commit comments

Comments
 (0)