import type { IncomingMessage, ServerResponse } from 'http' import type { LoadComponentsReturnType } from './load-components' import React from 'react' import { ParsedUrlQuery, stringify as stringifyQuery } from 'querystring' import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack' import { renderToReadableStream } from 'next/dist/compiled/react-server-dom-webpack/writer.browser.server' import { StyleRegistry, createStyleRegistry } from 'styled-jsx' import { NextParsedUrlQuery } from './request-meta' import RenderResult from './render-result' import { readableStreamTee, encodeText, decodeText, renderToInitialStream, createBufferedTransformStream, continueFromInitialStream, } from './node-web-streams-helper' import { FlushEffectsContext } from '../shared/lib/flush-effects' import { isDynamicRoute } from '../shared/lib/router/utils' import { tryGetPreviewData } from './api-utils/node' const ReactDOMServer = process.env.__NEXT_REACT_ROOT ? require('react-dom/server.browser') : require('react-dom/server') export type RenderOptsPartial = { err?: Error | null dev?: boolean serverComponentManifest?: any supportsDynamicHTML?: boolean runtime?: 'nodejs' | 'edge' serverComponents?: boolean reactRoot: boolean } export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial const rscCache = new Map() // Shadowing check does not work with TypeScript enums // eslint-disable-next-line no-shadow const enum RecordStatus { Pending, Resolved, Rejected, } type Record = { status: RecordStatus value: any } function createRecordFromThenable(thenable: Promise) { const record: Record = { status: RecordStatus.Pending, value: thenable, } thenable.then( function (value) { if (record.status === RecordStatus.Pending) { const resolvedRecord = record resolvedRecord.status = RecordStatus.Resolved resolvedRecord.value = value } }, function (err) { if (record.status === RecordStatus.Pending) { const rejectedRecord = record rejectedRecord.status = RecordStatus.Rejected rejectedRecord.value = err } } ) return record } function readRecordValue(record: Record) { if (record.status === RecordStatus.Resolved) { return record.value } else { throw record.value } } function preloadDataFetchingRecord( map: Map, key: string, fetcher: () => Promise | any ) { let record = map.get(key) if (!record) { const thenable = fetcher() record = createRecordFromThenable(thenable) map.set(key, record) } return record } function createFlightHook() { return ( writable: WritableStream, id: string, req: ReadableStream, bootstrap: boolean ) => { let entry = rscCache.get(id) if (!entry) { const [renderStream, forwardStream] = readableStreamTee(req) entry = createFromReadableStream(renderStream) rscCache.set(id, entry) let bootstrapped = false const forwardReader = forwardStream.getReader() const writer = writable.getWriter() function process() { forwardReader.read().then(({ done, value }) => { if (bootstrap && !bootstrapped) { bootstrapped = true writer.write( encodeText( `` ) ) } if (done) { rscCache.delete(id) writer.close() } else { writer.write( encodeText( `` ) ) process() } }) } process() } return entry } } const useFlightResponse = createFlightHook() // Create the wrapper component for a Flight stream. function createServerComponentRenderer( ComponentToRender: React.ComponentType, ComponentMod: any, { cachePrefix, transformStream, serverComponentManifest, }: { cachePrefix: string transformStream: TransformStream serverComponentManifest: NonNullable } ) { // We need to expose the `__webpack_require__` API globally for // react-server-dom-webpack. This is a hack until we find a better way. if (ComponentMod.__next_rsc__) { // @ts-ignore globalThis.__webpack_require__ = ComponentMod.__next_rsc__.__webpack_require__ // @ts-ignore globalThis.__webpack_chunk_load__ = () => Promise.resolve() } const writable = transformStream.writable const ServerComponentWrapper = (props: any) => { const id = (React as any).useId() const reqStream: ReadableStream = renderToReadableStream( , serverComponentManifest ) const response = useFlightResponse( writable, cachePrefix + ',' + id, reqStream, true ) const root = response.readRoot() rscCache.delete(id) return root } return ServerComponentWrapper } export async function renderToHTML( req: IncomingMessage, res: ServerResponse, pathname: string, query: NextParsedUrlQuery, renderOpts: RenderOpts ): Promise { // don't modify original query object query = Object.assign({}, query) const { buildManifest, serverComponentManifest, supportsDynamicHTML, runtime, ComponentMod, } = renderOpts const isFlight = query.__flight__ !== undefined const flightRouterPath = isFlight ? query.__flight_router_path__ : undefined delete query.__flight__ delete query.__flight_router_path__ const hasConcurrentFeatures = !!runtime const pageIsDynamic = isDynamicRoute(pathname) const componentPaths = Object.keys(ComponentMod.components) const components = componentPaths .filter((path) => { // Rendering part of the page is only allowed for flight data if (flightRouterPath) { // TODO: check the actual path const pathLength = path.length return pathLength >= flightRouterPath.length } return true }) .sort() .map((path) => { const mod = ComponentMod.components[path]() mod.Component = mod.default || mod return mod }) const isSubtreeRender = components.length < componentPaths.length // Reads of this are cached on the `req` object, so this should resolve // instantly. There's no need to pass this data down from a previous // invoke, where we'd have to consider server & serverless. const previewData = tryGetPreviewData( req, res, (renderOpts as any).previewProps ) const isPreview = previewData !== false const dataCache = new Map() let WrappedComponent: any for (let i = components.length - 1; i >= 0; i--) { const dataCacheKey = i.toString() const layout = components[i] let fetcher: any // TODO: pass a shared cache from previous getStaticProps/ // getServerSideProps calls? if (layout.getServerSideProps) { fetcher = () => Promise.resolve( layout.getServerSideProps!({ req: req as any, res: res, query, resolvedUrl: (renderOpts as any).resolvedUrl as string, ...(pageIsDynamic ? { params: (renderOpts as any).params as ParsedUrlQuery } : undefined), ...(isPreview ? { preview: true, previewData: previewData } : undefined), locales: (renderOpts as any).locales, locale: (renderOpts as any).locale, defaultLocale: (renderOpts as any).defaultLocale, }) ) } // TODO: implement layout specific caching for getStaticProps if (layout.getStaticProps) { fetcher = () => Promise.resolve( layout.getStaticProps!({ ...(pageIsDynamic ? { params: query as ParsedUrlQuery } : undefined), ...(isPreview ? { preview: true, previewData: previewData } : undefined), locales: (renderOpts as any).locales, locale: (renderOpts as any).locale, defaultLocale: (renderOpts as any).defaultLocale, }) ) } if (fetcher) { // Kick off data fetching before rendering, this ensures there is no waterfall for layouts as // all data fetching required to render the page is kicked off simultaneously preloadDataFetchingRecord(dataCache, dataCacheKey, fetcher) } // eslint-disable-next-line no-loop-func const lastComponent = WrappedComponent WrappedComponent = (props: any) => { if (fetcher) { // The data fetching was kicked off before rendering (see above) // if the data was not resolved yet the layout rendering will be suspended const record = preloadDataFetchingRecord( dataCache, dataCacheKey, fetcher ) // Result of calling getStaticProps or getServerSideProps. If promise is not resolve yet it will suspend. const recordValue = readRecordValue(record) if (props) { props = Object.assign({}, props, recordValue.props) } else { props = recordValue.props } } // if this is the root layout pass children as bodyChildren prop if (!isSubtreeRender && i === 0) { return React.createElement(layout.Component, { ...props, headChildren: props.headChildren, bodyChildren: React.createElement( lastComponent || React.Fragment, {}, null ), }) } return React.createElement( layout.Component, props, React.createElement(lastComponent || React.Fragment, {}, null) ) } // TODO: loading state // const AfterWrap = WrappedComponent // WrappedComponent = () => { // return ( // Loading...}> // // // ) // } } const headChildren = !isSubtreeRender ? buildManifest.rootMainFiles.map((src) => (