Skip to content

Commit 616e6ac

Browse files
authored
fix(render): Render async next 14 (#1009)
1 parent 95643be commit 616e6ac

File tree

2 files changed

+37
-52
lines changed

2 files changed

+37
-52
lines changed

packages/render/src/render-async.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe("renderAsync using renderToStaticMarkup", () => {
77
const actualOutput = await renderAsync(<Template firstName="Jim" />);
88

99
expect(actualOutput).toMatchInlineSnapshot(
10-
'"<!DOCTYPE html PUBLIC \\"-//W3C//DTD XHTML 1.0 Transitional//EN\\" \\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\\"><h1>Welcome, Jim!</h1><img alt=\\"test\\" src=\\"img/test.png\\"/><p>Thanks for trying our product. We&#x27;re thrilled to have you on board!</p>"',
10+
'"<!DOCTYPE html PUBLIC \\"-//W3C//DTD XHTML 1.0 Transitional//EN\\" \\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\\"><h1>Welcome, <!-- -->Jim<!-- -->!</h1><img alt=\\"test\\" src=\\"img/test.png\\"/><p>Thanks for trying our product. We&#x27;re thrilled to have you on board!</p>"',
1111
);
1212
});
1313

@@ -39,7 +39,7 @@ describe("renderAsync using renderToReadableStream", () => {
3939
const actualOutput = await renderAsync(<Template firstName="Jim" />);
4040

4141
expect(actualOutput).toMatchInlineSnapshot(
42-
'"<!DOCTYPE html PUBLIC \\"-//W3C//DTD XHTML 1.0 Transitional//EN\\" \\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\\"><h1>Welcome, Jim!</h1><img alt=\\"test\\" src=\\"img/test.png\\"/><p>Thanks for trying our product. We&#x27;re thrilled to have you on board!</p>"',
42+
'"<!DOCTYPE html PUBLIC \\"-//W3C//DTD XHTML 1.0 Transitional//EN\\" \\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\\"><h1>Welcome, <!-- -->Jim<!-- -->!</h1><img alt=\\"test\\" src=\\"img/test.png\\"/><p>Thanks for trying our product. We&#x27;re thrilled to have you on board!</p>"',
4343
);
4444
});
4545

packages/render/src/render-async.ts

Lines changed: 35 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,26 @@
1-
import { type ReadableStream } from "node:stream/web";
1+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
22
import { convert } from "html-to-text";
33
import pretty from "pretty";
4-
import { type ReactNode } from "react";
5-
import react from "react-dom/server";
6-
7-
const { renderToStaticMarkup } = react;
8-
9-
// Note: only available in platforms that support WebStreams
10-
// https://react.dev/reference/react-dom/server/renderToString#alternatives
11-
const renderToStream =
12-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
13-
react.renderToReadableStream || react.renderToPipeableStream;
14-
15-
export default async function renderToString(children: ReactNode) {
16-
const stream = await renderToStream(children);
17-
18-
const html = await readableStreamToString(
19-
// ReactDOMServerReadableStream behaves like ReadableStream
20-
// in modern edge runtimes but the types are not compatible
21-
stream as unknown as ReadableStream<Uint8Array>,
22-
);
23-
24-
return (
25-
html
26-
// Remove leading doctype becuase we add it manually
27-
.replace(/^<!DOCTYPE html>/, "")
28-
// Remove empty comments to match the output of renderToStaticMarkup
29-
.replace(/<!-- -->/g, "")
30-
);
31-
}
32-
33-
async function readableStreamToString(
34-
readableStream: ReadableStream<Uint8Array>,
35-
) {
36-
let result = "";
37-
38-
const decoder = new TextDecoder();
39-
40-
for await (const chunk of readableStream) {
41-
result += decoder.decode(chunk);
4+
import type { ReactDOMServerReadableStream } from "react-dom/server";
5+
6+
const readStream = async (readableStream: ReactDOMServerReadableStream) => {
7+
const reader = readableStream.getReader();
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
const chunks: any[] = [];
10+
11+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
12+
while (true) {
13+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-await-in-loop
14+
const { value, done } = await reader.read();
15+
if (done) {
16+
break;
17+
}
18+
chunks.push(value);
4219
}
4320

44-
return result;
45-
}
21+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
22+
return chunks.map((chunk) => new TextDecoder("utf-8").decode(chunk)).join("");
23+
};
4624

4725
export const renderAsync = async (
4826
component: React.ReactElement,
@@ -51,24 +29,31 @@ export const renderAsync = async (
5129
plainText?: boolean;
5230
},
5331
) => {
54-
const markup =
55-
typeof renderToStaticMarkup === "undefined"
56-
? await renderToString(component)
57-
: renderToStaticMarkup(component);
32+
const reactDOMServer = (await import("react-dom/server")).default;
33+
const renderToStream =
34+
reactDOMServer.renderToReadableStream ||
35+
reactDOMServer.renderToString ||
36+
reactDOMServer.renderToPipeableStream;
37+
38+
const doctype =
39+
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
40+
41+
const readableStream = await renderToStream(component);
42+
const html =
43+
typeof readableStream === "string"
44+
? readableStream
45+
: await readStream(readableStream);
5846

5947
if (options?.plainText) {
60-
return convert(markup, {
48+
return convert(html, {
6149
selectors: [
6250
{ selector: "img", format: "skip" },
6351
{ selector: "#__react-email-preview", format: "skip" },
6452
],
6553
});
6654
}
6755

68-
const doctype =
69-
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
70-
71-
const document = `${doctype}${markup}`;
56+
const document = `${doctype}${html}`;
7257

7358
if (options?.pretty) {
7459
return pretty(document);

0 commit comments

Comments
 (0)