diff --git a/.changeset/tall-cameras-cry.md b/.changeset/tall-cameras-cry.md new file mode 100644 index 0000000000..a839374a8a --- /dev/null +++ b/.changeset/tall-cameras-cry.md @@ -0,0 +1,5 @@ +--- +"@react-email/render": patch +--- + +Fix null characters in between chunks when using high-density characters diff --git a/packages/react-email/next-env.d.ts b/packages/react-email/next-env.d.ts index 4f11a03dc6..40c3d68096 100644 --- a/packages/react-email/next-env.d.ts +++ b/packages/react-email/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/packages/render/package.json b/packages/render/package.json index 69514876d5..63b9b892aa 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -94,6 +94,8 @@ "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" }, "devDependencies": { + "@types/react": "npm:types-react@19.0.0-rc.1", + "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@edge-runtime/vm": "3.1.8", "@types/html-to-text": "9.0.4", "@types/js-beautify": "1.14.3", diff --git a/packages/render/src/browser/__snapshots__/render-async-web.spec.tsx.snap b/packages/render/src/browser/__snapshots__/render-async-web.spec.tsx.snap new file mode 100644 index 0000000000..873fa93880 --- /dev/null +++ b/packages/render/src/browser/__snapshots__/render-async-web.spec.tsx.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`renderAsync on the browser environment > should handle characters with a higher byte count gracefully 1`] = `"

Test Normal 情報Ⅰコース担当者様

平素よりお世話になっております。 情報Ⅰサポートチームです。 情報Ⅰ本講座につきまして仕様変更のためご連絡させていただきました。

今後ジクタス上の講座につきましては、8回分の授業をひとまとまりとしてパート分けされた状態で公開されてまいります。

伴いまして、画面上の表示に一部変更がありますのでお知らせいたします。 ご登録いただいた各生徒の受講ペースに応じて公開パートが増えてまいります。 具体的な表示イメージは下記ページをご確認ください。

2024年8月末時点で情報Ⅰ本講座を受講していたアカウントにつきましては、 今まで公開していた第1~9回までの講座一覧に加え、パート分けされた講座が追加で公開されてまりいます。 第1~9回の表示はそのまま引き継がれますが、Webドリルの進捗表示はパート分けの講座には適用されません。ご了承くださいませ。 仕様変更に伴い、現在教室長もしくは講師に生徒アカウントログインをお願いしておりますが、今後は生徒自身にてログインをしていただいて問題ございません。

また、生徒が自宅等にてログインし復習に取り組むことも問題ございませんので、教室にてご指示いただければと存じます。 (実際にご指示いただくかは教室判断に委ねさせていただきます。)

受講ペースが変更したり、増コマが発生したりする場合などは、公開ペースを本部にて調整いたしますので、下記より必ずご連絡くださいませ。 また本件に関して不明な点がございましたら、同フォームよりお問い合わせください。 以上、引き続きよろしくお願い申し上げます。 情報Ⅰサポートチーム

"`; diff --git a/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap b/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap new file mode 100644 index 0000000000..2a39925b95 --- /dev/null +++ b/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`render on the browser environment > should handle characters with a higher byte count gracefully 1`] = `"

Test Normal 情報Ⅰコース担当者様

平素よりお世話になっております。 情報Ⅰサポートチームです。 情報Ⅰ本講座につきまして仕様変更のためご連絡させていただきました。

今後ジクタス上の講座につきましては、8回分の授業をひとまとまりとしてパート分けされた状態で公開されてまいります。

伴いまして、画面上の表示に一部変更がありますのでお知らせいたします。 ご登録いただいた各生徒の受講ペースに応じて公開パートが増えてまいります。 具体的な表示イメージは下記ページをご確認ください。

2024年8月末時点で情報Ⅰ本講座を受講していたアカウントにつきましては、 今まで公開していた第1~9回までの講座一覧に加え、パート分けされた講座が追加で公開されてまりいます。 第1~9回の表示はそのまま引き継がれますが、Webドリルの進捗表示はパート分けの講座には適用されません。ご了承くださいませ。 仕様変更に伴い、現在教室長もしくは講師に生徒アカウントログインをお願いしておりますが、今後は生徒自身にてログインをしていただいて問題ございません。

また、生徒が自宅等にてログインし復習に取り組むことも問題ございませんので、教室にてご指示いただければと存じます。 (実際にご指示いただくかは教室判断に委ねさせていただきます。)

受講ペースが変更したり、増コマが発生したりする場合などは、公開ペースを本部にて調整いたしますので、下記より必ずご連絡くださいませ。 また本件に関して不明な点がございましたら、同フォームよりお問い合わせください。 以上、引き続きよろしくお願い申し上げます。 情報Ⅰサポートチーム

"`; diff --git a/packages/render/src/browser/read-stream.ts b/packages/render/src/browser/read-stream.ts new file mode 100644 index 0000000000..fea97fc18a --- /dev/null +++ b/packages/render/src/browser/read-stream.ts @@ -0,0 +1,44 @@ +import { + PipeableStream, + ReactDOMServerReadableStream, +} from "react-dom/server.browser"; + +const decoder = new TextDecoder("utf-8"); + +export const readStream = async ( + stream: PipeableStream | ReactDOMServerReadableStream, +) => { + const chunks: Uint8Array[] = []; + + if ("pipeTo" in stream) { + // means it's a readable stream + const writableStream = new WritableStream({ + write(chunk: Uint8Array) { + chunks.push(chunk); + }, + }); + await stream.pipeTo(writableStream); + } else { + throw new Error( + "For some reason, the Node version of `react-dom/server` has been imported instead of the browser one.", + { + cause: { + stream, + }, + }, + ); + } + + let length = 0; + chunks.forEach((item) => { + length += item.length; + }); + const mergedChunks = new Uint8Array(length); + let offset = 0; + chunks.forEach((item) => { + mergedChunks.set(item, offset); + offset += item.length; + }); + + return decoder.decode(mergedChunks); +}; diff --git a/packages/render/src/browser/render-async-web.spec.tsx b/packages/render/src/browser/render-async-web.spec.tsx index b2f050338f..53aecebf6f 100644 --- a/packages/render/src/browser/render-async-web.spec.tsx +++ b/packages/render/src/browser/render-async-web.spec.tsx @@ -65,6 +65,42 @@ describe("renderAsync on the browser environment", () => { ); }); + // This is a test to ensure we have no regressions for https://github.com/resend/react-email/issues/1667 + it("should handle characters with a higher byte count gracefully", async () => { + const actualOutput = await renderAsync( + <> +

Test Normal 情報Ⅰコース担当者様

+

+ 平素よりお世話になっております。 情報Ⅰサポートチームです。 + 情報Ⅰ本講座につきまして仕様変更のためご連絡させていただきました。{" "} +

+ 今後ジクタス上の講座につきましては、8回分の授業をひとまとまりとしてパート分けされた状態で公開されてまいります。 +

+ 伴いまして、画面上の表示に一部変更がありますのでお知らせいたします。 + ご登録いただいた各生徒の受講ペースに応じて公開パートが増えてまいります。 + 具体的な表示イメージは下記ページをご確認ください。 +

+

+ 2024年8月末時点で情報Ⅰ本講座を受講していたアカウントにつきましては、 + 今まで公開していた第1~9回までの講座一覧に加え、パート分けされた講座が追加で公開されてまりいます。 + 第1~9回の表示はそのまま引き継がれますが、Webドリルの進捗表示はパート分けの講座には適用されません。ご了承くださいませ。 + 仕様変更に伴い、現在教室長もしくは講師に生徒アカウントログインをお願いしておりますが、今後は生徒自身にてログインをしていただいて問題ございません。 +

+

+ また、生徒が自宅等にてログインし復習に取り組むことも問題ございませんので、教室にてご指示いただければと存じます。 + (実際にご指示いただくかは教室判断に委ねさせていただきます。) +

+

+ 受講ペースが変更したり、増コマが発生したりする場合などは、公開ペースを本部にて調整いたしますので、下記より必ずご連絡くださいませ。 + また本件に関して不明な点がございましたら、同フォームよりお問い合わせください。 + 以上、引き続きよろしくお願い申し上げます。 情報Ⅰサポートチーム +

+ , + ); + + expect(actualOutput).toMatchSnapshot(); + }); + it("converts a React component into PlainText", async () => { const actualOutput = await renderAsync(