Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/next/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export type NextConfig = { [key: string]: any } & {
staticPageGenerationTimeout?: number
pageDataCollectionTimeout?: number
isrMemoryCacheSize?: number
concurrentFeatures?: boolean
}
}

Expand Down Expand Up @@ -185,6 +186,7 @@ export const defaultConfig: NextConfig = {
pageDataCollectionTimeout: 60,
// default to 50MB limit
isrMemoryCacheSize: 50 * 1024 * 1024,
concurrentFeatures: false,
},
future: {
strictPostcssConfiguration: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/next/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export default class Server {
defaultLocale?: string
domainLocales?: DomainLocale[]
distDir: string
concurrentFeatures?: boolean
}
private compression?: Middleware
private incrementalCache: IncrementalCache
Expand Down Expand Up @@ -241,6 +242,7 @@ export default class Server {
.disableOptimizedLoading,
domainLocales: this.nextConfig.i18n?.domains,
distDir: this.distDir,
concurrentFeatures: this.nextConfig.experimental.concurrentFeatures,
}

// Only the `publicRuntimeConfig` key is exposed to the client side
Expand Down
60 changes: 51 additions & 9 deletions packages/next/server/render.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { IncomingMessage, ServerResponse } from 'http'
import { ParsedUrlQuery } from 'querystring'
import { PassThrough } from 'stream'
import React from 'react'
import { renderToStaticMarkup, renderToString } from 'react-dom/server'
import * as ReactDOMServer from 'react-dom/server'
import { warn } from '../build/output/log'
import { UnwrapPromise } from '../lib/coalesced-function'
import {
Expand Down Expand Up @@ -43,6 +44,7 @@ import {
loadGetInitialProps,
NextComponentType,
RenderPage,
RenderPageResult,
} from '../shared/lib/utils'
import {
tryGetPreviewData,
Expand Down Expand Up @@ -190,6 +192,7 @@ export type RenderOptsPartial = {
domainLocales?: DomainLocale[]
disableOptimizedLoading?: boolean
requireStaticHTML?: boolean
concurrentFeatures?: boolean
}

export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
Expand Down Expand Up @@ -263,7 +266,7 @@ function renderDocument(
): string {
return (
'<!DOCTYPE html>' +
renderToStaticMarkup(
ReactDOMServer.renderToStaticMarkup(
<AmpStateContext.Provider value={ampState}>
{Document.renderDocument(Document, {
__NEXT_DATA__: {
Expand Down Expand Up @@ -408,6 +411,7 @@ export async function renderToHTML(
previewProps,
basePath,
devOnlyCacheBusterQueryString,
concurrentFeatures,
} = renderOpts

const getFontDefinition = (url: string): string => {
Expand Down Expand Up @@ -626,6 +630,8 @@ export async function renderToHTML(
let head: JSX.Element[] = defaultHead(inAmpMode)

let scriptLoader: any = {}
const nextExport =
!isSSG && (renderOpts.nextExport || (dev && (isAutoExport || isFallback)))

const AppContainer = ({ children }: any) => (
<RouterContext.Provider value={router}>
Expand Down Expand Up @@ -991,11 +997,45 @@ export async function renderToHTML(
}
}

// TODO: Support SSR streaming of Suspense.
const renderToString = concurrentFeatures
? (element: React.ReactElement) =>
new Promise<string>((resolve, reject) => {
const stream = new PassThrough()
const buffers: Buffer[] = []
stream.on('data', (chunk) => {
buffers.push(chunk)
})
stream.once('end', () => {
resolve(Buffer.concat(buffers).toString('utf-8'))
})

const {
abort,
startWriting,
} = (ReactDOMServer as any).pipeToNodeWritable(element, stream, {
onError(error: Error) {
abort()
reject(error)
},
onCompleteAll() {
startWriting()
},
})
})
: ReactDOMServer.renderToString

const renderPage: RenderPage = (
options: ComponentsEnhancer = {}
): { html: string; head: any } => {
): RenderPageResult | Promise<RenderPageResult> => {
if (ctx.err && ErrorDebug) {
return { html: renderToString(<ErrorDebug error={ctx.err} />), head }
const htmlOrPromise = renderToString(<ErrorDebug error={ctx.err} />)
return typeof htmlOrPromise === 'string'
? { html: htmlOrPromise, head }
: htmlOrPromise.then((html) => ({
html,
head,
}))
}

if (dev && (props.router || props.Component)) {
Expand All @@ -1009,13 +1049,17 @@ export async function renderToHTML(
Component: EnhancedComponent,
} = enhanceComponents(options, App, Component)

const html = renderToString(
const htmlOrPromise = renderToString(
<AppContainer>
<EnhancedApp Component={EnhancedComponent} router={router} {...props} />
</AppContainer>
)

return { html, head }
return typeof htmlOrPromise === 'string'
? { html: htmlOrPromise, head }
: htmlOrPromise.then((html) => ({
html,
head,
}))
}
const documentCtx = { ...ctx, renderPage }
const docProps: DocumentInitialProps = await loadGetInitialProps(
Expand Down Expand Up @@ -1049,8 +1093,6 @@ export async function renderToHTML(
const hybridAmp = ampState.hybrid

const docComponentsRendered: DocumentProps['docComponentsRendered'] = {}
const nextExport =
!isSSG && (renderOpts.nextExport || (dev && (isAutoExport || isFallback)))

let html = renderDocument(Document, {
...renderOpts,
Expand Down