diff --git a/packages/render/package.json b/packages/render/package.json index ed821c9f6b..55eceef8cf 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -11,14 +11,14 @@ ], "exports": { ".": { - "node": { + "workerd": { "import": { - "types": "./dist/node/index.d.mts", - "default": "./dist/node/index.mjs" + "types": "./dist/edge/index.d.mts", + "default": "./dist/edge/index.mjs" }, "require": { - "types": "./dist/node/index.d.ts", - "default": "./dist/node/index.js" + "types": "./dist/edge/index.d.ts", + "default": "./dist/edge/index.js" } }, "deno": { @@ -41,6 +41,26 @@ "default": "./dist/browser/index.js" } }, + "edge-light": { + "import": { + "types": "./dist/edge/index.d.mts", + "default": "./dist/edge/index.mjs" + }, + "require": { + "types": "./dist/edge/index.d.ts", + "default": "./dist/edge/index.js" + } + }, + "node": { + "import": { + "types": "./dist/node/index.d.mts", + "default": "./dist/node/index.mjs" + }, + "require": { + "types": "./dist/node/index.d.ts", + "default": "./dist/node/index.js" + } + }, "browser": { "import": { "types": "./dist/browser/index.d.mts", diff --git a/packages/render/src/browser/render.tsx b/packages/render/src/browser/render.tsx index 9beb4cb3df..d0e7b0ed3d 100644 --- a/packages/render/src/browser/render.tsx +++ b/packages/render/src/browser/render.tsx @@ -1,52 +1,9 @@ import { convert } from 'html-to-text'; import { Suspense } from 'react'; -import type { - PipeableStream, - ReactDOMServerReadableStream, -} from 'react-dom/server'; import { pretty } from '../node'; import type { Options } from '../shared/options'; import { plainTextSelectors } from '../shared/plain-text-selectors'; - -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.browser'; export const render = async (node: React.ReactNode, options?: Options) => { const suspendedElement = {node}; diff --git a/packages/render/src/edge/__snapshots__/render.spec.tsx.snap b/packages/render/src/edge/__snapshots__/render.spec.tsx.snap new file mode 100644 index 0000000000..baa09f38a6 --- /dev/null +++ b/packages/render/src/edge/__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 1`] = `"

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

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

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

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

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

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

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

"`; diff --git a/packages/render/src/edge/import-react-dom.tsx b/packages/render/src/edge/import-react-dom.tsx new file mode 100644 index 0000000000..e6a84410d2 --- /dev/null +++ b/packages/render/src/edge/import-react-dom.tsx @@ -0,0 +1,7 @@ +export const importReactDOM = async () => { + try { + return await import('react-dom/server.edge'); + } catch (_exception) { + return await import('react-dom/server'); + } +}; diff --git a/packages/render/src/edge/index.ts b/packages/render/src/edge/index.ts new file mode 100644 index 0000000000..3d930a1cf7 --- /dev/null +++ b/packages/render/src/edge/index.ts @@ -0,0 +1,14 @@ +import type { Options } from '../shared/options'; +import { render } from './render'; + +/** + * @deprecated use {@link render} + */ +export const renderAsync = (element: React.ReactElement, options?: Options) => { + return render(element, options); +}; + +export * from '../shared/options'; +export * from '../shared/plain-text-selectors'; +export * from '../shared/utils/pretty'; +export * from './render'; diff --git a/packages/render/src/edge/render.spec.tsx b/packages/render/src/edge/render.spec.tsx new file mode 100644 index 0000000000..f1507b2d98 --- /dev/null +++ b/packages/render/src/edge/render.spec.tsx @@ -0,0 +1,126 @@ +/** + * @vitest-environment jsdom + */ + +import { Preview } from '../shared/utils/preview'; +import { Template } from '../shared/utils/template'; +import { render } from './render'; + +type Import = typeof import('react-dom/server') & { + default: typeof import('react-dom/server'); +}; + +describe('render on the edge', () => { + beforeAll(() => { + global.MessageChannel = class { + constructor() { + throw new Error('MessageChannel is not supported'); + } + } as any; + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it('converts a React component into HTML with Next 14 error stubs', async () => { + vi.mock('react-dom/server', async (_importOriginal) => { + const ReactDOMServerBrowser = await vi.importActual( + 'react-dom/server.browser', + ); + const ERROR_MESSAGE = + 'Internal Error: do not use legacy react-dom/server APIs. If you encountered this error, please open an issue on the Next.js repo.'; + + return { + ...ReactDOMServerBrowser, + default: { + ...ReactDOMServerBrowser, + renderToString() { + throw new Error(ERROR_MESSAGE); + }, + renderToStaticMarkup() { + throw new Error(ERROR_MESSAGE); + }, + }, + renderToString() { + throw new Error(ERROR_MESSAGE); + }, + renderToStaticMarkup() { + throw new Error(ERROR_MESSAGE); + }, + }; + }); + + const actualOutput = await render(