From 6c820c968c71d0bd4cc045fd7f106711e5fda571 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 28 Oct 2024 01:16:54 -0300 Subject: [PATCH 1/3] create a separate getReactDOMServer that handles importing react-dom/server.edge --- .../src/browser/get-react-dom-server.tsx | 16 ++++++ packages/render/src/browser/render-async.tsx | 11 ++-- packages/render/src/browser/render.tsx | 54 +++---------------- packages/render/src/node/read-stream.ts | 41 -------------- packages/render/src/node/render-async.tsx | 2 +- packages/render/src/node/render.tsx | 2 +- .../src/{browser => shared}/read-stream.ts | 0 7 files changed, 30 insertions(+), 96 deletions(-) create mode 100644 packages/render/src/browser/get-react-dom-server.tsx delete mode 100644 packages/render/src/node/read-stream.ts rename packages/render/src/{browser => shared}/read-stream.ts (100%) diff --git a/packages/render/src/browser/get-react-dom-server.tsx b/packages/render/src/browser/get-react-dom-server.tsx new file mode 100644 index 0000000000..60eacaf4cf --- /dev/null +++ b/packages/render/src/browser/get-react-dom-server.tsx @@ -0,0 +1,16 @@ +let reactDOMServer: typeof import("react-dom/server") | undefined; + +export const getReactDOMServer = async () => { + if (!reactDOMServer) { + try { + reactDOMServer = (await import( + // @ts-expect-error There are no types for this particular export yet + "react-dom/server.edge" + )) as typeof import("react-dom/server"); + } catch (exception) { + reactDOMServer = await import("react-dom/server"); + } + } + + return reactDOMServer; +}; diff --git a/packages/render/src/browser/render-async.tsx b/packages/render/src/browser/render-async.tsx index 9795b5fcd9..33c9c65219 100644 --- a/packages/render/src/browser/render-async.tsx +++ b/packages/render/src/browser/render-async.tsx @@ -3,23 +3,24 @@ import { Suspense } from "react"; import { pretty } from "../shared/utils/pretty"; import { plainTextSelectors } from "../shared/plain-text-selectors"; import type { Options } from "../shared/options"; -import { readStream } from "./read-stream"; +import { readStream } from "../shared/read-stream"; +import { getReactDOMServer } from "./get-react-dom-server"; export const renderAsync = async ( element: React.ReactElement, options?: Options, ) => { const suspendedElement = {element}; - const { default: reactDOMServer } = await import("react-dom/server"); + const ReactDOMServer = await getReactDOMServer(); let html!: string; - if (Object.hasOwn(reactDOMServer, "renderToReadableStream")) { + if (Object.hasOwn(ReactDOMServer, "renderToReadableStream")) { html = await readStream( - await reactDOMServer.renderToReadableStream(suspendedElement), + await ReactDOMServer.renderToReadableStream(suspendedElement), ); } else { await new Promise((resolve, reject) => { - const stream = reactDOMServer.renderToPipeableStream(suspendedElement, { + const stream = ReactDOMServer.renderToPipeableStream(suspendedElement, { async onAllReady() { html = await readStream(stream); resolve(); diff --git a/packages/render/src/browser/render.tsx b/packages/render/src/browser/render.tsx index c05f7c242d..57bb977e59 100644 --- a/packages/render/src/browser/render.tsx +++ b/packages/render/src/browser/render.tsx @@ -1,68 +1,26 @@ import { convert } from "html-to-text"; -import type { - PipeableStream, - ReactDOMServerReadableStream, -} from "react-dom/server"; import { Suspense } from "react"; import { pretty } from "../shared/utils/pretty"; import { plainTextSelectors } from "../shared/plain-text-selectors"; import type { Options } from "../shared/options"; - -const decoder = new TextDecoder("utf-8"); - -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); -}; +import { readStream } from "../shared/read-stream"; +import { getReactDOMServer } from "./get-react-dom-server"; export const render = async ( element: React.ReactElement, options?: Options, ) => { const suspendedElement = {element}; - const { default: reactDOMServer } = await import("react-dom/server"); + const ReactDOMServer = await getReactDOMServer(); let html!: string; - if (Object.hasOwn(reactDOMServer, "renderToReadableStream")) { + if (Object.hasOwn(ReactDOMServer, "renderToReadableStream")) { html = await readStream( - await reactDOMServer.renderToReadableStream(suspendedElement), + await ReactDOMServer.renderToReadableStream(suspendedElement), ); } else { await new Promise((resolve, reject) => { - const stream = reactDOMServer.renderToPipeableStream(suspendedElement, { + const stream = ReactDOMServer.renderToPipeableStream(suspendedElement, { async onAllReady() { html = await readStream(stream); resolve(); diff --git a/packages/render/src/node/read-stream.ts b/packages/render/src/node/read-stream.ts deleted file mode 100644 index 8c24dcfe37..0000000000 --- a/packages/render/src/node/read-stream.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Writable } from "node:stream"; -import { - PipeableStream, - ReactDOMServerReadableStream, -} from "react-dom/server.browser"; - -const decoder = new TextDecoder("utf-8"); - -export const readStream = async ( - stream: PipeableStream | ReactDOMServerReadableStream, -) => { - let result = ""; - - if ("pipeTo" in stream) { - // means it's a readable stream - const writableStream = new WritableStream({ - write(chunk: BufferSource) { - result += decoder.decode(chunk); - }, - }); - await stream.pipeTo(writableStream); - } else { - const writable = new Writable({ - write(chunk: BufferSource, _encoding, callback) { - result += decoder.decode(chunk); - - callback(); - }, - }); - stream.pipe(writable); - - return new Promise((resolve, reject) => { - writable.on("error", reject); - writable.on("close", () => { - resolve(result); - }); - }); - } - - return result; -}; diff --git a/packages/render/src/node/render-async.tsx b/packages/render/src/node/render-async.tsx index a81a6694ae..e2c63e2618 100644 --- a/packages/render/src/node/render-async.tsx +++ b/packages/render/src/node/render-async.tsx @@ -3,7 +3,7 @@ import { Suspense } from "react"; import { pretty } from "../shared/utils/pretty"; import { plainTextSelectors } from "../shared/plain-text-selectors"; import type { Options } from "../shared/options"; -import { readStream } from "./read-stream"; +import { readStream } from "../shared/read-stream"; /** * @deprecated use `render` diff --git a/packages/render/src/node/render.tsx b/packages/render/src/node/render.tsx index 693e4582f1..a72258e7ee 100644 --- a/packages/render/src/node/render.tsx +++ b/packages/render/src/node/render.tsx @@ -3,7 +3,7 @@ import { Suspense } from "react"; import { pretty } from "../shared/utils/pretty"; import { plainTextSelectors } from "../shared/plain-text-selectors"; import type { Options } from "../shared/options"; -import { readStream } from "./read-stream"; +import { readStream } from "../shared/read-stream"; export const render = async ( element: React.ReactElement, diff --git a/packages/render/src/browser/read-stream.ts b/packages/render/src/shared/read-stream.ts similarity index 100% rename from packages/render/src/browser/read-stream.ts rename to packages/render/src/shared/read-stream.ts From a0b76cf032ba5cdb0b92a06c5d4e1837c8ef1e65 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 28 Oct 2024 16:34:37 -0300 Subject: [PATCH 2/3] use a separate export for the edge-light environment --- packages/render/package.json | 10 ++++ .../src/browser/get-react-dom-server.tsx | 16 ------ packages/render/src/browser/render-async.tsx | 11 ++-- packages/render/src/browser/render.tsx | 11 ++-- .../render-async-edge.spec.tsx.snap | 3 ++ .../render-async-node.spec.tsx.snap | 53 ++++++++++++++++++ .../__snapshots__/render-async.spec.tsx.snap | 3 ++ .../__snapshots__/render-edge.spec.tsx.snap | 3 ++ .../__snapshots__/render-node.spec.tsx.snap | 53 ++++++++++++++++++ .../__snapshots__/render.spec.tsx.snap | 3 ++ packages/render/src/edge-light/index.ts | 5 ++ .../render-async.spec.tsx} | 10 ++++ .../render/src/edge-light/render-async.tsx | 54 +++++++++++++++++++ .../render.spec.tsx} | 10 ++++ packages/render/src/edge-light/render.tsx | 51 ++++++++++++++++++ .../src/node/render-async-node.spec.tsx | 2 +- packages/render/src/react-internals.d.ts | 3 ++ .../render/src/shared/read-stream.browser.ts | 44 +++++++++++++++ packages/render/src/shared/read-stream.ts | 26 ++++++--- packages/render/tsup.config.ts | 6 +++ 20 files changed, 341 insertions(+), 36 deletions(-) delete mode 100644 packages/render/src/browser/get-react-dom-server.tsx create mode 100644 packages/render/src/edge-light/__snapshots__/render-async-edge.spec.tsx.snap create mode 100644 packages/render/src/edge-light/__snapshots__/render-async-node.spec.tsx.snap create mode 100644 packages/render/src/edge-light/__snapshots__/render-async.spec.tsx.snap create mode 100644 packages/render/src/edge-light/__snapshots__/render-edge.spec.tsx.snap create mode 100644 packages/render/src/edge-light/__snapshots__/render-node.spec.tsx.snap create mode 100644 packages/render/src/edge-light/__snapshots__/render.spec.tsx.snap create mode 100644 packages/render/src/edge-light/index.ts rename packages/render/src/{node/render-async-edge.spec.tsx => edge-light/render-async.spec.tsx} (93%) create mode 100644 packages/render/src/edge-light/render-async.tsx rename packages/render/src/{node/render-edge.spec.tsx => edge-light/render.spec.tsx} (94%) create mode 100644 packages/render/src/edge-light/render.tsx create mode 100644 packages/render/src/shared/read-stream.browser.ts diff --git a/packages/render/package.json b/packages/render/package.json index b5b5475be6..5ee8775366 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -41,6 +41,16 @@ "default": "./dist/browser/index.js" } }, + "edge-light": { + "import": { + "types": "./dist/edge-light/index.d.mts", + "default": "./dist/edge-light/index.mjs" + }, + "require": { + "types": "./dist/edge-light/index.d.ts", + "default": "./dist/edge-light/index.js" + } + }, "browser": { "import": { "types": "./dist/browser/index.d.mts", diff --git a/packages/render/src/browser/get-react-dom-server.tsx b/packages/render/src/browser/get-react-dom-server.tsx deleted file mode 100644 index 60eacaf4cf..0000000000 --- a/packages/render/src/browser/get-react-dom-server.tsx +++ /dev/null @@ -1,16 +0,0 @@ -let reactDOMServer: typeof import("react-dom/server") | undefined; - -export const getReactDOMServer = async () => { - if (!reactDOMServer) { - try { - reactDOMServer = (await import( - // @ts-expect-error There are no types for this particular export yet - "react-dom/server.edge" - )) as typeof import("react-dom/server"); - } catch (exception) { - reactDOMServer = await import("react-dom/server"); - } - } - - return reactDOMServer; -}; diff --git a/packages/render/src/browser/render-async.tsx b/packages/render/src/browser/render-async.tsx index 33c9c65219..65299dc279 100644 --- a/packages/render/src/browser/render-async.tsx +++ b/packages/render/src/browser/render-async.tsx @@ -3,24 +3,23 @@ import { Suspense } from "react"; import { pretty } from "../shared/utils/pretty"; import { plainTextSelectors } from "../shared/plain-text-selectors"; import type { Options } from "../shared/options"; -import { readStream } from "../shared/read-stream"; -import { getReactDOMServer } from "./get-react-dom-server"; +import { readStream } from "../shared/read-stream.browser"; export const renderAsync = async ( element: React.ReactElement, options?: Options, ) => { const suspendedElement = {element}; - const ReactDOMServer = await getReactDOMServer(); + const reactDOMServer = await import("react-dom/server"); let html!: string; - if (Object.hasOwn(ReactDOMServer, "renderToReadableStream")) { + if (Object.hasOwn(reactDOMServer, "renderToReadableStream")) { html = await readStream( - await ReactDOMServer.renderToReadableStream(suspendedElement), + await reactDOMServer.renderToReadableStream(suspendedElement), ); } else { await new Promise((resolve, reject) => { - const stream = ReactDOMServer.renderToPipeableStream(suspendedElement, { + const stream = reactDOMServer.renderToPipeableStream(suspendedElement, { async onAllReady() { html = await readStream(stream); resolve(); diff --git a/packages/render/src/browser/render.tsx b/packages/render/src/browser/render.tsx index 57bb977e59..4b855b008d 100644 --- a/packages/render/src/browser/render.tsx +++ b/packages/render/src/browser/render.tsx @@ -3,24 +3,23 @@ import { Suspense } from "react"; import { pretty } from "../shared/utils/pretty"; import { plainTextSelectors } from "../shared/plain-text-selectors"; import type { Options } from "../shared/options"; -import { readStream } from "../shared/read-stream"; -import { getReactDOMServer } from "./get-react-dom-server"; +import { readStream } from "../shared/read-stream.browser"; export const render = async ( element: React.ReactElement, options?: Options, ) => { const suspendedElement = {element}; - const ReactDOMServer = await getReactDOMServer(); + const reactDOMServer = await import("react-dom/server"); let html!: string; - if (Object.hasOwn(ReactDOMServer, "renderToReadableStream")) { + if (Object.hasOwn(reactDOMServer, "renderToReadableStream")) { html = await readStream( - await ReactDOMServer.renderToReadableStream(suspendedElement), + await reactDOMServer.renderToReadableStream(suspendedElement), ); } else { await new Promise((resolve, reject) => { - const stream = ReactDOMServer.renderToPipeableStream(suspendedElement, { + const stream = reactDOMServer.renderToPipeableStream(suspendedElement, { async onAllReady() { html = await readStream(stream); resolve(); diff --git a/packages/render/src/edge-light/__snapshots__/render-async-edge.spec.tsx.snap b/packages/render/src/edge-light/__snapshots__/render-async-edge.spec.tsx.snap new file mode 100644 index 0000000000..b022d5e92f --- /dev/null +++ b/packages/render/src/edge-light/__snapshots__/render-async-edge.spec.tsx.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`renderAsync on the edge > should handle characters with a higher byte count gracefully in React 18 1`] = `"

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

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

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

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

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

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

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

"`; diff --git a/packages/render/src/edge-light/__snapshots__/render-async-node.spec.tsx.snap b/packages/render/src/edge-light/__snapshots__/render-async-node.spec.tsx.snap new file mode 100644 index 0000000000..6228bbef16 --- /dev/null +++ b/packages/render/src/edge-light/__snapshots__/render-async-node.spec.tsx.snap @@ -0,0 +1,53 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`renderAsync on node environments > should handle characters with a higher byte count gracefully in React 18 1`] = `"

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

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

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

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

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

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

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

"`; + +exports[`renderAsync on node environments > that it properly waits for Suepsense boundaries to resolve before resolving 1`] = ` +"
+ + + Example Domain + + + + + + + + +
+

Example Domain

+

This domain is for use in illustrative examples in documents. You may use this + domain in literature without prior coordination or asking for permission.

+

More information...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

"`; diff --git a/packages/render/src/edge-light/__snapshots__/render-node.spec.tsx.snap b/packages/render/src/edge-light/__snapshots__/render-node.spec.tsx.snap new file mode 100644 index 0000000000..c97cf084a3 --- /dev/null +++ b/packages/render/src/edge-light/__snapshots__/render-node.spec.tsx.snap @@ -0,0 +1,53 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`render on node environments > should handle characters with a higher byte count gracefully in React 18 1`] = `"

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

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

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

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

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

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

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

"`; + +exports[`render on node environments > that it properly waits for Suepsense boundaries to resolve before resolving 1`] = ` +"
+ + + Example Domain + + + + + + + + +
+

Example Domain

+

This domain is for use in illustrative examples in documents. You may use this + domain in literature without prior coordination or asking for permission.

+

More information...

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

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

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

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

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

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

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

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

"`; diff --git a/packages/render/src/edge-light/index.ts b/packages/render/src/edge-light/index.ts new file mode 100644 index 0000000000..2d3b9b5105 --- /dev/null +++ b/packages/render/src/edge-light/index.ts @@ -0,0 +1,5 @@ +export * from "./render"; +export * from "./render-async"; + +export * from "../shared/options"; +export * from "../shared/plain-text-selectors"; diff --git a/packages/render/src/node/render-async-edge.spec.tsx b/packages/render/src/edge-light/render-async.spec.tsx similarity index 93% rename from packages/render/src/node/render-async-edge.spec.tsx rename to packages/render/src/edge-light/render-async.spec.tsx index 25511d0ed1..33492363fd 100644 --- a/packages/render/src/node/render-async-edge.spec.tsx +++ b/packages/render/src/edge-light/render-async.spec.tsx @@ -12,6 +12,16 @@ type Import = typeof import("react-dom/server") & { }; describe("renderAsync on the edge", () => { + beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-extraneous-class + global.MessageChannel = class { + constructor() { + throw new Error("MessageChannel is not supported"); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + }); + it("converts a React component into HTML with Next 14 error stubs", async () => { vi.mock("react-dom/server", async () => { const ReactDOMServer = await vi.importActual("react-dom/server"); diff --git a/packages/render/src/edge-light/render-async.tsx b/packages/render/src/edge-light/render-async.tsx new file mode 100644 index 0000000000..94ba322adc --- /dev/null +++ b/packages/render/src/edge-light/render-async.tsx @@ -0,0 +1,54 @@ +import { convert } from "html-to-text"; +import { Suspense } from "react"; +import { pretty } from "../shared/utils/pretty"; +import { plainTextSelectors } from "../shared/plain-text-selectors"; +import type { Options } from "../shared/options"; +import { readStream } from "../shared/read-stream"; + +/** + * @deprecated use `render` + */ +export const renderAsync = async ( + element: React.ReactElement, + options?: Options, +) => { + const suspendedElement = {element}; + const { default: reactDOMServer } = await import("react-dom/server.edge"); + + let html!: string; + if (Object.hasOwn(reactDOMServer, "renderToReadableStream")) { + html = await readStream( + await reactDOMServer.renderToReadableStream(suspendedElement), + ); + } else { + await new Promise((resolve, reject) => { + const stream = reactDOMServer.renderToPipeableStream(suspendedElement, { + async onAllReady() { + html = await readStream(stream); + resolve(); + }, + onError(error) { + reject(error as Error); + }, + }); + }); + } + + if (options?.plainText) { + return convert(html, { + selectors: plainTextSelectors, + ...options.htmlToTextOptions, + }); + } + + const doctype = + ''; + + const document = `${doctype}${html.replace(//, "")}`; + + if (options?.pretty) { + return pretty(document); + } + + return document; +}; diff --git a/packages/render/src/node/render-edge.spec.tsx b/packages/render/src/edge-light/render.spec.tsx similarity index 94% rename from packages/render/src/node/render-edge.spec.tsx rename to packages/render/src/edge-light/render.spec.tsx index eedcecd9f8..3e1ed358b0 100644 --- a/packages/render/src/node/render-edge.spec.tsx +++ b/packages/render/src/edge-light/render.spec.tsx @@ -12,6 +12,16 @@ type Import = typeof import("react-dom/server") & { }; describe("render on the edge", () => { + beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-extraneous-class + global.MessageChannel = class { + constructor() { + throw new Error("MessageChannel is not supported"); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + }); + it("converts a React component into HTML with Next 14 error stubs", async () => { vi.mock("react-dom/server", async () => { const ReactDOMServer = await vi.importActual("react-dom/server"); diff --git a/packages/render/src/edge-light/render.tsx b/packages/render/src/edge-light/render.tsx new file mode 100644 index 0000000000..049f4f9d6f --- /dev/null +++ b/packages/render/src/edge-light/render.tsx @@ -0,0 +1,51 @@ +import { convert } from "html-to-text"; +import { Suspense } from "react"; +import { pretty } from "../shared/utils/pretty"; +import { plainTextSelectors } from "../shared/plain-text-selectors"; +import type { Options } from "../shared/options"; +import { readStream } from "../shared/read-stream"; + +export const render = async ( + element: React.ReactElement, + options?: Options, +) => { + const suspendedElement = {element}; + const { default: reactDOMServer } = await import("react-dom/server.edge"); + + let html!: string; + if (Object.hasOwn(reactDOMServer, "renderToReadableStream")) { + html = await readStream( + await reactDOMServer.renderToReadableStream(suspendedElement), + ); + } else { + await new Promise((resolve, reject) => { + const stream = reactDOMServer.renderToPipeableStream(suspendedElement, { + async onAllReady() { + html = await readStream(stream); + resolve(); + }, + onError(error) { + reject(error as Error); + }, + }); + }); + } + + if (options?.plainText) { + return convert(html, { + selectors: plainTextSelectors, + ...options.htmlToTextOptions, + }); + } + + const doctype = + ''; + + const document = `${doctype}${html.replace(//, "")}`; + + if (options?.pretty) { + return pretty(document); + } + + return document; +}; diff --git a/packages/render/src/node/render-async-node.spec.tsx b/packages/render/src/node/render-async-node.spec.tsx index 7a3d0b1c4c..b692ea09a4 100644 --- a/packages/render/src/node/render-async-node.spec.tsx +++ b/packages/render/src/node/render-async-node.spec.tsx @@ -3,7 +3,7 @@ */ import usePromise from "react-promise-suspense"; -import { Suspense } from "react"; +import React, { Suspense } from "react"; import { Template } from "../shared/utils/template"; import { Preview } from "../shared/utils/preview"; import { renderAsync } from "./render-async"; diff --git a/packages/render/src/react-internals.d.ts b/packages/render/src/react-internals.d.ts index d15d3ded51..0f0855a206 100644 --- a/packages/render/src/react-internals.d.ts +++ b/packages/render/src/react-internals.d.ts @@ -1,3 +1,6 @@ declare module "react-dom/server.browser" { export * from "react-dom/server"; } +declare module "react-dom/server.edge" { + export * from "react-dom/server"; +} diff --git a/packages/render/src/shared/read-stream.browser.ts b/packages/render/src/shared/read-stream.browser.ts new file mode 100644 index 0000000000..fea97fc18a --- /dev/null +++ b/packages/render/src/shared/read-stream.browser.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/shared/read-stream.ts b/packages/render/src/shared/read-stream.ts index fea97fc18a..d6cb593add 100644 --- a/packages/render/src/shared/read-stream.ts +++ b/packages/render/src/shared/read-stream.ts @@ -1,3 +1,4 @@ +import { Writable } from "node:stream"; import { PipeableStream, ReactDOMServerReadableStream, @@ -5,6 +6,15 @@ import { const decoder = new TextDecoder("utf-8"); +const promisify = (writable: Writable) => { + return new Promise((resolve, reject) => { + writable.on("error", reject); + writable.on("close", () => { + resolve(); + }); + }); +}; + export const readStream = async ( stream: PipeableStream | ReactDOMServerReadableStream, ) => { @@ -19,14 +29,16 @@ export const readStream = async ( }); 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, - }, + const writable = new Writable({ + write(chunk: Uint8Array, _encoding, callback) { + chunks.push(chunk); + + callback(); }, - ); + }); + stream.pipe(writable); + + await promisify(writable); } let length = 0; diff --git a/packages/render/tsup.config.ts b/packages/render/tsup.config.ts index 7d973ea7bc..276b2285de 100644 --- a/packages/render/tsup.config.ts +++ b/packages/render/tsup.config.ts @@ -7,6 +7,12 @@ export default defineConfig([ outDir: "./dist/node", format: ["cjs", "esm"], }, + { + dts: true, + entry: ["./src/edge-light/index.ts"], + outDir: "./dist/edge-light", + format: ["cjs", "esm"], + }, { dts: true, entry: ["./src/browser/index.ts"], From 87e2a7348acacdf62b7c62d5329c219fa43b859e Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 28 Oct 2024 16:58:43 -0300 Subject: [PATCH 3/3] fix wrong readStream type being used for the edge --- packages/render/src/edge-light/render-async.tsx | 2 +- packages/render/src/edge-light/render.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/render/src/edge-light/render-async.tsx b/packages/render/src/edge-light/render-async.tsx index 94ba322adc..60ba50eef5 100644 --- a/packages/render/src/edge-light/render-async.tsx +++ b/packages/render/src/edge-light/render-async.tsx @@ -3,7 +3,7 @@ import { Suspense } from "react"; import { pretty } from "../shared/utils/pretty"; import { plainTextSelectors } from "../shared/plain-text-selectors"; import type { Options } from "../shared/options"; -import { readStream } from "../shared/read-stream"; +import { readStream } from "../shared/read-stream.browser"; /** * @deprecated use `render` diff --git a/packages/render/src/edge-light/render.tsx b/packages/render/src/edge-light/render.tsx index 049f4f9d6f..c2edc48979 100644 --- a/packages/render/src/edge-light/render.tsx +++ b/packages/render/src/edge-light/render.tsx @@ -3,7 +3,7 @@ import { Suspense } from "react"; import { pretty } from "../shared/utils/pretty"; import { plainTextSelectors } from "../shared/plain-text-selectors"; import type { Options } from "../shared/options"; -import { readStream } from "../shared/read-stream"; +import { readStream } from "../shared/read-stream.browser"; export const render = async ( element: React.ReactElement,