Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(render): Render async next 14 #1009

Merged
merged 3 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/render/src/render-async.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe("renderAsync using renderToStaticMarkup", () => {
const actualOutput = await renderAsync(<Template firstName="Jim" />);

expect(actualOutput).toMatchInlineSnapshot(
'"<!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>"',
'"<!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>"',
);
});

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

expect(actualOutput).toMatchInlineSnapshot(
'"<!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>"',
'"<!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>"',
);
});

Expand Down
85 changes: 35 additions & 50 deletions packages/render/src/render-async.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,26 @@
import { type ReadableStream } from "node:stream/web";
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import { convert } from "html-to-text";
import pretty from "pretty";
import { type ReactNode } from "react";
import react from "react-dom/server";

const { renderToStaticMarkup } = react;

// Note: only available in platforms that support WebStreams
// https://react.dev/reference/react-dom/server/renderToString#alternatives
const renderToStream =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
react.renderToReadableStream || react.renderToPipeableStream;

export default async function renderToString(children: ReactNode) {
const stream = await renderToStream(children);

const html = await readableStreamToString(
// ReactDOMServerReadableStream behaves like ReadableStream
// in modern edge runtimes but the types are not compatible
stream as unknown as ReadableStream<Uint8Array>,
);

return (
html
// Remove leading doctype becuase we add it manually
.replace(/^<!DOCTYPE html>/, "")
// Remove empty comments to match the output of renderToStaticMarkup
.replace(/<!-- -->/g, "")
);
}

async function readableStreamToString(
readableStream: ReadableStream<Uint8Array>,
) {
let result = "";

const decoder = new TextDecoder();

for await (const chunk of readableStream) {
result += decoder.decode(chunk);
import type { ReactDOMServerReadableStream } from "react-dom/server";

const readStream = async (readableStream: ReactDOMServerReadableStream) => {
const reader = readableStream.getReader();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const chunks: any[] = [];

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
while (true) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-await-in-loop
const { value, done } = await reader.read();
if (done) {
break;
}
chunks.push(value);
}

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

export const renderAsync = async (
component: React.ReactElement,
Expand All @@ -51,24 +29,31 @@ export const renderAsync = async (
plainText?: boolean;
},
) => {
const markup =
typeof renderToStaticMarkup === "undefined"
? await renderToString(component)
: renderToStaticMarkup(component);
const reactDOMServer = (await import("react-dom/server")).default;
const renderToStream =
reactDOMServer.renderToReadableStream ||
reactDOMServer.renderToString ||
reactDOMServer.renderToPipeableStream;

const doctype =
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';

const readableStream = await renderToStream(component);
const html =
typeof readableStream === "string"
? readableStream
: await readStream(readableStream);

if (options?.plainText) {
return convert(markup, {
return convert(html, {
selectors: [
{ selector: "img", format: "skip" },
{ selector: "#__react-email-preview", format: "skip" },
],
});
}

const doctype =
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';

const document = `${doctype}${markup}`;
const document = `${doctype}${html}`;

if (options?.pretty) {
return pretty(document);
Expand Down
Loading