From 853442dfc36e37a6b1de543fef12a303364be29b Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 16 Mar 2022 13:11:57 +0100 Subject: [PATCH] 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> --- packages/next/build/entries.ts | 3 - packages/next/build/index.ts | 35 +++-- packages/next/build/webpack-config.ts | 13 +- packages/next/export/index.ts | 1 + packages/next/export/worker.ts | 11 +- packages/next/pages/_document.tsx | 4 +- packages/next/server/dev/hot-reloader.ts | 13 +- packages/next/server/load-components.ts | 18 ++- packages/next/server/next-server.ts | 2 +- packages/next/server/render.tsx | 15 ++- packages/next/shared/lib/html-context.ts | 1 + .../react-18-invalid-config/index.test.js | 13 -- .../react-18/app/pages/suspense/no-preload.js | 3 +- test/integration/react-18/test/basics.js | 2 +- test/integration/react-18/test/blocking.js | 7 +- .../app/pages/err/render.js | 4 + .../app/pages/err/suspense.js | 4 + .../app/pages/next-api/image.server.js | 4 + .../app/pages/next-api/link.server.js | 4 + .../app/pages/partial-hydration.server.js | 4 + .../app/pages/routes/[dynamic].server.js | 4 + .../app/pages/streaming-rsc.server.js | 4 + .../app/pages/streaming.js | 4 + .../switchable-runtime/next.config.js | 9 ++ .../switchable-runtime/package.json | 9 ++ .../switchable-runtime/pages-manifest.json | 9 ++ .../pages/edge-rsc.server.js | 18 +++ .../switchable-runtime/pages/edge.js | 18 +++ .../pages/node-rsc-ssg.server.js | 26 ++++ .../pages/node-rsc-ssr.server.js | 26 ++++ .../pages/node-rsc.server.js | 18 +++ .../switchable-runtime/pages/node-ssg.js | 26 ++++ .../switchable-runtime/pages/node-ssr.js | 26 ++++ .../switchable-runtime/pages/node.js | 18 +++ .../switchable-runtime/pages/static.js | 14 ++ .../switchable-runtime/utils/runtime.js | 3 + .../switchable-runtime/utils/time.js | 3 + .../test/switchable-runtime.test.js | 127 ++++++++++++++++++ .../unsupported-native-module/next.config.js | 1 - .../unsupported-native-module/pages/index.js | 4 + 40 files changed, 468 insertions(+), 60 deletions(-) create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/next.config.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/package.json create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages-manifest.json create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge-rsc.server.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssg.server.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssr.server.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc.server.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssg.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssr.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/node.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/pages/static.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/utils/runtime.js create mode 100644 test/integration/react-streaming-and-server-components/switchable-runtime/utils/time.js create mode 100644 test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 304b7047b1490..c6a57df897a06 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -187,9 +187,6 @@ export async function getPageRuntime( if (!pageRuntime) { if (isRuntimeRequired) { pageRuntime = globalRuntimeFallback - } else { - // @TODO: Remove this branch to fully implement the RFC. - pageRuntime = globalRuntimeFallback } } diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index e1176656fe08d..3e0584d68b439 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -76,7 +76,11 @@ import { } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' import { CompilerResult, runCompiler } from './compiler' -import { createEntrypoints, createPagesMapping } from './entries' +import { + createEntrypoints, + createPagesMapping, + getPageRuntime, +} from './entries' import { generateBuildId } from './generate-build-id' import { isWriteable } from './is-writeable' import * as Log from './output/log' @@ -153,11 +157,10 @@ export default async function build( setGlobal('phase', PHASE_PRODUCTION_BUILD) setGlobal('distDir', distDir) - // Currently, when the runtime option is set (either `nodejs` or `edge`), - // we enable concurrent features (Fizz-related rendering architecture). - const runtime = config.experimental.runtime + // We enable concurrent features (Fizz-related rendering architecture) when + // using React 18 or experimental. const hasReactRoot = shouldUseReactRoot() - const hasConcurrentFeatures = !!runtime + const hasConcurrentFeatures = hasReactRoot const hasServerComponents = hasReactRoot && !!config.experimental.serverComponents @@ -622,6 +625,7 @@ export default async function build( entrypoints: entrypoints.client, rewrites, runWebpackSpan, + hasReactRoot, }), getBaseWebpackConfig(dir, { buildId, @@ -633,6 +637,7 @@ export default async function build( entrypoints: entrypoints.server, rewrites, runWebpackSpan, + hasReactRoot, }), hasReactRoot ? getBaseWebpackConfig(dir, { @@ -646,6 +651,7 @@ export default async function build( entrypoints: entrypoints.edgeServer, rewrites, runWebpackSpan, + hasReactRoot, }) : null, ]) @@ -954,10 +960,22 @@ export default async function build( let ssgPageRoutes: string[] | null = null 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 ( !isMiddlewareRoute && !isReservedPage(page) && - !hasConcurrentFeatures + // We currently don't support staic optimization in the Edge runtime. + pageRuntime !== 'edge' ) { try { let isPageStaticSpan = @@ -1483,10 +1501,7 @@ export default async function build( const combinedPages = [...staticPages, ...ssgPages] - if ( - !hasConcurrentFeatures && - (combinedPages.length > 0 || useStatic404 || useDefaultStatic500) - ) { + if (combinedPages.length > 0 || useStatic404 || useDefaultStatic500) { const staticGenerationSpan = nextBuildSpan.traceChild('static-generation') await staticGenerationSpan.traceAsyncFn(async () => { detectConflictingPaths( diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 0c3e64cbe1212..48d208912cb74 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -48,7 +48,6 @@ import type { Span } from '../trace' import { getRawPageExtensions } from './utils' import browserslist from 'next/dist/compiled/browserslist' import loadJsConfig from './load-jsconfig' -import { shouldUseReactRoot } from '../server/config' import { getMiddlewareSourceMapPlugins } from './webpack/plugins/middleware-source-maps-plugin' const watchOptions = Object.freeze({ @@ -310,6 +309,7 @@ export default async function getBaseWebpackConfig( rewrites, isDevFallback = false, runWebpackSpan, + hasReactRoot, }: { buildId: string config: NextConfigComplete @@ -323,6 +323,7 @@ export default async function getBaseWebpackConfig( rewrites: CustomRoutes['rewrites'] isDevFallback?: boolean runWebpackSpan: Span + hasReactRoot: boolean } ): Promise { const { useTypeScript, jsConfig, resolvedBaseUrl } = await loadJsConfig( @@ -335,10 +336,10 @@ export default async function getBaseWebpackConfig( rewrites.afterFiles.length > 0 || rewrites.fallback.length > 0 const hasReactRefresh: boolean = dev && !isServer - const hasReactRoot = shouldUseReactRoot() + 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) { 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.' ) } - if (config.experimental.serverComponents && !runtime) { + if (config.experimental.serverComponents && !hasReactRoot) { 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 hasConcurrentFeatures = !!runtime && hasReactRoot + const hasConcurrentFeatures = hasReactRoot const hasServerComponents = hasConcurrentFeatures && !!config.experimental.serverComponents const disableOptimizedLoading = hasConcurrentFeatures diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 937c01ab089cf..df995262048ee 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -588,6 +588,7 @@ export default async function exportApp( nextConfig.experimental.disableOptimizedLoading, parentSpanId: pageExportSpan.id, httpAgentOptions: nextConfig.httpAgentOptions, + serverComponents: nextConfig.experimental.serverComponents, }) for (const validation of result.ampValidations || []) { diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index f2e6dbb64f133..3679b28eebd1d 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -59,6 +59,7 @@ interface ExportPageInput { disableOptimizedLoading: any parentSpanId: any httpAgentOptions: NextConfigComplete['httpAgentOptions'] + serverComponents?: boolean } interface ExportPageResults { @@ -106,6 +107,7 @@ export default async function exportPage({ optimizeCss, disableOptimizedLoading, httpAgentOptions, + serverComponents, }: ExportPageInput): Promise { setHttpAgentOptions(httpAgentOptions) const exportPageSpan = trace('export-page-worker', parentSpanId) @@ -260,7 +262,7 @@ export default async function exportPage({ getServerSideProps, getStaticProps, pageConfig, - } = await loadComponents(distDir, page, serverless) + } = await loadComponents(distDir, page, serverless, serverComponents) const ampState = { ampFirst: pageConfig?.amp === true, hasQuery: Boolean(query.amp), @@ -321,7 +323,12 @@ export default async function exportPage({ throw new Error(`Failed to render serverless page`) } } else { - const components = await loadComponents(distDir, page, serverless) + const components = await loadComponents( + distDir, + page, + serverless, + serverComponents + ) const ampState = { ampFirst: components.pageConfig?.amp === true, hasQuery: Boolean(query.amp), diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 8ac37914b0026..72868292fa6ae 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -585,11 +585,9 @@ export class Head extends Component< disableOptimizedLoading, optimizeCss, optimizeFonts, - runtime, + hasConcurrentFeatures, } = this.context - const hasConcurrentFeatures = !!runtime - const disableRuntimeJS = unstable_runtimeJS === false const disableJsPreload = unstable_JsPreload === false || !disableOptimizedLoading diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index bcd58454ab578..b30f365b54ffc 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -154,6 +154,7 @@ export default class HotReloader { private config: NextConfigComplete private runtime?: 'nodejs' | 'edge' private hasServerComponents: boolean + private hasReactRoot: boolean public clientStats: webpack5.Stats | null public serverStats: webpack5.Stats | null private clientError: Error | null = null @@ -197,7 +198,9 @@ export default class HotReloader { this.config = config this.runtime = config.experimental.runtime - this.hasServerComponents = !!config.experimental.serverComponents + this.hasReactRoot = shouldUseReactRoot() + this.hasServerComponents = + this.hasReactRoot && !!config.experimental.serverComponents this.previewProps = previewProps this.rewrites = rewrites this.hotReloaderSpan = trace('hot-reloader', undefined, { @@ -340,8 +343,6 @@ export default class HotReloader { ) ) - const hasReactRoot = shouldUseReactRoot() - return webpackConfigSpan .traceChild('generate-webpack-config') .traceAsyncFn(() => @@ -356,6 +357,7 @@ export default class HotReloader { rewrites: this.rewrites, entrypoints: entrypoints.client, runWebpackSpan: this.hotReloaderSpan, + hasReactRoot: this.hasReactRoot, }), getBaseWebpackConfig(this.dir, { dev: true, @@ -366,9 +368,10 @@ export default class HotReloader { rewrites: this.rewrites, entrypoints: entrypoints.server, runWebpackSpan: this.hotReloaderSpan, + hasReactRoot: this.hasReactRoot, }), // The edge runtime is only supported with React root. - hasReactRoot + this.hasReactRoot ? getBaseWebpackConfig(this.dir, { dev: true, isServer: true, @@ -379,6 +382,7 @@ export default class HotReloader { rewrites: this.rewrites, entrypoints: entrypoints.edgeServer, runWebpackSpan: this.hotReloaderSpan, + hasReactRoot: this.hasReactRoot, }) : null, ].filter(Boolean) as webpack.Configuration[] @@ -417,6 +421,7 @@ export default class HotReloader { this.pagesDir ) ).client, + hasReactRoot: this.hasReactRoot, }) const fallbackCompiler = webpack(fallbackConfig) diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index ae247d9ea66fd..361f0183899d5 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -6,6 +6,7 @@ import type { import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, + MIDDLEWARE_FLIGHT_MANIFEST, } from '../shared/lib/constants' import { join } from 'path' import { requirePage } from './require' @@ -30,6 +31,7 @@ export type LoadComponentsReturnType = { pageConfig: PageConfig buildManifest: BuildManifest reactLoadableManifest: ReactLoadableManifest + serverComponentManifest?: any | null Document: DocumentType App: AppType getStaticProps?: GetStaticProps @@ -61,7 +63,8 @@ export async function loadDefaultErrorComponents(distDir: string) { export async function loadComponents( distDir: string, pathname: string, - serverless: boolean + serverless: boolean, + serverComponents?: boolean ): Promise { if (serverless) { const ComponentMod = await requirePage(pathname, distDir, serverless) @@ -102,10 +105,14 @@ export async function loadComponents( requirePage(pathname, distDir, serverless), ]) - const [buildManifest, reactLoadableManifest] = await Promise.all([ - require(join(distDir, BUILD_MANIFEST)), - require(join(distDir, REACT_LOADABLE_MANIFEST)), - ]) + const [buildManifest, reactLoadableManifest, serverComponentManifest] = + await Promise.all([ + require(join(distDir, BUILD_MANIFEST)), + require(join(distDir, REACT_LOADABLE_MANIFEST)), + serverComponents + ? require(join(distDir, 'server', MIDDLEWARE_FLIGHT_MANIFEST + '.json')) + : null, + ]) const Component = interopDefault(ComponentMod) const Document = interopDefault(DocumentMod) @@ -125,5 +132,6 @@ export async function loadComponents( getServerSideProps, getStaticProps, getStaticPaths, + serverComponentManifest, } } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 83a8d7e007875..2455112a313d7 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -693,7 +693,7 @@ export default class NextNodeServer extends BaseServer { } protected getServerComponentManifest() { - if (!this.nextConfig.experimental.runtime) return undefined + if (!this.nextConfig.experimental.serverComponents) return undefined return require(join( this.distDir, 'server', diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 7797d1d654047..190cfadc6b279 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -450,12 +450,12 @@ export async function renderToHTML( supportsDynamicHTML, images, reactRoot, - runtime, + runtime: globalRuntime, ComponentMod, AppMod, } = renderOpts - const hasConcurrentFeatures = !!runtime + const hasConcurrentFeatures = reactRoot let Document = renderOpts.Document const OriginalComponent = renderOpts.Component @@ -464,7 +464,7 @@ export async function renderToHTML( const isServerComponent = !!serverComponentManifest && hasConcurrentFeatures && - ComponentMod.__next_rsc__ + !!ComponentMod.__next_rsc__ let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) = renderOpts.Component @@ -1243,7 +1243,7 @@ export async function renderToHTML( | typeof Document | undefined - if (runtime === 'edge' && Document.getInitialProps) { + if (process.browser && Document.getInitialProps) { // In the Edge runtime, `Document.getInitialProps` isn't supported. // We throw an error here if it's customized. if (!builtinDocument) { @@ -1329,7 +1329,8 @@ export async function renderToHTML( ) : ( - {renderOpts.serverComponents && AppMod.__next_rsc__ ? ( + {isServerComponent && AppMod.__next_rsc__ ? ( + // _app.server.js is used. ) : ( @@ -1361,7 +1362,6 @@ export async function renderToHTML( ), generateStaticHTML: true, }) - const flushed = await streamToString(flushEffectStream) return flushed } @@ -1489,7 +1489,8 @@ export async function renderToHTML( optimizeCss: renderOpts.optimizeCss, optimizeFonts: renderOpts.optimizeFonts, nextScriptWorkers: renderOpts.nextScriptWorkers, - runtime, + runtime: globalRuntime, + hasConcurrentFeatures, } const document = ( diff --git a/packages/next/shared/lib/html-context.ts b/packages/next/shared/lib/html-context.ts index 0a4455715361e..b069fca0f49f9 100644 --- a/packages/next/shared/lib/html-context.ts +++ b/packages/next/shared/lib/html-context.ts @@ -38,6 +38,7 @@ export type HtmlProps = { optimizeFonts?: boolean nextScriptWorkers?: boolean runtime?: 'edge' | 'nodejs' + hasConcurrentFeatures?: boolean } export const HtmlContext = createContext(null as any) diff --git a/test/integration/react-18-invalid-config/index.test.js b/test/integration/react-18-invalid-config/index.test.js index e0096a0b35023..9372f0f199f4d 100644 --- a/test/integration/react-18-invalid-config/index.test.js +++ b/test/integration/react-18-invalid-config/index.test.js @@ -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 () => { const reactDomPackagePah = join(appDir, 'node_modules/react-dom') await fs.mkdirp(reactDomPackagePah) diff --git a/test/integration/react-18/app/pages/suspense/no-preload.js b/test/integration/react-18/app/pages/suspense/no-preload.js index aaf54e244118f..8ae8c7c599f2e 100644 --- a/test/integration/react-18/app/pages/suspense/no-preload.js +++ b/test/integration/react-18/app/pages/suspense/no-preload.js @@ -2,6 +2,7 @@ import { Suspense } from 'react' import dynamic from 'next/dynamic' const Bar = dynamic(() => import('../../components/bar'), { + ssr: false, suspense: true, // Explicitly declare loaded modules. // For suspense cases, they'll be ignored. @@ -14,7 +15,7 @@ const Bar = dynamic(() => import('../../components/bar'), { export default function NoPreload() { return ( - + ) diff --git a/test/integration/react-18/test/basics.js b/test/integration/react-18/test/basics.js index debbad85a56b5..bc63d2ad4f0f5 100644 --- a/test/integration/react-18/test/basics.js +++ b/test/integration/react-18/test/basics.js @@ -31,7 +31,7 @@ export default (context) => { const nextData = JSON.parse($('#__NEXT_DATA__').text()) const content = $('#__next').text() // is suspended - expect(content).toBe('rab') + expect(content).toBe('fallback') expect(nextData.dynamicIds).toBeUndefined() }) diff --git a/test/integration/react-18/test/blocking.js b/test/integration/react-18/test/blocking.js index c4feeb2e8a301..0db4aacd6cf97 100644 --- a/test/integration/react-18/test/blocking.js +++ b/test/integration/react-18/test/blocking.js @@ -8,15 +8,16 @@ export default (context, render) => { 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 nextData = JSON.parse($('#__NEXT_DATA__').text()) const content = $('#__next').text() - expect(content).toBe('rab') + expect(content).toBe('fallback') 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 html = $('body').html() expect(html).toContain('loading') diff --git a/test/integration/react-streaming-and-server-components/app/pages/err/render.js b/test/integration/react-streaming-and-server-components/app/pages/err/render.js index 0c6ec0bd1baed..7d9d321f7cb0a 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/err/render.js +++ b/test/integration/react-streaming-and-server-components/app/pages/err/render.js @@ -5,3 +5,7 @@ export default function MyError() { throw new Error('oops') } } + +export const config = { + runtime: 'edge', +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/err/suspense.js b/test/integration/react-streaming-and-server-components/app/pages/err/suspense.js index e23ccd094bbab..05f85b9f4b16f 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/err/suspense.js +++ b/test/integration/react-streaming-and-server-components/app/pages/err/suspense.js @@ -18,3 +18,7 @@ export default function page() { ) } + +export const config = { + runtime: 'edge', +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/next-api/image.server.js b/test/integration/react-streaming-and-server-components/app/pages/next-api/image.server.js index 0083c3b9d4ccb..96c50ace36a55 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/next-api/image.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/next-api/image.server.js @@ -7,3 +7,7 @@ const Page = () => { } export default Page + +export const config = { + runtime: 'edge', +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js b/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js index a0e988f015f2c..c5fdc1f1d40fc 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js @@ -15,3 +15,7 @@ export default function LinkPage({ router }) { ) } + +export const config = { + runtime: 'edge', +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js b/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js index c3740b8719fed..8604f3bdcb726 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js @@ -40,3 +40,7 @@ export default function () { ) } + +export const config = { + runtime: 'edge', +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/routes/[dynamic].server.js b/test/integration/react-streaming-and-server-components/app/pages/routes/[dynamic].server.js index 288d6165f9990..27c7c0bddf028 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/routes/[dynamic].server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/routes/[dynamic].server.js @@ -1,3 +1,7 @@ export default function Pid({ router }) { return
{`query: ${router.query.dynamic}`}
} + +export const config = { + runtime: 'edge', +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js b/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js index ac58f5e9e2c38..70aaf4ced70ef 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js @@ -21,3 +21,7 @@ export default function Page() {
) } + +export const config = { + runtime: 'edge', +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/streaming.js b/test/integration/react-streaming-and-server-components/app/pages/streaming.js index ac58f5e9e2c38..70aaf4ced70ef 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/streaming.js +++ b/test/integration/react-streaming-and-server-components/app/pages/streaming.js @@ -21,3 +21,7 @@ export default function Page() { ) } + +export const config = { + runtime: 'edge', +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/next.config.js b/test/integration/react-streaming-and-server-components/switchable-runtime/next.config.js new file mode 100644 index 0000000000000..7b4ccb839cd64 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/next.config.js @@ -0,0 +1,9 @@ +const withReact18 = require('../../react-18/test/with-react-18') + +module.exports = withReact18({ + reactStrictMode: true, + experimental: { + serverComponents: true, + // runtime: 'edge', + }, +}) diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/package.json b/test/integration/react-streaming-and-server-components/switchable-runtime/package.json new file mode 100644 index 0000000000000..90af0ce830c99 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/package.json @@ -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" + } +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages-manifest.json b/test/integration/react-streaming-and-server-components/switchable-runtime/pages-manifest.json new file mode 100644 index 0000000000000..4aa797aeaee30 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages-manifest.json @@ -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" +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge-rsc.server.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge-rsc.server.js new file mode 100644 index 0000000000000..83dc8c219e84e --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge-rsc.server.js @@ -0,0 +1,18 @@ +import getRuntime from '../utils/runtime' +import getTime from '../utils/time' + +export default function Page() { + return ( +
+ This is a SSR RSC page. +
+ {'Runtime: ' + getRuntime()} +
+ {'Time: ' + getTime()} +
+ ) +} + +export const config = { + runtime: 'edge', +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge.js new file mode 100644 index 0000000000000..c3425c7e64af7 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/edge.js @@ -0,0 +1,18 @@ +import getRuntime from '../utils/runtime' +import getTime from '../utils/time' + +export default function Page() { + return ( +
+ This is a SSR page. +
+ {'Runtime: ' + getRuntime()} +
+ {'Time: ' + getTime()} +
+ ) +} + +export const config = { + runtime: 'edge', +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssg.server.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssg.server.js new file mode 100644 index 0000000000000..362e634644eb2 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssg.server.js @@ -0,0 +1,26 @@ +import getRuntime from '../utils/runtime' +import getTime from '../utils/time' + +export default function Page({ type }) { + return ( +
+ This is a {type} RSC page. +
+ {'Runtime: ' + getRuntime()} +
+ {'Time: ' + getTime()} +
+ ) +} + +export function getStaticProps() { + return { + props: { + type: 'SSG', + }, + } +} + +export const config = { + runtime: 'nodejs', +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssr.server.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssr.server.js new file mode 100644 index 0000000000000..1b8b01526a3ce --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc-ssr.server.js @@ -0,0 +1,26 @@ +import getRuntime from '../utils/runtime' +import getTime from '../utils/time' + +export default function Page({ type }) { + return ( +
+ This is a {type} RSC page. +
+ {'Runtime: ' + getRuntime()} +
+ {'Time: ' + getTime()} +
+ ) +} + +export function getServerSideProps() { + return { + props: { + type: 'SSR', + }, + } +} + +export const config = { + runtime: 'nodejs', +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc.server.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc.server.js new file mode 100644 index 0000000000000..f3563039b63bd --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-rsc.server.js @@ -0,0 +1,18 @@ +import getRuntime from '../utils/runtime' +import getTime from '../utils/time' + +export default function Page() { + return ( +
+ This is a static RSC page. +
+ {'Runtime: ' + getRuntime()} +
+ {'Time: ' + getTime()} +
+ ) +} + +export const config = { + runtime: 'nodejs', +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssg.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssg.js new file mode 100644 index 0000000000000..d555009acfcdb --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssg.js @@ -0,0 +1,26 @@ +import getRuntime from '../utils/runtime' +import getTime from '../utils/time' + +export default function Page({ type }) { + return ( +
+ This is a {type} page. +
+ {'Runtime: ' + getRuntime()} +
+ {'Time: ' + getTime()} +
+ ) +} + +export function getStaticProps() { + return { + props: { + type: 'SSG', + }, + } +} + +export const config = { + runtime: 'nodejs', +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssr.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssr.js new file mode 100644 index 0000000000000..e58276b47a763 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node-ssr.js @@ -0,0 +1,26 @@ +import getRuntime from '../utils/runtime' +import getTime from '../utils/time' + +export default function Page({ type }) { + return ( +
+ This is a {type} page. +
+ {'Runtime: ' + getRuntime()} +
+ {'Time: ' + getTime()} +
+ ) +} + +export function getServerSideProps() { + return { + props: { + type: 'SSR', + }, + } +} + +export const config = { + runtime: 'nodejs', +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node.js new file mode 100644 index 0000000000000..bf065da478ba8 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/node.js @@ -0,0 +1,18 @@ +import getRuntime from '../utils/runtime' +import getTime from '../utils/time' + +export default function Page() { + return ( +
+ This is a static page. +
+ {'Runtime: ' + getRuntime()} +
+ {'Time: ' + getTime()} +
+ ) +} + +export const config = { + runtime: 'nodejs', +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/static.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/static.js new file mode 100644 index 0000000000000..e44edfd795232 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/static.js @@ -0,0 +1,14 @@ +import getRuntime from '../utils/runtime' +import getTime from '../utils/time' + +export default function Page() { + return ( +
+ This is a static page. +
+ {'Runtime: ' + getRuntime()} +
+ {'Time: ' + getTime()} +
+ ) +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/utils/runtime.js b/test/integration/react-streaming-and-server-components/switchable-runtime/utils/runtime.js new file mode 100644 index 0000000000000..444f1ee8b498b --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/utils/runtime.js @@ -0,0 +1,3 @@ +export default function getRuntime() { + return process.version ? `Node.js ${process.version}` : 'Edge/Browser' +} diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/utils/time.js b/test/integration/react-streaming-and-server-components/switchable-runtime/utils/time.js new file mode 100644 index 0000000000000..cf78549b9a7c1 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/utils/time.js @@ -0,0 +1,3 @@ +export default function getTime() { + return Date.now() +} diff --git a/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js b/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js new file mode 100644 index 0000000000000..62ebdcadf4264 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js @@ -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, + }) + }) +}) diff --git a/test/integration/react-streaming-and-server-components/unsupported-native-module/next.config.js b/test/integration/react-streaming-and-server-components/unsupported-native-module/next.config.js index 4783ccbdadb78..deb87bcba88d7 100644 --- a/test/integration/react-streaming-and-server-components/unsupported-native-module/next.config.js +++ b/test/integration/react-streaming-and-server-components/unsupported-native-module/next.config.js @@ -3,7 +3,6 @@ const withReact18 = require('../../react-18/test/with-react-18') module.exports = withReact18({ experimental: { reactRoot: true, - runtime: 'edge', serverComponents: true, }, }) diff --git a/test/integration/react-streaming-and-server-components/unsupported-native-module/pages/index.js b/test/integration/react-streaming-and-server-components/unsupported-native-module/pages/index.js index 31f0f204a9d3b..8371b4c194f7d 100644 --- a/test/integration/react-streaming-and-server-components/unsupported-native-module/pages/index.js +++ b/test/integration/react-streaming-and-server-components/unsupported-native-module/pages/index.js @@ -8,3 +8,7 @@ export default function Index() { console.log(EOF) return 'Access Node.js native module dns' } + +export const config = { + runtime: 'edge', +}