Skip to content

Commit 3699e26

Browse files
authored
fix(render): Issue with ESM due to dyanmic require (#1491)
1 parent 4b9ccd1 commit 3699e26

20 files changed

+223
-46
lines changed

packages/render/package.json

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,74 @@
33
"version": "0.0.15",
44
"description": "Transform React components into HTML email templates",
55
"sideEffects": false,
6-
"main": "./dist/index.js",
7-
"module": "./dist/index.mjs",
8-
"types": "./dist/index.d.ts",
6+
"main": "./dist/browser/index.js",
7+
"module": "./dist/browser/index.mjs",
8+
"types": "./dist/browser/index.d.ts",
99
"files": [
1010
"dist/**"
1111
],
1212
"exports": {
1313
".": {
14-
"import": {
15-
"types": "./dist/index.d.mts",
16-
"default": "./dist/index.mjs"
14+
"node": {
15+
"import": {
16+
"types": "./dist/node/index.d.mts",
17+
"default": "./dist/node/index.mjs"
18+
},
19+
"require": {
20+
"types": "./dist/node/index.d.ts",
21+
"default": "./dist/node/index.js"
22+
}
1723
},
18-
"require": {
19-
"types": "./dist/index.d.ts",
20-
"default": "./dist/index.js"
24+
"deno": {
25+
"import": {
26+
"types": "./dist/browser/index.d.mts",
27+
"default": "./dist/browser/index.mjs"
28+
},
29+
"require": {
30+
"types": "./dist/browser/index.d.ts",
31+
"default": "./dist/browser/index.js"
32+
}
33+
},
34+
"worker": {
35+
"import": {
36+
"types": "./dist/browser/index.d.mts",
37+
"default": "./dist/browser/index.mjs"
38+
},
39+
"require": {
40+
"types": "./dist/browser/index.d.ts",
41+
"default": "./dist/browser/index.js"
42+
}
43+
},
44+
"browser": {
45+
"import": {
46+
"types": "./dist/browser/index.d.mts",
47+
"default": "./dist/browser/index.mjs"
48+
},
49+
"require": {
50+
"types": "./dist/browser/index.d.ts",
51+
"default": "./dist/browser/index.js"
52+
}
53+
},
54+
"default": {
55+
"import": {
56+
"types": "./dist/node/index.d.mts",
57+
"default": "./dist/node/index.mjs"
58+
},
59+
"require": {
60+
"types": "./dist/node/index.d.ts",
61+
"default": "./dist/node/index.js"
62+
}
2163
}
2264
}
2365
},
2466
"license": "MIT",
2567
"scripts": {
26-
"build": "tsup src/index.ts --format esm,cjs --dts --external react",
68+
"build": "tsup-node",
2769
"clean": "rm -rf dist",
28-
"dev": "tsup src/index.ts --format esm,cjs --dts --external react --watch",
70+
"dev": "tsup-node --watch",
2971
"lint": "eslint .",
30-
"test:watch": "vitest",
31-
"test": "vitest run"
72+
"test": "vitest run",
73+
"test:watch": "vitest"
3274
},
3375
"repository": {
3476
"type": "git",
@@ -57,6 +99,7 @@
5799
"eslint-config-custom": "workspace:*",
58100
"jsdom": "23.0.1",
59101
"tsconfig": "workspace:*",
102+
"tsup": "7.2.0",
60103
"typescript": "5.1.6",
61104
"vitest": "1.1.2"
62105
},
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from "../shared/render";
2+
export * from "./render-async";
3+
4+
export * from "../shared/options";
5+
export * from "../shared/plain-text-selectors";

packages/render/src/render-async-web.spec.tsx renamed to packages/render/src/browser/render-async-web.spec.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
* @vitest-environment jsdom
33
*/
44

5-
import { Template } from "./utils/template";
6-
import { Preview } from "./utils/preview";
5+
import { Template } from "../shared/utils/template";
6+
import { Preview } from "../shared/utils/preview";
77
import { renderAsync } from "./render-async";
88

9+
type Import = typeof import("react-dom/server") & {
10+
default: typeof import("react-dom/server");
11+
};
12+
913
describe("renderAsync on the browser environment", () => {
1014
beforeEach(() => {
1115
vi.mock(
@@ -20,14 +24,23 @@ describe("renderAsync on the browser environment", () => {
2024

2125
it("converts a React component into HTML with Next 14 error stubs", async () => {
2226
vi.mock("react-dom/server", async (_importOriginal) => {
23-
const ReactDOMServerBrowser = await vi.importActual<
24-
typeof import("react-dom/server.browser")
25-
>("react-dom/server.browser");
27+
const ReactDOMServerBrowser = await vi.importActual<Import>(
28+
"react-dom/server.browser",
29+
);
2630
const ERROR_MESSAGE =
2731
"Internal Error: do not use legacy react-dom/server APIs. If you encountered this error, please open an issue on the Next.js repo.";
2832

2933
return {
3034
...ReactDOMServerBrowser,
35+
default: {
36+
...ReactDOMServerBrowser,
37+
renderToString() {
38+
throw new Error(ERROR_MESSAGE);
39+
},
40+
renderToStaticMarkup() {
41+
throw new Error(ERROR_MESSAGE);
42+
},
43+
},
3144
renderToString() {
3245
throw new Error(ERROR_MESSAGE);
3346
},
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { convert } from "html-to-text";
2+
import type {
3+
PipeableStream,
4+
ReactDOMServerReadableStream,
5+
} from "react-dom/server";
6+
import { pretty } from "../shared/utils/pretty";
7+
import { plainTextSelectors } from "../shared/plain-text-selectors";
8+
import type { Options } from "../shared/options";
9+
10+
const decoder = new TextDecoder("utf-8");
11+
12+
const readStream = async (
13+
stream: PipeableStream | ReactDOMServerReadableStream,
14+
) => {
15+
let result = "";
16+
17+
if ("pipeTo" in stream) {
18+
// means it's a readable stream
19+
const writableStream = new WritableStream({
20+
write(chunk: BufferSource) {
21+
result += decoder.decode(chunk);
22+
},
23+
});
24+
await stream.pipeTo(writableStream);
25+
} else {
26+
throw new Error(
27+
"For some reason, the Node version of `react-dom/server` has been imported instead of the browser one.",
28+
{
29+
cause: {
30+
stream,
31+
},
32+
},
33+
);
34+
}
35+
36+
return result;
37+
};
38+
39+
export const renderAsync = async (
40+
component: React.ReactElement,
41+
options?: Options,
42+
) => {
43+
const { default: reactDOMServer } = await import("react-dom/server");
44+
45+
let html!: string;
46+
if (Object.hasOwn(reactDOMServer, "renderToReadableStream")) {
47+
html = await readStream(
48+
await reactDOMServer.renderToReadableStream(component),
49+
);
50+
} else {
51+
await new Promise<void>((resolve, reject) => {
52+
const stream = reactDOMServer.renderToPipeableStream(component, {
53+
async onAllReady() {
54+
html = await readStream(stream);
55+
resolve();
56+
},
57+
onError(error) {
58+
reject(error as Error);
59+
},
60+
});
61+
});
62+
}
63+
64+
if (options?.plainText) {
65+
return convert(html, {
66+
selectors: plainTextSelectors,
67+
...options.htmlToTextOptions,
68+
});
69+
}
70+
71+
const doctype =
72+
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
73+
74+
const document = `${doctype}${html.replace(/<!DOCTYPE.*?>/, "")}`;
75+
76+
if (options?.pretty) {
77+
return pretty(document);
78+
}
79+
80+
return document;
81+
};

packages/render/src/index.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/render/src/node/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from "../shared/render";
2+
export * from "./render-async";
3+
4+
export * from "../shared/options";
5+
export * from "../shared/plain-text-selectors";

packages/render/src/render-async-edge.spec.tsx renamed to packages/render/src/node/render-async-edge.spec.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,33 @@
22
* @vitest-environment edge-runtime
33
*/
44

5-
import { Template } from "./utils/template";
6-
import { Preview } from "./utils/preview";
5+
import React from "react";
6+
import { Template } from "../shared/utils/template";
7+
import { Preview } from "../shared/utils/preview";
78
import { renderAsync } from "./render-async";
89

10+
type Import = typeof import("react-dom/server") & {
11+
default: typeof import("react-dom/server");
12+
};
13+
914
describe("renderAsync on the edge", () => {
1015
it("converts a React component into HTML with Next 14 error stubs", async () => {
1116
vi.mock("react-dom/server", async () => {
12-
const ReactDOMServer =
13-
await vi.importActual<typeof import("react-dom/server")>(
14-
"react-dom/server",
15-
);
17+
const ReactDOMServer = await vi.importActual<Import>("react-dom/server");
1618
const ERROR_MESSAGE =
1719
"Internal Error: do not use legacy react-dom/server APIs. If you encountered this error, please open an issue on the Next.js repo.";
1820

1921
return {
2022
...ReactDOMServer,
23+
default: {
24+
...ReactDOMServer.default,
25+
renderToString() {
26+
throw new Error(ERROR_MESSAGE);
27+
},
28+
renderToStaticMarkup() {
29+
throw new Error(ERROR_MESSAGE);
30+
},
31+
},
2132
renderToString() {
2233
throw new Error(ERROR_MESSAGE);
2334
},

packages/render/src/render-async-node.spec.tsx renamed to packages/render/src/node/render-async-node.spec.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,32 @@
44

55
import usePromise from "react-promise-suspense";
66
import { Suspense } from "react";
7-
import { Template } from "./utils/template";
8-
import { Preview } from "./utils/preview";
7+
import { Template } from "../shared/utils/template";
8+
import { Preview } from "../shared/utils/preview";
99
import { renderAsync } from "./render-async";
1010

11+
type Import = typeof import("react-dom/server") & {
12+
default: typeof import("react-dom/server");
13+
};
14+
1115
describe("renderAsync on node environments", () => {
1216
it("converts a React component into HTML with Next 14 error stubs", async () => {
1317
vi.mock("react-dom/server", async () => {
14-
const ReactDOMServer =
15-
await vi.importActual<typeof import("react-dom/server")>(
16-
"react-dom/server",
17-
);
18+
const ReactDOMServer = await vi.importActual<Import>("react-dom/server");
1819
const ERROR_MESSAGE =
1920
"Internal Error: do not use legacy react-dom/server APIs. If you encountered this error, please open an issue on the Next.js repo.";
2021

2122
return {
2223
...ReactDOMServer,
24+
default: {
25+
...ReactDOMServer.default,
26+
renderToString() {
27+
throw new Error(ERROR_MESSAGE);
28+
},
29+
renderToStaticMarkup() {
30+
throw new Error(ERROR_MESSAGE);
31+
},
32+
},
2333
renderToString() {
2434
throw new Error(ERROR_MESSAGE);
2535
},

packages/render/src/render-async.ts renamed to packages/render/src/node/render-async.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { Writable } from "node:stream";
12
import { convert } from "html-to-text";
23
import type {
34
PipeableStream,
45
ReactDOMServerReadableStream,
56
} from "react-dom/server";
6-
import { pretty } from "./utils/pretty";
7-
import { plainTextSelectors } from "./plain-text-selectors";
8-
import type { Options } from "./options";
7+
import { pretty } from "../shared/utils/pretty";
8+
import { plainTextSelectors } from "../shared/plain-text-selectors";
9+
import type { Options } from "../shared/options";
910

1011
const decoder = new TextDecoder("utf-8");
1112

@@ -23,12 +24,6 @@ const readStream = async (
2324
});
2425
await stream.pipeTo(writableStream);
2526
} else {
26-
// Using an `await import` here proved to cause issues when running
27-
// inside of Node's VM after `esbuild` would have this compiled to CJS.
28-
//
29-
// See https://github.com/resend/react-email/blob/c56cb71ab61a718ee932048a08b65185daeeafa5/packages/react-email/src/utils/get-email-component.ts
30-
// eslint-disable-next-line @typescript-eslint/no-var-requires
31-
const { Writable } = require("node:stream") as typeof import("node:stream");
3227
const writable = new Writable({
3328
write(chunk: BufferSource, _encoding, callback) {
3429
result += decoder.decode(chunk);
@@ -53,7 +48,7 @@ export const renderAsync = async (
5348
component: React.ReactElement,
5449
options?: Options,
5550
) => {
56-
const reactDOMServer = await import("react-dom/server");
51+
const { default: reactDOMServer } = await import("react-dom/server");
5752

5853
let html!: string;
5954
if (Object.hasOwn(reactDOMServer, "renderToReadableStream")) {

0 commit comments

Comments
 (0)