| | import type { ReactDOMServerReadableStream } from 'react-dom/server' |
| | import { getTracer } from '../lib/trace/tracer' |
| | import { AppRenderSpan } from '../lib/trace/constants' |
| | import { DetachedPromise } from '../../lib/detached-promise' |
| | import { |
| | scheduleImmediate, |
| | atLeastOneTask, |
| | waitAtLeastOneReactRenderTask, |
| | } from '../../lib/scheduler' |
| | import { ENCODED_TAGS } from './encoded-tags' |
| | import { |
| | indexOfUint8Array, |
| | isEquivalentUint8Arrays, |
| | removeFromUint8Array, |
| | } from './uint8array-helpers' |
| | import { MISSING_ROOT_TAGS_ERROR } from '../../shared/lib/errors/constants' |
| | import { insertBuildIdComment } from '../../shared/lib/segment-cache/output-export-prefetch-encoding' |
| | import { |
| | RSC_HEADER, |
| | NEXT_ROUTER_PREFETCH_HEADER, |
| | NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, |
| | NEXT_RSC_UNION_QUERY, |
| | } from '../../client/components/app-router-headers' |
| | import { computeCacheBustingSearchParam } from '../../shared/lib/router/utils/cache-busting-search-param' |
| |
|
| | function voidCatch() { |
| | |
| | |
| | |
| | } |
| |
|
| | |
| | |
| | |
| | const encoder = new TextEncoder() |
| |
|
| | export function chainStreams<T>( |
| | ...streams: ReadableStream<T>[] |
| | ): ReadableStream<T> { |
| | |
| | |
| | if (streams.length === 0) { |
| | return new ReadableStream<T>({ |
| | start(controller) { |
| | controller.close() |
| | }, |
| | }) |
| | } |
| |
|
| | |
| | if (streams.length === 1) { |
| | return streams[0] |
| | } |
| |
|
| | const { readable, writable } = new TransformStream() |
| |
|
| | |
| | |
| | let promise = streams[0].pipeTo(writable, { preventClose: true }) |
| |
|
| | let i = 1 |
| | for (; i < streams.length - 1; i++) { |
| | const nextStream = streams[i] |
| | promise = promise.then(() => |
| | nextStream.pipeTo(writable, { preventClose: true }) |
| | ) |
| | } |
| |
|
| | |
| | |
| | const lastStream = streams[i] |
| | promise = promise.then(() => lastStream.pipeTo(writable)) |
| |
|
| | |
| | |
| | promise.catch(voidCatch) |
| |
|
| | return readable |
| | } |
| |
|
| | export function streamFromString(str: string): ReadableStream<Uint8Array> { |
| | return new ReadableStream({ |
| | start(controller) { |
| | controller.enqueue(encoder.encode(str)) |
| | controller.close() |
| | }, |
| | }) |
| | } |
| |
|
| | export function streamFromBuffer(chunk: Buffer): ReadableStream<Uint8Array> { |
| | return new ReadableStream({ |
| | start(controller) { |
| | controller.enqueue(chunk) |
| | controller.close() |
| | }, |
| | }) |
| | } |
| |
|
| | async function streamToChunks( |
| | stream: ReadableStream<Uint8Array> |
| | ): Promise<Array<Uint8Array>> { |
| | const reader = stream.getReader() |
| | const chunks: Array<Uint8Array> = [] |
| |
|
| | while (true) { |
| | const { done, value } = await reader.read() |
| | if (done) { |
| | break |
| | } |
| |
|
| | chunks.push(value) |
| | } |
| |
|
| | return chunks |
| | } |
| |
|
| | function concatUint8Arrays(chunks: Array<Uint8Array>): Uint8Array { |
| | const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0) |
| | const result = new Uint8Array(totalLength) |
| | let offset = 0 |
| | for (const chunk of chunks) { |
| | result.set(chunk, offset) |
| | offset += chunk.length |
| | } |
| | return result |
| | } |
| |
|
| | export async function streamToUint8Array( |
| | stream: ReadableStream<Uint8Array> |
| | ): Promise<Uint8Array> { |
| | return concatUint8Arrays(await streamToChunks(stream)) |
| | } |
| |
|
| | export async function streamToBuffer( |
| | stream: ReadableStream<Uint8Array> |
| | ): Promise<Buffer> { |
| | return Buffer.concat(await streamToChunks(stream)) |
| | } |
| |
|
| | export async function streamToString( |
| | stream: ReadableStream<Uint8Array>, |
| | signal?: AbortSignal |
| | ): Promise<string> { |
| | const decoder = new TextDecoder('utf-8', { fatal: true }) |
| | let string = '' |
| |
|
| | for await (const chunk of stream) { |
| | if (signal?.aborted) { |
| | return string |
| | } |
| |
|
| | string += decoder.decode(chunk, { stream: true }) |
| | } |
| |
|
| | string += decoder.decode() |
| |
|
| | return string |
| | } |
| |
|
| | export type BufferedTransformOptions = { |
| | |
| | |
| | |
| | readonly maxBufferByteLength?: number |
| | } |
| |
|
| | export function createBufferedTransformStream( |
| | options: BufferedTransformOptions = {} |
| | ): TransformStream<Uint8Array, Uint8Array> { |
| | const { maxBufferByteLength = Infinity } = options |
| |
|
| | let bufferedChunks: Array<Uint8Array> = [] |
| | let bufferByteLength: number = 0 |
| | let pending: DetachedPromise<void> | undefined |
| |
|
| | const flush = (controller: TransformStreamDefaultController) => { |
| | try { |
| | if (bufferedChunks.length === 0) { |
| | return |
| | } |
| |
|
| | const chunk = new Uint8Array(bufferByteLength) |
| | let copiedBytes = 0 |
| |
|
| | for (let i = 0; i < bufferedChunks.length; i++) { |
| | const bufferedChunk = bufferedChunks[i] |
| | chunk.set(bufferedChunk, copiedBytes) |
| | copiedBytes += bufferedChunk.byteLength |
| | } |
| | |
| | |
| | bufferedChunks.length = 0 |
| | bufferByteLength = 0 |
| | controller.enqueue(chunk) |
| | } catch { |
| | |
| | |
| | |
| | } |
| | } |
| |
|
| | const scheduleFlush = (controller: TransformStreamDefaultController) => { |
| | if (pending) { |
| | return |
| | } |
| |
|
| | const detached = new DetachedPromise<void>() |
| | pending = detached |
| |
|
| | scheduleImmediate(() => { |
| | try { |
| | flush(controller) |
| | } finally { |
| | pending = undefined |
| | detached.resolve() |
| | } |
| | }) |
| | } |
| |
|
| | return new TransformStream({ |
| | transform(chunk, controller) { |
| | |
| | bufferedChunks.push(chunk) |
| | bufferByteLength += chunk.byteLength |
| |
|
| | if (bufferByteLength >= maxBufferByteLength) { |
| | flush(controller) |
| | } else { |
| | scheduleFlush(controller) |
| | } |
| | }, |
| | flush() { |
| | return pending?.promise |
| | }, |
| | }) |
| | } |
| |
|
| | function createPrefetchCommentStream( |
| | isBuildTimePrerendering: boolean, |
| | buildId: string |
| | ): TransformStream<Uint8Array, Uint8Array> { |
| | |
| | |
| | |
| | |
| | |
| | let didTransformFirstChunk = false |
| | return new TransformStream({ |
| | transform(chunk, controller) { |
| | if (isBuildTimePrerendering && !didTransformFirstChunk) { |
| | didTransformFirstChunk = true |
| | const decoder = new TextDecoder('utf-8', { fatal: true }) |
| | const chunkStr = decoder.decode(chunk, { |
| | stream: true, |
| | }) |
| | const updatedChunkStr = insertBuildIdComment(chunkStr, buildId) |
| | controller.enqueue(encoder.encode(updatedChunkStr)) |
| | return |
| | } |
| | controller.enqueue(chunk) |
| | }, |
| | }) |
| | } |
| |
|
| | export function renderToInitialFizzStream({ |
| | ReactDOMServer, |
| | element, |
| | streamOptions, |
| | }: { |
| | ReactDOMServer: { |
| | renderToReadableStream: typeof import('react-dom/server').renderToReadableStream |
| | } |
| | element: React.ReactElement |
| | streamOptions?: Parameters<typeof ReactDOMServer.renderToReadableStream>[1] |
| | }): Promise<ReactDOMServerReadableStream> { |
| | return getTracer().trace(AppRenderSpan.renderToReadableStream, async () => |
| | ReactDOMServer.renderToReadableStream(element, streamOptions) |
| | ) |
| | } |
| |
|
| | function createMetadataTransformStream( |
| | insert: () => Promise<string> | string |
| | ): TransformStream<Uint8Array, Uint8Array> { |
| | let chunkIndex = -1 |
| | let isMarkRemoved = false |
| |
|
| | return new TransformStream({ |
| | async transform(chunk, controller) { |
| | let iconMarkIndex = -1 |
| | let closedHeadIndex = -1 |
| | chunkIndex++ |
| |
|
| | if (isMarkRemoved) { |
| | controller.enqueue(chunk) |
| | return |
| | } |
| | let iconMarkLength = 0 |
| | |
| | if (iconMarkIndex === -1) { |
| | iconMarkIndex = indexOfUint8Array(chunk, ENCODED_TAGS.META.ICON_MARK) |
| | if (iconMarkIndex === -1) { |
| | controller.enqueue(chunk) |
| | return |
| | } else { |
| | |
| | |
| | iconMarkLength = ENCODED_TAGS.META.ICON_MARK.length |
| | |
| | if (chunk[iconMarkIndex + iconMarkLength] === 47) { |
| | iconMarkLength += 2 |
| | } else { |
| | |
| | iconMarkLength++ |
| | } |
| | } |
| | } |
| |
|
| | |
| | if (chunkIndex === 0) { |
| | closedHeadIndex = indexOfUint8Array(chunk, ENCODED_TAGS.CLOSED.HEAD) |
| | if (iconMarkIndex !== -1) { |
| | |
| | |
| | |
| | if (iconMarkIndex < closedHeadIndex) { |
| | const replaced = new Uint8Array(chunk.length - iconMarkLength) |
| |
|
| | |
| | replaced.set(chunk.subarray(0, iconMarkIndex)) |
| | replaced.set( |
| | chunk.subarray(iconMarkIndex + iconMarkLength), |
| | iconMarkIndex |
| | ) |
| | chunk = replaced |
| | } else { |
| | |
| | const insertion = await insert() |
| | const encodedInsertion = encoder.encode(insertion) |
| | const insertionLength = encodedInsertion.length |
| | const replaced = new Uint8Array( |
| | chunk.length - iconMarkLength + insertionLength |
| | ) |
| | replaced.set(chunk.subarray(0, iconMarkIndex)) |
| | replaced.set(encodedInsertion, iconMarkIndex) |
| | replaced.set( |
| | chunk.subarray(iconMarkIndex + iconMarkLength), |
| | iconMarkIndex + insertionLength |
| | ) |
| | chunk = replaced |
| | } |
| | isMarkRemoved = true |
| | } |
| | |
| | } else { |
| | |
| | |
| | const insertion = await insert() |
| | const encodedInsertion = encoder.encode(insertion) |
| | const insertionLength = encodedInsertion.length |
| | |
| | const replaced = new Uint8Array( |
| | chunk.length - iconMarkLength + insertionLength |
| | ) |
| | |
| | replaced.set(chunk.subarray(0, iconMarkIndex)) |
| | |
| | replaced.set(encodedInsertion, iconMarkIndex) |
| |
|
| | |
| | replaced.set( |
| | chunk.subarray(iconMarkIndex + iconMarkLength), |
| | iconMarkIndex + insertionLength |
| | ) |
| | chunk = replaced |
| | isMarkRemoved = true |
| | } |
| | controller.enqueue(chunk) |
| | }, |
| | }) |
| | } |
| |
|
| | function createHeadInsertionTransformStream( |
| | insert: () => Promise<string> |
| | ): TransformStream<Uint8Array, Uint8Array> { |
| | let inserted = false |
| |
|
| | |
| | |
| | let hasBytes = false |
| |
|
| | return new TransformStream({ |
| | async transform(chunk, controller) { |
| | hasBytes = true |
| |
|
| | const insertion = await insert() |
| | if (inserted) { |
| | if (insertion) { |
| | const encodedInsertion = encoder.encode(insertion) |
| | controller.enqueue(encodedInsertion) |
| | } |
| | controller.enqueue(chunk) |
| | } else { |
| | |
| | const index = indexOfUint8Array(chunk, ENCODED_TAGS.CLOSED.HEAD) |
| | |
| | |
| | if (index !== -1) { |
| | if (insertion) { |
| | const encodedInsertion = encoder.encode(insertion) |
| | |
| | |
| | |
| | |
| | |
| | const insertedHeadContent = new Uint8Array( |
| | chunk.length + encodedInsertion.length |
| | ) |
| | |
| | insertedHeadContent.set(chunk.slice(0, index)) |
| | |
| | insertedHeadContent.set(encodedInsertion, index) |
| | |
| | insertedHeadContent.set( |
| | chunk.slice(index), |
| | index + encodedInsertion.length |
| | ) |
| | controller.enqueue(insertedHeadContent) |
| | } else { |
| | controller.enqueue(chunk) |
| | } |
| | inserted = true |
| | } else { |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (insertion) { |
| | controller.enqueue(encoder.encode(insertion)) |
| | } |
| | controller.enqueue(chunk) |
| | inserted = true |
| | } |
| | } |
| | }, |
| | async flush(controller) { |
| | |
| | if (hasBytes) { |
| | const insertion = await insert() |
| | if (insertion) { |
| | controller.enqueue(encoder.encode(insertion)) |
| | } |
| | } |
| | }, |
| | }) |
| | } |
| |
|
| | function createClientResumeScriptInsertionTransformStream(): TransformStream< |
| | Uint8Array, |
| | Uint8Array |
| | > { |
| | const segmentPath = '/_full' |
| | const cacheBustingHeader = computeCacheBustingSearchParam( |
| | '1', |
| | '/_full', |
| | undefined, |
| | undefined |
| | ) |
| | const searchStr = `${NEXT_RSC_UNION_QUERY}=${cacheBustingHeader}` |
| | const NEXT_CLIENT_RESUME_SCRIPT = `<script>__NEXT_CLIENT_RESUME=fetch(location.pathname+'?${searchStr}',{credentials:'same-origin',headers:{'${RSC_HEADER}': '1','${NEXT_ROUTER_PREFETCH_HEADER}': '1','${NEXT_ROUTER_SEGMENT_PREFETCH_HEADER}': '${segmentPath}'}})</script>` |
| |
|
| | let didAlreadyInsert = false |
| | return new TransformStream({ |
| | transform(chunk, controller) { |
| | if (didAlreadyInsert) { |
| | |
| | controller.enqueue(chunk) |
| | return |
| | } |
| | |
| | const headClosingTagIndex = indexOfUint8Array( |
| | chunk, |
| | ENCODED_TAGS.CLOSED.HEAD |
| | ) |
| |
|
| | if (headClosingTagIndex === -1) { |
| | |
| | |
| | controller.enqueue(chunk) |
| | return |
| | } |
| |
|
| | const encodedInsertion = encoder.encode(NEXT_CLIENT_RESUME_SCRIPT) |
| | |
| | |
| | |
| | |
| | |
| | const insertedHeadContent = new Uint8Array( |
| | chunk.length + encodedInsertion.length |
| | ) |
| | |
| | insertedHeadContent.set(chunk.slice(0, headClosingTagIndex)) |
| | |
| | insertedHeadContent.set(encodedInsertion, headClosingTagIndex) |
| | |
| | insertedHeadContent.set( |
| | chunk.slice(headClosingTagIndex), |
| | headClosingTagIndex + encodedInsertion.length |
| | ) |
| |
|
| | controller.enqueue(insertedHeadContent) |
| | didAlreadyInsert = true |
| | }, |
| | }) |
| | } |
| |
|
| | |
| | |
| | function createDeferredSuffixStream( |
| | suffix: string |
| | ): TransformStream<Uint8Array, Uint8Array> { |
| | let flushed = false |
| | let pending: DetachedPromise<void> | undefined |
| |
|
| | const flush = (controller: TransformStreamDefaultController) => { |
| | const detached = new DetachedPromise<void>() |
| | pending = detached |
| |
|
| | scheduleImmediate(() => { |
| | try { |
| | controller.enqueue(encoder.encode(suffix)) |
| | } catch { |
| | |
| | |
| | |
| | } finally { |
| | pending = undefined |
| | detached.resolve() |
| | } |
| | }) |
| | } |
| |
|
| | return new TransformStream({ |
| | transform(chunk, controller) { |
| | controller.enqueue(chunk) |
| |
|
| | |
| | if (flushed) return |
| |
|
| | |
| | flushed = true |
| | flush(controller) |
| | }, |
| | flush(controller) { |
| | if (pending) return pending.promise |
| | if (flushed) return |
| |
|
| | |
| | controller.enqueue(encoder.encode(suffix)) |
| | }, |
| | }) |
| | } |
| |
|
| | function createFlightDataInjectionTransformStream( |
| | stream: ReadableStream<Uint8Array>, |
| | delayDataUntilFirstHtmlChunk: boolean |
| | ): TransformStream<Uint8Array, Uint8Array> { |
| | let htmlStreamFinished = false |
| |
|
| | let pull: Promise<void> | null = null |
| | let donePulling = false |
| |
|
| | function startOrContinuePulling( |
| | controller: TransformStreamDefaultController |
| | ) { |
| | if (!pull) { |
| | pull = startPulling(controller) |
| | } |
| | return pull |
| | } |
| |
|
| | async function startPulling(controller: TransformStreamDefaultController) { |
| | const reader = stream.getReader() |
| |
|
| | if (delayDataUntilFirstHtmlChunk) { |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | await atLeastOneTask() |
| | } |
| |
|
| | try { |
| | while (true) { |
| | const { done, value } = await reader.read() |
| | if (done) { |
| | donePulling = true |
| | return |
| | } |
| |
|
| | |
| | |
| | |
| | if (!delayDataUntilFirstHtmlChunk && !htmlStreamFinished) { |
| | await atLeastOneTask() |
| | } |
| | controller.enqueue(value) |
| | } |
| | } catch (err) { |
| | controller.error(err) |
| | } |
| | } |
| |
|
| | return new TransformStream({ |
| | start(controller) { |
| | if (!delayDataUntilFirstHtmlChunk) { |
| | startOrContinuePulling(controller) |
| | } |
| | }, |
| | transform(chunk, controller) { |
| | controller.enqueue(chunk) |
| |
|
| | |
| | if (delayDataUntilFirstHtmlChunk) { |
| | startOrContinuePulling(controller) |
| | } |
| | }, |
| | flush(controller) { |
| | htmlStreamFinished = true |
| | if (donePulling) { |
| | return |
| | } |
| | return startOrContinuePulling(controller) |
| | }, |
| | }) |
| | } |
| |
|
| | const CLOSE_TAG = '</body></html>' |
| |
|
| | |
| | |
| | |
| | |
| | |
| | function createMoveSuffixStream(): TransformStream<Uint8Array, Uint8Array> { |
| | let foundSuffix = false |
| |
|
| | return new TransformStream({ |
| | transform(chunk, controller) { |
| | if (foundSuffix) { |
| | return controller.enqueue(chunk) |
| | } |
| |
|
| | const index = indexOfUint8Array(chunk, ENCODED_TAGS.CLOSED.BODY_AND_HTML) |
| | if (index > -1) { |
| | foundSuffix = true |
| |
|
| | |
| | |
| | if (chunk.length === ENCODED_TAGS.CLOSED.BODY_AND_HTML.length) { |
| | return |
| | } |
| |
|
| | |
| | const before = chunk.slice(0, index) |
| | controller.enqueue(before) |
| |
|
| | |
| | |
| | if (chunk.length > ENCODED_TAGS.CLOSED.BODY_AND_HTML.length + index) { |
| | |
| | const after = chunk.slice( |
| | index + ENCODED_TAGS.CLOSED.BODY_AND_HTML.length |
| | ) |
| | controller.enqueue(after) |
| | } |
| | } else { |
| | controller.enqueue(chunk) |
| | } |
| | }, |
| | flush(controller) { |
| | |
| | |
| | controller.enqueue(ENCODED_TAGS.CLOSED.BODY_AND_HTML) |
| | }, |
| | }) |
| | } |
| |
|
| | function createStripDocumentClosingTagsTransform(): TransformStream< |
| | Uint8Array, |
| | Uint8Array |
| | > { |
| | return new TransformStream({ |
| | transform(chunk, controller) { |
| | |
| | |
| | |
| | |
| | |
| | if ( |
| | isEquivalentUint8Arrays(chunk, ENCODED_TAGS.CLOSED.BODY_AND_HTML) || |
| | isEquivalentUint8Arrays(chunk, ENCODED_TAGS.CLOSED.BODY) || |
| | isEquivalentUint8Arrays(chunk, ENCODED_TAGS.CLOSED.HTML) |
| | ) { |
| | |
| | return |
| | } |
| |
|
| | |
| | |
| | |
| | chunk = removeFromUint8Array(chunk, ENCODED_TAGS.CLOSED.BODY) |
| | chunk = removeFromUint8Array(chunk, ENCODED_TAGS.CLOSED.HTML) |
| |
|
| | controller.enqueue(chunk) |
| | }, |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | export function createRootLayoutValidatorStream(): TransformStream< |
| | Uint8Array, |
| | Uint8Array |
| | > { |
| | let foundHtml = false |
| | let foundBody = false |
| | return new TransformStream({ |
| | async transform(chunk, controller) { |
| | |
| | if ( |
| | !foundHtml && |
| | indexOfUint8Array(chunk, ENCODED_TAGS.OPENING.HTML) > -1 |
| | ) { |
| | foundHtml = true |
| | } |
| |
|
| | if ( |
| | !foundBody && |
| | indexOfUint8Array(chunk, ENCODED_TAGS.OPENING.BODY) > -1 |
| | ) { |
| | foundBody = true |
| | } |
| |
|
| | controller.enqueue(chunk) |
| | }, |
| | flush(controller) { |
| | const missingTags: ('html' | 'body')[] = [] |
| | if (!foundHtml) missingTags.push('html') |
| | if (!foundBody) missingTags.push('body') |
| |
|
| | if (!missingTags.length) return |
| |
|
| | controller.enqueue( |
| | encoder.encode( |
| | `<html id="__next_error__"> |
| | <template |
| | data-next-error-message="Missing ${missingTags |
| | .map((c) => `<${c}>`) |
| | .join( |
| | missingTags.length > 1 ? ' and ' : '' |
| | )} tags in the root layout.\nRead more at https://nextjs.org/docs/messages/missing-root-layout-tags" |
| | data-next-error-digest="${MISSING_ROOT_TAGS_ERROR}" |
| | data-next-error-stack="" |
| | ></template> |
| | ` |
| | ) |
| | ) |
| | }, |
| | }) |
| | } |
| |
|
| | function chainTransformers<T>( |
| | readable: ReadableStream<T>, |
| | transformers: ReadonlyArray<TransformStream<T, T> | null> |
| | ): ReadableStream<T> { |
| | let stream = readable |
| | for (const transformer of transformers) { |
| | if (!transformer) continue |
| |
|
| | stream = stream.pipeThrough(transformer) |
| | } |
| | return stream |
| | } |
| |
|
| | export type ContinueStreamOptions = { |
| | inlinedDataStream: ReadableStream<Uint8Array> | undefined |
| | isStaticGeneration: boolean |
| | isBuildTimePrerendering: boolean |
| | buildId: string |
| | getServerInsertedHTML: () => Promise<string> |
| | getServerInsertedMetadata: () => Promise<string> |
| | validateRootLayout?: boolean |
| | |
| | |
| | |
| | suffix?: string | undefined |
| | } |
| |
|
| | export async function continueFizzStream( |
| | renderStream: ReactDOMServerReadableStream, |
| | { |
| | suffix, |
| | inlinedDataStream, |
| | isStaticGeneration, |
| | isBuildTimePrerendering, |
| | buildId, |
| | getServerInsertedHTML, |
| | getServerInsertedMetadata, |
| | validateRootLayout, |
| | }: ContinueStreamOptions |
| | ): Promise<ReadableStream<Uint8Array>> { |
| | |
| | const suffixUnclosed = suffix ? suffix.split(CLOSE_TAG, 1)[0] : null |
| |
|
| | if (isStaticGeneration) { |
| | |
| | await renderStream.allReady |
| | } else { |
| | |
| | |
| | await waitAtLeastOneReactRenderTask() |
| | } |
| |
|
| | return chainTransformers(renderStream, [ |
| | |
| | createBufferedTransformStream(), |
| |
|
| | |
| | createPrefetchCommentStream(isBuildTimePrerendering, buildId), |
| |
|
| | |
| | createMetadataTransformStream(getServerInsertedMetadata), |
| |
|
| | |
| | suffixUnclosed != null && suffixUnclosed.length > 0 |
| | ? createDeferredSuffixStream(suffixUnclosed) |
| | : null, |
| |
|
| | |
| | inlinedDataStream |
| | ? createFlightDataInjectionTransformStream(inlinedDataStream, true) |
| | : null, |
| |
|
| | |
| | validateRootLayout ? createRootLayoutValidatorStream() : null, |
| |
|
| | |
| | createMoveSuffixStream(), |
| |
|
| | |
| | |
| | |
| | createHeadInsertionTransformStream(getServerInsertedHTML), |
| | ]) |
| | } |
| |
|
| | type ContinueDynamicPrerenderOptions = { |
| | getServerInsertedHTML: () => Promise<string> |
| | getServerInsertedMetadata: () => Promise<string> |
| | } |
| |
|
| | export async function continueDynamicPrerender( |
| | prerenderStream: ReadableStream<Uint8Array>, |
| | { |
| | getServerInsertedHTML, |
| | getServerInsertedMetadata, |
| | }: ContinueDynamicPrerenderOptions |
| | ) { |
| | return ( |
| | prerenderStream |
| | |
| | .pipeThrough(createBufferedTransformStream()) |
| | .pipeThrough(createStripDocumentClosingTagsTransform()) |
| | |
| | .pipeThrough(createHeadInsertionTransformStream(getServerInsertedHTML)) |
| | |
| | .pipeThrough(createMetadataTransformStream(getServerInsertedMetadata)) |
| | ) |
| | } |
| |
|
| | type ContinueStaticPrerenderOptions = { |
| | inlinedDataStream: ReadableStream<Uint8Array> |
| | getServerInsertedHTML: () => Promise<string> |
| | getServerInsertedMetadata: () => Promise<string> |
| | isBuildTimePrerendering: boolean |
| | buildId: string |
| | } |
| |
|
| | export async function continueStaticPrerender( |
| | prerenderStream: ReadableStream<Uint8Array>, |
| | { |
| | inlinedDataStream, |
| | getServerInsertedHTML, |
| | getServerInsertedMetadata, |
| | isBuildTimePrerendering, |
| | buildId, |
| | }: ContinueStaticPrerenderOptions |
| | ) { |
| | return ( |
| | prerenderStream |
| | |
| | .pipeThrough(createBufferedTransformStream()) |
| | |
| | .pipeThrough( |
| | createPrefetchCommentStream(isBuildTimePrerendering, buildId) |
| | ) |
| | |
| | .pipeThrough(createHeadInsertionTransformStream(getServerInsertedHTML)) |
| | |
| | .pipeThrough(createMetadataTransformStream(getServerInsertedMetadata)) |
| | |
| | .pipeThrough( |
| | createFlightDataInjectionTransformStream(inlinedDataStream, true) |
| | ) |
| | |
| | .pipeThrough(createMoveSuffixStream()) |
| | ) |
| | } |
| |
|
| | export async function continueStaticFallbackPrerender( |
| | prerenderStream: ReadableStream<Uint8Array>, |
| | { |
| | inlinedDataStream, |
| | getServerInsertedHTML, |
| | getServerInsertedMetadata, |
| | isBuildTimePrerendering, |
| | buildId, |
| | }: ContinueStaticPrerenderOptions |
| | ) { |
| | |
| | |
| | |
| | return ( |
| | prerenderStream |
| | |
| | .pipeThrough(createBufferedTransformStream()) |
| | |
| | .pipeThrough( |
| | createPrefetchCommentStream(isBuildTimePrerendering, buildId) |
| | ) |
| | |
| | .pipeThrough(createHeadInsertionTransformStream(getServerInsertedHTML)) |
| | |
| | .pipeThrough(createClientResumeScriptInsertionTransformStream()) |
| | |
| | .pipeThrough(createMetadataTransformStream(getServerInsertedMetadata)) |
| | |
| | .pipeThrough( |
| | createFlightDataInjectionTransformStream(inlinedDataStream, true) |
| | ) |
| | |
| | .pipeThrough(createMoveSuffixStream()) |
| | ) |
| | } |
| |
|
| | type ContinueResumeOptions = { |
| | inlinedDataStream: ReadableStream<Uint8Array> |
| | getServerInsertedHTML: () => Promise<string> |
| | getServerInsertedMetadata: () => Promise<string> |
| | delayDataUntilFirstHtmlChunk: boolean |
| | } |
| |
|
| | export async function continueDynamicHTMLResume( |
| | renderStream: ReadableStream<Uint8Array>, |
| | { |
| | delayDataUntilFirstHtmlChunk, |
| | inlinedDataStream, |
| | getServerInsertedHTML, |
| | getServerInsertedMetadata, |
| | }: ContinueResumeOptions |
| | ) { |
| | return ( |
| | renderStream |
| | |
| | .pipeThrough(createBufferedTransformStream()) |
| | |
| | .pipeThrough(createHeadInsertionTransformStream(getServerInsertedHTML)) |
| | |
| | .pipeThrough(createMetadataTransformStream(getServerInsertedMetadata)) |
| | |
| | .pipeThrough( |
| | createFlightDataInjectionTransformStream( |
| | inlinedDataStream, |
| | delayDataUntilFirstHtmlChunk |
| | ) |
| | ) |
| | |
| | .pipeThrough(createMoveSuffixStream()) |
| | ) |
| | } |
| |
|
| | export function createDocumentClosingStream(): ReadableStream<Uint8Array> { |
| | return streamFromString(CLOSE_TAG) |
| | } |
| |
|