Make concurrent features independent from the global runtime option (#35245)

This PR depends on #35242 and #35243. It allows the global runtime to be unset, as well as enables static optimization for Fizz and RSC pages in the Node.js runtime. Currently for the Edge runtime pages are still always SSR'd.

Closes #31317.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`


Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
This commit is contained in:
Shu Ding 2022-03-16 13:11:57 +01:00 committed by GitHub
parent 86c1bf6d2b
commit 853442dfc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 468 additions and 60 deletions

View file

@ -187,9 +187,6 @@ export async function getPageRuntime(
if (!pageRuntime) { if (!pageRuntime) {
if (isRuntimeRequired) { if (isRuntimeRequired) {
pageRuntime = globalRuntimeFallback pageRuntime = globalRuntimeFallback
} else {
// @TODO: Remove this branch to fully implement the RFC.
pageRuntime = globalRuntimeFallback
} }
} }

View file

@ -76,7 +76,11 @@ import {
} from '../telemetry/events' } from '../telemetry/events'
import { Telemetry } from '../telemetry/storage' import { Telemetry } from '../telemetry/storage'
import { CompilerResult, runCompiler } from './compiler' import { CompilerResult, runCompiler } from './compiler'
import { createEntrypoints, createPagesMapping } from './entries' import {
createEntrypoints,
createPagesMapping,
getPageRuntime,
} from './entries'
import { generateBuildId } from './generate-build-id' import { generateBuildId } from './generate-build-id'
import { isWriteable } from './is-writeable' import { isWriteable } from './is-writeable'
import * as Log from './output/log' import * as Log from './output/log'
@ -153,11 +157,10 @@ export default async function build(
setGlobal('phase', PHASE_PRODUCTION_BUILD) setGlobal('phase', PHASE_PRODUCTION_BUILD)
setGlobal('distDir', distDir) setGlobal('distDir', distDir)
// Currently, when the runtime option is set (either `nodejs` or `edge`), // We enable concurrent features (Fizz-related rendering architecture) when
// we enable concurrent features (Fizz-related rendering architecture). // using React 18 or experimental.
const runtime = config.experimental.runtime
const hasReactRoot = shouldUseReactRoot() const hasReactRoot = shouldUseReactRoot()
const hasConcurrentFeatures = !!runtime const hasConcurrentFeatures = hasReactRoot
const hasServerComponents = const hasServerComponents =
hasReactRoot && !!config.experimental.serverComponents hasReactRoot && !!config.experimental.serverComponents
@ -622,6 +625,7 @@ export default async function build(
entrypoints: entrypoints.client, entrypoints: entrypoints.client,
rewrites, rewrites,
runWebpackSpan, runWebpackSpan,
hasReactRoot,
}), }),
getBaseWebpackConfig(dir, { getBaseWebpackConfig(dir, {
buildId, buildId,
@ -633,6 +637,7 @@ export default async function build(
entrypoints: entrypoints.server, entrypoints: entrypoints.server,
rewrites, rewrites,
runWebpackSpan, runWebpackSpan,
hasReactRoot,
}), }),
hasReactRoot hasReactRoot
? getBaseWebpackConfig(dir, { ? getBaseWebpackConfig(dir, {
@ -646,6 +651,7 @@ export default async function build(
entrypoints: entrypoints.edgeServer, entrypoints: entrypoints.edgeServer,
rewrites, rewrites,
runWebpackSpan, runWebpackSpan,
hasReactRoot,
}) })
: null, : null,
]) ])
@ -954,10 +960,22 @@ export default async function build(
let ssgPageRoutes: string[] | null = null let ssgPageRoutes: string[] | null = null
let isMiddlewareRoute = !!page.match(MIDDLEWARE_ROUTE) let isMiddlewareRoute = !!page.match(MIDDLEWARE_ROUTE)
const pagePath = pagePaths.find((_path) =>
_path.startsWith(actualPage + '.')
)
const pageRuntime =
hasConcurrentFeatures && pagePath
? await getPageRuntime(
join(pagesDir, pagePath),
config.experimental.runtime
)
: null
if ( if (
!isMiddlewareRoute && !isMiddlewareRoute &&
!isReservedPage(page) && !isReservedPage(page) &&
!hasConcurrentFeatures // We currently don't support staic optimization in the Edge runtime.
pageRuntime !== 'edge'
) { ) {
try { try {
let isPageStaticSpan = let isPageStaticSpan =
@ -1483,10 +1501,7 @@ export default async function build(
const combinedPages = [...staticPages, ...ssgPages] const combinedPages = [...staticPages, ...ssgPages]
if ( if (combinedPages.length > 0 || useStatic404 || useDefaultStatic500) {
!hasConcurrentFeatures &&
(combinedPages.length > 0 || useStatic404 || useDefaultStatic500)
) {
const staticGenerationSpan = nextBuildSpan.traceChild('static-generation') const staticGenerationSpan = nextBuildSpan.traceChild('static-generation')
await staticGenerationSpan.traceAsyncFn(async () => { await staticGenerationSpan.traceAsyncFn(async () => {
detectConflictingPaths( detectConflictingPaths(

View file

@ -48,7 +48,6 @@ import type { Span } from '../trace'
import { getRawPageExtensions } from './utils' import { getRawPageExtensions } from './utils'
import browserslist from 'next/dist/compiled/browserslist' import browserslist from 'next/dist/compiled/browserslist'
import loadJsConfig from './load-jsconfig' import loadJsConfig from './load-jsconfig'
import { shouldUseReactRoot } from '../server/config'
import { getMiddlewareSourceMapPlugins } from './webpack/plugins/middleware-source-maps-plugin' import { getMiddlewareSourceMapPlugins } from './webpack/plugins/middleware-source-maps-plugin'
const watchOptions = Object.freeze({ const watchOptions = Object.freeze({
@ -310,6 +309,7 @@ export default async function getBaseWebpackConfig(
rewrites, rewrites,
isDevFallback = false, isDevFallback = false,
runWebpackSpan, runWebpackSpan,
hasReactRoot,
}: { }: {
buildId: string buildId: string
config: NextConfigComplete config: NextConfigComplete
@ -323,6 +323,7 @@ export default async function getBaseWebpackConfig(
rewrites: CustomRoutes['rewrites'] rewrites: CustomRoutes['rewrites']
isDevFallback?: boolean isDevFallback?: boolean
runWebpackSpan: Span runWebpackSpan: Span
hasReactRoot: boolean
} }
): Promise<webpack.Configuration> { ): Promise<webpack.Configuration> {
const { useTypeScript, jsConfig, resolvedBaseUrl } = await loadJsConfig( const { useTypeScript, jsConfig, resolvedBaseUrl } = await loadJsConfig(
@ -335,10 +336,10 @@ export default async function getBaseWebpackConfig(
rewrites.afterFiles.length > 0 || rewrites.afterFiles.length > 0 ||
rewrites.fallback.length > 0 rewrites.fallback.length > 0
const hasReactRefresh: boolean = dev && !isServer const hasReactRefresh: boolean = dev && !isServer
const hasReactRoot = shouldUseReactRoot()
const runtime = config.experimental.runtime const runtime = config.experimental.runtime
// Make sure reactRoot is enabled when react 18 is detected // Make sure `reactRoot` is enabled when React 18 or experimental is detected.
if (hasReactRoot) { if (hasReactRoot) {
config.experimental.reactRoot = true config.experimental.reactRoot = true
} }
@ -353,14 +354,14 @@ export default async function getBaseWebpackConfig(
'`experimental.runtime` requires `experimental.reactRoot` to be enabled along with React 18.' '`experimental.runtime` requires `experimental.reactRoot` to be enabled along with React 18.'
) )
} }
if (config.experimental.serverComponents && !runtime) { if (config.experimental.serverComponents && !hasReactRoot) {
throw new Error( throw new Error(
'`experimental.runtime` is required to be set along with `experimental.serverComponents`.' '`experimental.serverComponents` requires React 18 to be installed.'
) )
} }
const targetWeb = isEdgeRuntime || !isServer const targetWeb = isEdgeRuntime || !isServer
const hasConcurrentFeatures = !!runtime && hasReactRoot const hasConcurrentFeatures = hasReactRoot
const hasServerComponents = const hasServerComponents =
hasConcurrentFeatures && !!config.experimental.serverComponents hasConcurrentFeatures && !!config.experimental.serverComponents
const disableOptimizedLoading = hasConcurrentFeatures const disableOptimizedLoading = hasConcurrentFeatures

View file

@ -588,6 +588,7 @@ export default async function exportApp(
nextConfig.experimental.disableOptimizedLoading, nextConfig.experimental.disableOptimizedLoading,
parentSpanId: pageExportSpan.id, parentSpanId: pageExportSpan.id,
httpAgentOptions: nextConfig.httpAgentOptions, httpAgentOptions: nextConfig.httpAgentOptions,
serverComponents: nextConfig.experimental.serverComponents,
}) })
for (const validation of result.ampValidations || []) { for (const validation of result.ampValidations || []) {

View file

@ -59,6 +59,7 @@ interface ExportPageInput {
disableOptimizedLoading: any disableOptimizedLoading: any
parentSpanId: any parentSpanId: any
httpAgentOptions: NextConfigComplete['httpAgentOptions'] httpAgentOptions: NextConfigComplete['httpAgentOptions']
serverComponents?: boolean
} }
interface ExportPageResults { interface ExportPageResults {
@ -106,6 +107,7 @@ export default async function exportPage({
optimizeCss, optimizeCss,
disableOptimizedLoading, disableOptimizedLoading,
httpAgentOptions, httpAgentOptions,
serverComponents,
}: ExportPageInput): Promise<ExportPageResults> { }: ExportPageInput): Promise<ExportPageResults> {
setHttpAgentOptions(httpAgentOptions) setHttpAgentOptions(httpAgentOptions)
const exportPageSpan = trace('export-page-worker', parentSpanId) const exportPageSpan = trace('export-page-worker', parentSpanId)
@ -260,7 +262,7 @@ export default async function exportPage({
getServerSideProps, getServerSideProps,
getStaticProps, getStaticProps,
pageConfig, pageConfig,
} = await loadComponents(distDir, page, serverless) } = await loadComponents(distDir, page, serverless, serverComponents)
const ampState = { const ampState = {
ampFirst: pageConfig?.amp === true, ampFirst: pageConfig?.amp === true,
hasQuery: Boolean(query.amp), hasQuery: Boolean(query.amp),
@ -321,7 +323,12 @@ export default async function exportPage({
throw new Error(`Failed to render serverless page`) throw new Error(`Failed to render serverless page`)
} }
} else { } else {
const components = await loadComponents(distDir, page, serverless) const components = await loadComponents(
distDir,
page,
serverless,
serverComponents
)
const ampState = { const ampState = {
ampFirst: components.pageConfig?.amp === true, ampFirst: components.pageConfig?.amp === true,
hasQuery: Boolean(query.amp), hasQuery: Boolean(query.amp),

View file

@ -585,11 +585,9 @@ export class Head extends Component<
disableOptimizedLoading, disableOptimizedLoading,
optimizeCss, optimizeCss,
optimizeFonts, optimizeFonts,
runtime, hasConcurrentFeatures,
} = this.context } = this.context
const hasConcurrentFeatures = !!runtime
const disableRuntimeJS = unstable_runtimeJS === false const disableRuntimeJS = unstable_runtimeJS === false
const disableJsPreload = const disableJsPreload =
unstable_JsPreload === false || !disableOptimizedLoading unstable_JsPreload === false || !disableOptimizedLoading

View file

@ -154,6 +154,7 @@ export default class HotReloader {
private config: NextConfigComplete private config: NextConfigComplete
private runtime?: 'nodejs' | 'edge' private runtime?: 'nodejs' | 'edge'
private hasServerComponents: boolean private hasServerComponents: boolean
private hasReactRoot: boolean
public clientStats: webpack5.Stats | null public clientStats: webpack5.Stats | null
public serverStats: webpack5.Stats | null public serverStats: webpack5.Stats | null
private clientError: Error | null = null private clientError: Error | null = null
@ -197,7 +198,9 @@ export default class HotReloader {
this.config = config this.config = config
this.runtime = config.experimental.runtime this.runtime = config.experimental.runtime
this.hasServerComponents = !!config.experimental.serverComponents this.hasReactRoot = shouldUseReactRoot()
this.hasServerComponents =
this.hasReactRoot && !!config.experimental.serverComponents
this.previewProps = previewProps this.previewProps = previewProps
this.rewrites = rewrites this.rewrites = rewrites
this.hotReloaderSpan = trace('hot-reloader', undefined, { this.hotReloaderSpan = trace('hot-reloader', undefined, {
@ -340,8 +343,6 @@ export default class HotReloader {
) )
) )
const hasReactRoot = shouldUseReactRoot()
return webpackConfigSpan return webpackConfigSpan
.traceChild('generate-webpack-config') .traceChild('generate-webpack-config')
.traceAsyncFn(() => .traceAsyncFn(() =>
@ -356,6 +357,7 @@ export default class HotReloader {
rewrites: this.rewrites, rewrites: this.rewrites,
entrypoints: entrypoints.client, entrypoints: entrypoints.client,
runWebpackSpan: this.hotReloaderSpan, runWebpackSpan: this.hotReloaderSpan,
hasReactRoot: this.hasReactRoot,
}), }),
getBaseWebpackConfig(this.dir, { getBaseWebpackConfig(this.dir, {
dev: true, dev: true,
@ -366,9 +368,10 @@ export default class HotReloader {
rewrites: this.rewrites, rewrites: this.rewrites,
entrypoints: entrypoints.server, entrypoints: entrypoints.server,
runWebpackSpan: this.hotReloaderSpan, runWebpackSpan: this.hotReloaderSpan,
hasReactRoot: this.hasReactRoot,
}), }),
// The edge runtime is only supported with React root. // The edge runtime is only supported with React root.
hasReactRoot this.hasReactRoot
? getBaseWebpackConfig(this.dir, { ? getBaseWebpackConfig(this.dir, {
dev: true, dev: true,
isServer: true, isServer: true,
@ -379,6 +382,7 @@ export default class HotReloader {
rewrites: this.rewrites, rewrites: this.rewrites,
entrypoints: entrypoints.edgeServer, entrypoints: entrypoints.edgeServer,
runWebpackSpan: this.hotReloaderSpan, runWebpackSpan: this.hotReloaderSpan,
hasReactRoot: this.hasReactRoot,
}) })
: null, : null,
].filter(Boolean) as webpack.Configuration[] ].filter(Boolean) as webpack.Configuration[]
@ -417,6 +421,7 @@ export default class HotReloader {
this.pagesDir this.pagesDir
) )
).client, ).client,
hasReactRoot: this.hasReactRoot,
}) })
const fallbackCompiler = webpack(fallbackConfig) const fallbackCompiler = webpack(fallbackConfig)

View file

@ -6,6 +6,7 @@ import type {
import { import {
BUILD_MANIFEST, BUILD_MANIFEST,
REACT_LOADABLE_MANIFEST, REACT_LOADABLE_MANIFEST,
MIDDLEWARE_FLIGHT_MANIFEST,
} from '../shared/lib/constants' } from '../shared/lib/constants'
import { join } from 'path' import { join } from 'path'
import { requirePage } from './require' import { requirePage } from './require'
@ -30,6 +31,7 @@ export type LoadComponentsReturnType = {
pageConfig: PageConfig pageConfig: PageConfig
buildManifest: BuildManifest buildManifest: BuildManifest
reactLoadableManifest: ReactLoadableManifest reactLoadableManifest: ReactLoadableManifest
serverComponentManifest?: any | null
Document: DocumentType Document: DocumentType
App: AppType App: AppType
getStaticProps?: GetStaticProps getStaticProps?: GetStaticProps
@ -61,7 +63,8 @@ export async function loadDefaultErrorComponents(distDir: string) {
export async function loadComponents( export async function loadComponents(
distDir: string, distDir: string,
pathname: string, pathname: string,
serverless: boolean serverless: boolean,
serverComponents?: boolean
): Promise<LoadComponentsReturnType> { ): Promise<LoadComponentsReturnType> {
if (serverless) { if (serverless) {
const ComponentMod = await requirePage(pathname, distDir, serverless) const ComponentMod = await requirePage(pathname, distDir, serverless)
@ -102,9 +105,13 @@ export async function loadComponents(
requirePage(pathname, distDir, serverless), requirePage(pathname, distDir, serverless),
]) ])
const [buildManifest, reactLoadableManifest] = await Promise.all([ const [buildManifest, reactLoadableManifest, serverComponentManifest] =
await Promise.all([
require(join(distDir, BUILD_MANIFEST)), require(join(distDir, BUILD_MANIFEST)),
require(join(distDir, REACT_LOADABLE_MANIFEST)), require(join(distDir, REACT_LOADABLE_MANIFEST)),
serverComponents
? require(join(distDir, 'server', MIDDLEWARE_FLIGHT_MANIFEST + '.json'))
: null,
]) ])
const Component = interopDefault(ComponentMod) const Component = interopDefault(ComponentMod)
@ -125,5 +132,6 @@ export async function loadComponents(
getServerSideProps, getServerSideProps,
getStaticProps, getStaticProps,
getStaticPaths, getStaticPaths,
serverComponentManifest,
} }
} }

View file

@ -693,7 +693,7 @@ export default class NextNodeServer extends BaseServer {
} }
protected getServerComponentManifest() { protected getServerComponentManifest() {
if (!this.nextConfig.experimental.runtime) return undefined if (!this.nextConfig.experimental.serverComponents) return undefined
return require(join( return require(join(
this.distDir, this.distDir,
'server', 'server',

View file

@ -450,12 +450,12 @@ export async function renderToHTML(
supportsDynamicHTML, supportsDynamicHTML,
images, images,
reactRoot, reactRoot,
runtime, runtime: globalRuntime,
ComponentMod, ComponentMod,
AppMod, AppMod,
} = renderOpts } = renderOpts
const hasConcurrentFeatures = !!runtime const hasConcurrentFeatures = reactRoot
let Document = renderOpts.Document let Document = renderOpts.Document
const OriginalComponent = renderOpts.Component const OriginalComponent = renderOpts.Component
@ -464,7 +464,7 @@ export async function renderToHTML(
const isServerComponent = const isServerComponent =
!!serverComponentManifest && !!serverComponentManifest &&
hasConcurrentFeatures && hasConcurrentFeatures &&
ComponentMod.__next_rsc__ !!ComponentMod.__next_rsc__
let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) = let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) =
renderOpts.Component renderOpts.Component
@ -1243,7 +1243,7 @@ export async function renderToHTML(
| typeof Document | typeof Document
| undefined | undefined
if (runtime === 'edge' && Document.getInitialProps) { if (process.browser && Document.getInitialProps) {
// In the Edge runtime, `Document.getInitialProps` isn't supported. // In the Edge runtime, `Document.getInitialProps` isn't supported.
// We throw an error here if it's customized. // We throw an error here if it's customized.
if (!builtinDocument) { if (!builtinDocument) {
@ -1329,7 +1329,8 @@ export async function renderToHTML(
) : ( ) : (
<Body> <Body>
<AppContainerWithIsomorphicFiberStructure> <AppContainerWithIsomorphicFiberStructure>
{renderOpts.serverComponents && AppMod.__next_rsc__ ? ( {isServerComponent && AppMod.__next_rsc__ ? (
// _app.server.js is used.
<Component {...props.pageProps} router={router} /> <Component {...props.pageProps} router={router} />
) : ( ) : (
<App {...props} Component={Component} router={router} /> <App {...props} Component={Component} router={router} />
@ -1361,7 +1362,6 @@ export async function renderToHTML(
), ),
generateStaticHTML: true, generateStaticHTML: true,
}) })
const flushed = await streamToString(flushEffectStream) const flushed = await streamToString(flushEffectStream)
return flushed return flushed
} }
@ -1489,7 +1489,8 @@ export async function renderToHTML(
optimizeCss: renderOpts.optimizeCss, optimizeCss: renderOpts.optimizeCss,
optimizeFonts: renderOpts.optimizeFonts, optimizeFonts: renderOpts.optimizeFonts,
nextScriptWorkers: renderOpts.nextScriptWorkers, nextScriptWorkers: renderOpts.nextScriptWorkers,
runtime, runtime: globalRuntime,
hasConcurrentFeatures,
} }
const document = ( const document = (

View file

@ -38,6 +38,7 @@ export type HtmlProps = {
optimizeFonts?: boolean optimizeFonts?: boolean
nextScriptWorkers?: boolean nextScriptWorkers?: boolean
runtime?: 'edge' | 'nodejs' runtime?: 'edge' | 'nodejs'
hasConcurrentFeatures?: boolean
} }
export const HtmlContext = createContext<HtmlProps>(null as any) export const HtmlContext = createContext<HtmlProps>(null as any)

View file

@ -25,19 +25,6 @@ describe('Invalid react 18 webpack config', () => {
) )
}) })
it('should require `experimental.runtime` for server components', async () => {
writeNextConfig({
reactRoot: true,
serverComponents: true,
})
const { stderr } = await nextBuild(appDir, [], { stderr: true })
nextConfig.restore()
expect(stderr).toContain(
'`experimental.runtime` is required to be set along with `experimental.serverComponents`.'
)
})
it('should warn user when not using react 18 and `experimental.reactRoot` is enabled', async () => { it('should warn user when not using react 18 and `experimental.reactRoot` is enabled', async () => {
const reactDomPackagePah = join(appDir, 'node_modules/react-dom') const reactDomPackagePah = join(appDir, 'node_modules/react-dom')
await fs.mkdirp(reactDomPackagePah) await fs.mkdirp(reactDomPackagePah)

View file

@ -2,6 +2,7 @@ import { Suspense } from 'react'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
const Bar = dynamic(() => import('../../components/bar'), { const Bar = dynamic(() => import('../../components/bar'), {
ssr: false,
suspense: true, suspense: true,
// Explicitly declare loaded modules. // Explicitly declare loaded modules.
// For suspense cases, they'll be ignored. // For suspense cases, they'll be ignored.
@ -14,7 +15,7 @@ const Bar = dynamic(() => import('../../components/bar'), {
export default function NoPreload() { export default function NoPreload() {
return ( return (
<Suspense fallback={'rab'}> <Suspense fallback={'fallback'}>
<Bar /> <Bar />
</Suspense> </Suspense>
) )

View file

@ -31,7 +31,7 @@ export default (context) => {
const nextData = JSON.parse($('#__NEXT_DATA__').text()) const nextData = JSON.parse($('#__NEXT_DATA__').text())
const content = $('#__next').text() const content = $('#__next').text()
// <Bar> is suspended // <Bar> is suspended
expect(content).toBe('rab') expect(content).toBe('fallback')
expect(nextData.dynamicIds).toBeUndefined() expect(nextData.dynamicIds).toBeUndefined()
}) })

View file

@ -8,15 +8,16 @@ export default (context, render) => {
return cheerio.load(html) return cheerio.load(html)
} }
it('should render fallback on server side if suspense without preload', async () => { it('should render fallback on server side if suspense without ssr', async () => {
const $ = await get$('/suspense/no-preload') const $ = await get$('/suspense/no-preload')
const nextData = JSON.parse($('#__NEXT_DATA__').text()) const nextData = JSON.parse($('#__NEXT_DATA__').text())
const content = $('#__next').text() const content = $('#__next').text()
expect(content).toBe('rab') expect(content).toBe('fallback')
expect(nextData.dynamicIds).toBeUndefined() expect(nextData.dynamicIds).toBeUndefined()
}) })
it('should render fallback on server side if suspended on server with preload', async () => { // Testing the same thing as above.
it.skip('should render import fallback on server side if suspended without ssr', async () => {
const $ = await get$('/suspense/thrown') const $ = await get$('/suspense/thrown')
const html = $('body').html() const html = $('body').html()
expect(html).toContain('loading') expect(html).toContain('loading')

View file

@ -5,3 +5,7 @@ export default function MyError() {
throw new Error('oops') throw new Error('oops')
} }
} }
export const config = {
runtime: 'edge',
}

View file

@ -18,3 +18,7 @@ export default function page() {
</> </>
) )
} }
export const config = {
runtime: 'edge',
}

View file

@ -7,3 +7,7 @@ const Page = () => {
} }
export default Page export default Page
export const config = {
runtime: 'edge',
}

View file

@ -15,3 +15,7 @@ export default function LinkPage({ router }) {
</> </>
) )
} }
export const config = {
runtime: 'edge',
}

View file

@ -40,3 +40,7 @@ export default function () {
</> </>
) )
} }
export const config = {
runtime: 'edge',
}

View file

@ -1,3 +1,7 @@
export default function Pid({ router }) { export default function Pid({ router }) {
return <div>{`query: ${router.query.dynamic}`}</div> return <div>{`query: ${router.query.dynamic}`}</div>
} }
export const config = {
runtime: 'edge',
}

View file

@ -21,3 +21,7 @@ export default function Page() {
</Suspense> </Suspense>
) )
} }
export const config = {
runtime: 'edge',
}

View file

@ -21,3 +21,7 @@ export default function Page() {
</Suspense> </Suspense>
) )
} }
export const config = {
runtime: 'edge',
}

View file

@ -0,0 +1,9 @@
const withReact18 = require('../../react-18/test/with-react-18')
module.exports = withReact18({
reactStrictMode: true,
experimental: {
serverComponents: true,
// runtime: 'edge',
},
})

View file

@ -0,0 +1,9 @@
{
"private": true,
"scripts": {
"lnext": "node -r ../../react-18/test/require-hook.js ../../../../packages/next/dist/bin/next",
"dev": "yarn lnext dev",
"build": "yarn lnext build",
"start": "yarn lnext start"
}
}

View file

@ -0,0 +1,9 @@
{
"/_app": "pages/_app.js",
"/_error": "pages/_error.js",
"/edge-rsc": "pages/edge-rsc.js",
"/static": "pages/static.js",
"/node-rsc": "pages/node-rsc.js",
"/node": "pages/node.js",
"/edge": "pages/edge.js"
}

View file

@ -0,0 +1,18 @@
import getRuntime from '../utils/runtime'
import getTime from '../utils/time'
export default function Page() {
return (
<div>
This is a SSR RSC page.
<br />
{'Runtime: ' + getRuntime()}
<br />
{'Time: ' + getTime()}
</div>
)
}
export const config = {
runtime: 'edge',
}

View file

@ -0,0 +1,18 @@
import getRuntime from '../utils/runtime'
import getTime from '../utils/time'
export default function Page() {
return (
<div>
This is a SSR page.
<br />
{'Runtime: ' + getRuntime()}
<br />
{'Time: ' + getTime()}
</div>
)
}
export const config = {
runtime: 'edge',
}

View file

@ -0,0 +1,26 @@
import getRuntime from '../utils/runtime'
import getTime from '../utils/time'
export default function Page({ type }) {
return (
<div>
This is a {type} RSC page.
<br />
{'Runtime: ' + getRuntime()}
<br />
{'Time: ' + getTime()}
</div>
)
}
export function getStaticProps() {
return {
props: {
type: 'SSG',
},
}
}
export const config = {
runtime: 'nodejs',
}

View file

@ -0,0 +1,26 @@
import getRuntime from '../utils/runtime'
import getTime from '../utils/time'
export default function Page({ type }) {
return (
<div>
This is a {type} RSC page.
<br />
{'Runtime: ' + getRuntime()}
<br />
{'Time: ' + getTime()}
</div>
)
}
export function getServerSideProps() {
return {
props: {
type: 'SSR',
},
}
}
export const config = {
runtime: 'nodejs',
}

View file

@ -0,0 +1,18 @@
import getRuntime from '../utils/runtime'
import getTime from '../utils/time'
export default function Page() {
return (
<div>
This is a static RSC page.
<br />
{'Runtime: ' + getRuntime()}
<br />
{'Time: ' + getTime()}
</div>
)
}
export const config = {
runtime: 'nodejs',
}

View file

@ -0,0 +1,26 @@
import getRuntime from '../utils/runtime'
import getTime from '../utils/time'
export default function Page({ type }) {
return (
<div>
This is a {type} page.
<br />
{'Runtime: ' + getRuntime()}
<br />
{'Time: ' + getTime()}
</div>
)
}
export function getStaticProps() {
return {
props: {
type: 'SSG',
},
}
}
export const config = {
runtime: 'nodejs',
}

View file

@ -0,0 +1,26 @@
import getRuntime from '../utils/runtime'
import getTime from '../utils/time'
export default function Page({ type }) {
return (
<div>
This is a {type} page.
<br />
{'Runtime: ' + getRuntime()}
<br />
{'Time: ' + getTime()}
</div>
)
}
export function getServerSideProps() {
return {
props: {
type: 'SSR',
},
}
}
export const config = {
runtime: 'nodejs',
}

View file

@ -0,0 +1,18 @@
import getRuntime from '../utils/runtime'
import getTime from '../utils/time'
export default function Page() {
return (
<div>
This is a static page.
<br />
{'Runtime: ' + getRuntime()}
<br />
{'Time: ' + getTime()}
</div>
)
}
export const config = {
runtime: 'nodejs',
}

View file

@ -0,0 +1,14 @@
import getRuntime from '../utils/runtime'
import getTime from '../utils/time'
export default function Page() {
return (
<div>
This is a static page.
<br />
{'Runtime: ' + getRuntime()}
<br />
{'Time: ' + getTime()}
</div>
)
}

View file

@ -0,0 +1,3 @@
export default function getRuntime() {
return process.version ? `Node.js ${process.version}` : 'Edge/Browser'
}

View file

@ -0,0 +1,3 @@
export default function getTime() {
return Date.now()
}

View file

@ -0,0 +1,127 @@
/* eslint-env jest */
import { join } from 'path'
import {
// File,
nextBuild as _nextBuild,
nextStart as _nextStart,
} from 'next-test-utils'
import { findPort, killApp, renderViaHTTP } from 'next-test-utils'
const nodeArgs = ['-r', join(__dirname, '../../react-18/test/require-hook.js')]
const appDir = join(__dirname, '../switchable-runtime')
// const nextConfig = new File(join(appDir, 'next.config.js'))
async function nextBuild(dir, options) {
return await _nextBuild(dir, [], {
...options,
stdout: true,
stderr: true,
nodeArgs,
})
}
async function nextStart(dir, port) {
return await _nextStart(dir, port, {
stdout: true,
stderr: true,
nodeArgs,
})
}
async function testRoute(appPort, url, { isStatic, isEdge }) {
const html1 = await renderViaHTTP(appPort, url)
const renderedAt1 = +html1.match(/Time: (\d+)/)[1]
expect(html1).toContain(`Runtime: ${isEdge ? 'Edge' : 'Node.js'}`)
const html2 = await renderViaHTTP(appPort, url)
const renderedAt2 = +html2.match(/Time: (\d+)/)[1]
expect(html2).toContain(`Runtime: ${isEdge ? 'Edge' : 'Node.js'}`)
if (isStatic) {
// Should not be re-rendered, some timestamp should be returned.
expect(renderedAt1).toBe(renderedAt2)
} else {
// Should be re-rendered.
expect(renderedAt1).toBeLessThan(renderedAt2)
}
}
describe('Without global runtime configuration', () => {
const context = { appDir }
beforeAll(async () => {
context.appPort = await findPort()
const { stderr } = await nextBuild(context.appDir)
context.stderr = stderr
context.server = await nextStart(context.appDir, context.appPort)
})
afterAll(async () => {
await killApp(context.server)
})
it('should build /static as a static page with the nodejs runtime', async () => {
await testRoute(context.appPort, '/static', {
isStatic: true,
isEdge: false,
})
})
it('should build /node as a static page with the nodejs runtime', async () => {
await testRoute(context.appPort, '/node', {
isStatic: true,
isEdge: false,
})
})
it('should build /node-ssr as a dynamic page with the nodejs runtime', async () => {
await testRoute(context.appPort, '/node-ssr', {
isStatic: false,
isEdge: false,
})
})
it('should build /node-ssg as a static page with the nodejs runtime', async () => {
await testRoute(context.appPort, '/node-ssg', {
isStatic: true,
isEdge: false,
})
})
it('should build /node-rsc as a static page with the nodejs runtime', async () => {
await testRoute(context.appPort, '/node-rsc', {
isStatic: true,
isEdge: false,
})
})
it('should build /node-rsc-ssr as a dynamic page with the nodejs runtime', async () => {
await testRoute(context.appPort, '/node-rsc-ssr', {
isStatic: false,
isEdge: false,
})
})
it('should build /node-rsc-ssg as a static page with the nodejs runtime', async () => {
await testRoute(context.appPort, '/node-rsc-ssg', {
isStatic: true,
isEdge: false,
})
})
it('should build /edge as a dynamic page with the edge runtime', async () => {
await testRoute(context.appPort, '/edge', {
isStatic: false,
isEdge: true,
})
})
it('should build /edge-rsc as a dynamic page with the edge runtime', async () => {
await testRoute(context.appPort, '/edge-rsc', {
isStatic: false,
isEdge: true,
})
})
})

View file

@ -3,7 +3,6 @@ const withReact18 = require('../../react-18/test/with-react-18')
module.exports = withReact18({ module.exports = withReact18({
experimental: { experimental: {
reactRoot: true, reactRoot: true,
runtime: 'edge',
serverComponents: true, serverComponents: true,
}, },
}) })

View file

@ -8,3 +8,7 @@ export default function Index() {
console.log(EOF) console.log(EOF)
return 'Access Node.js native module dns' return 'Access Node.js native module dns'
} }
export const config = {
runtime: 'edge',
}