diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 014b6c104a12..2dd64eb1b402 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -304,17 +304,15 @@ const rscCache = new Map() function createRSCHook() { return ( - writable: WritableStream, + writable: WritableStream, id: string, - req: ReadableStream, + req: ReadableStream, bootstrap: boolean ) => { let entry = rscCache.get(id) if (!entry) { const [renderStream, forwardStream] = readableStreamTee(req) - entry = createFromReadableStream( - pipeThrough(renderStream, createTextEncoderStream()) - ) + entry = createFromReadableStream(renderStream) rscCache.set(id, entry) let bootstrapped = false @@ -325,10 +323,11 @@ function createRSCHook() { if (bootstrap && !bootstrapped) { bootstrapped = true writer.write( - `` + encodeText( + `` + ) ) } if (done) { @@ -336,11 +335,11 @@ function createRSCHook() { writer.close() } else { writer.write( - `` + encodeText( + `` + ) ) process() } @@ -365,7 +364,7 @@ function createServerComponentRenderer( runtime, }: { cachePrefix: string - transformStream: TransformStream + transformStream: TransformStream serverComponentManifest: NonNullable runtime: 'nodejs' | 'edge' } @@ -381,12 +380,9 @@ function createServerComponentRenderer( const writable = transformStream.writable const ServerComponentWrapper = (props: any) => { const id = (React as any).useId() - const reqStream: ReadableStream = pipeThrough( - renderToReadableStream( - renderFlight(App, OriginalComponent, props), - serverComponentManifest - ), - createTextDecoderStream() + const reqStream: ReadableStream = renderToReadableStream( + renderFlight(App, OriginalComponent, props), + serverComponentManifest ) const response = useRSCResponse( @@ -482,8 +478,8 @@ export async function renderToHTML( let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) = renderOpts.Component let serverComponentsInlinedTransformStream: TransformStream< - string, - string + Uint8Array, + Uint8Array > | null = null if (isServerComponent) { @@ -1181,21 +1177,16 @@ export async function renderToHTML( if (isResSent(res) && !isSSG) return null if (renderServerComponentData) { - const stream: ReadableStream = pipeThrough( - renderToReadableStream( - renderFlight(App, OriginalComponent, { - ...props.pageProps, - ...serverComponentProps, - }), - serverComponentManifest - ), - createTextDecoderStream() + const stream: ReadableStream = renderToReadableStream( + renderFlight(App, OriginalComponent, { + ...props.pageProps, + ...serverComponentProps, + }), + serverComponentManifest ) + return new RenderResult( - pipeThrough( - pipeThrough(stream, createBufferedTransformStream()), - createTextEncoderStream() - ) + pipeThrough(stream, createBufferedTransformStream()) ) } @@ -1360,7 +1351,8 @@ export async function renderToHTML( generateStaticHTML: true, }) - return await streamToString(flushEffectStream) + const flushed = await streamToString(flushEffectStream) + return flushed } return await renderToStream({ @@ -1607,9 +1599,7 @@ export async function renderToHTML( return new RenderResult(html) } - return new RenderResult( - pipeThrough(chainStreams(streams), createTextEncoderStream()) - ) + return new RenderResult(chainStreams(streams)) } function errorToJSON(err: Error) { @@ -1707,27 +1697,10 @@ function createTransformStream({ } } -function createTextDecoderStream(): TransformStream { - const decoder = new TextDecoder() - return createTransformStream({ - transform(chunk, controller) { - controller.enqueue( - typeof chunk === 'string' ? chunk : decoder.decode(chunk) - ) - }, - }) -} - -function createTextEncoderStream(): TransformStream { - const encoder = new TextEncoder() - return createTransformStream({ - transform(chunk, controller) { - controller.enqueue(encoder.encode(chunk)) - }, - }) -} - -function createBufferedTransformStream(): TransformStream { +function createBufferedTransformStream(): TransformStream< + Uint8Array, + Uint8Array +> { let bufferedString = '' let pendingFlush: Promise | null = null @@ -1735,7 +1708,7 @@ function createBufferedTransformStream(): TransformStream { if (!pendingFlush) { pendingFlush = new Promise((resolve) => { setTimeout(() => { - controller.enqueue(bufferedString) + controller.enqueue(encodeText(bufferedString)) bufferedString = '' pendingFlush = null resolve() @@ -1747,7 +1720,7 @@ function createBufferedTransformStream(): TransformStream { return createTransformStream({ transform(chunk, controller) { - bufferedString += chunk + bufferedString += decodeText(chunk) flushBuffer(controller) }, @@ -1761,11 +1734,11 @@ function createBufferedTransformStream(): TransformStream { function createFlushEffectStream( handleFlushEffect: () => Promise -): TransformStream { +): TransformStream { return createTransformStream({ async transform(chunk, controller) { const extraChunk = await handleFlushEffect() - controller.enqueue(extraChunk + chunk) + controller.enqueue(encodeText(extraChunk + decodeText(chunk))) }, }) } @@ -1781,10 +1754,10 @@ function renderToStream({ ReactDOMServer: typeof import('react-dom/server') element: React.ReactElement suffix?: string - dataStream?: ReadableStream + dataStream?: ReadableStream generateStaticHTML: boolean flushEffectHandler?: () => Promise -}): Promise> { +}): Promise> { return new Promise((resolve, reject) => { let resolved = false @@ -1799,7 +1772,7 @@ function renderToStream({ // defer to a microtask to ensure `stream` is set. resolve( Promise.resolve().then(() => { - const transforms: Array> = [ + const transforms: Array> = [ createBufferedTransformStream(), flushEffectHandler ? createFlushEffectStream(flushEffectHandler) @@ -1820,45 +1793,57 @@ function renderToStream({ } } - const renderStream = pipeThrough( - (ReactDOMServer as any).renderToReadableStream(element, { - onError(err: Error) { - if (!resolved) { - resolved = true - reject(err) - } - }, - onCompleteShell() { - if (!generateStaticHTML) { - doResolve() - } - }, - onCompleteAll() { + const renderStream: ReadableStream = ( + ReactDOMServer as any + ).renderToReadableStream(element, { + onError(err: Error) { + if (!resolved) { + resolved = true + reject(err) + } + }, + onCompleteShell() { + if (!generateStaticHTML) { doResolve() - }, - }), - createTextDecoderStream() - ) + } + }, + onCompleteAll() { + doResolve() + }, + }) }) } -function createSuffixStream(suffix: string): TransformStream { +function encodeText(input: string) { + return new TextEncoder().encode(input) +} + +function decodeText(input?: Uint8Array) { + return new TextDecoder().decode(input) +} + +function createSuffixStream( + suffix: string +): TransformStream { return createTransformStream({ flush(controller) { if (suffix) { - controller.enqueue(suffix) + controller.enqueue(encodeText(suffix)) } }, }) } -function createPrefixStream(prefix: string): TransformStream { +function createPrefixStream( + prefix: string +): TransformStream { let prefixFlushed = false return createTransformStream({ transform(chunk, controller) { if (!prefixFlushed && prefix) { prefixFlushed = true - controller.enqueue(chunk + prefix) + controller.enqueue(chunk) + controller.enqueue(encodeText(prefix)) } else { controller.enqueue(chunk) } @@ -1866,15 +1851,15 @@ function createPrefixStream(prefix: string): TransformStream { flush(controller) { if (!prefixFlushed && prefix) { prefixFlushed = true - controller.enqueue(prefix) + controller.enqueue(encodeText(prefix)) } }, }) } function createInlineDataStream( - dataStream: ReadableStream -): TransformStream { + dataStream: ReadableStream +): TransformStream { let dataStreamFinished: Promise | null = null return createTransformStream({ transform(chunk, controller) { @@ -1966,19 +1951,21 @@ function chainStreams(streams: ReadableStream[]): ReadableStream { return readable } -function streamFromArray(strings: string[]): ReadableStream { +function streamFromArray(strings: string[]): ReadableStream { // Note: we use a TransformStream here instead of instantiating a ReadableStream // because the built-in ReadableStream polyfill runs strings through TextEncoder. const { readable, writable } = new TransformStream() const writer = writable.getWriter() - strings.forEach((str) => writer.write(str)) + strings.forEach((str) => writer.write(encodeText(str))) writer.close() return readable } -async function streamToString(stream: ReadableStream): Promise { +async function streamToString( + stream: ReadableStream +): Promise { const reader = stream.getReader() let bufferedString = '' @@ -1989,6 +1976,6 @@ async function streamToString(stream: ReadableStream): Promise { return bufferedString } - bufferedString += value + bufferedString += decodeText(value) } }