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 304b7047b149..c6a57df897a0 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 e1176656fe08..3e0584d68b43 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 0c3e64cbe121..48d208912cb7 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 937c01ab089c..df995262048e 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 f2e6dbb64f13..3679b28eebd1 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 8ac37914b002..72868292fa6a 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 bcd58454ab57..b30f365b54ff 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 ae247d9ea66f..361f0183899d 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 83a8d7e00787..2455112a313d 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 7797d1d65404..190cfadc6b27 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 0a4455715361..b069fca0f49f 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 e0096a0b3502..9372f0f199f4 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 aaf54e244118..8ae8c7c599f2 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 debbad85a56b..bc63d2ad4f0f 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 c4feeb2e8a30..0db4aacd6cf9 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 0c6ec0bd1bae..7d9d321f7cb0 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 e23ccd094bba..05f85b9f4b16 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 0083c3b9d4cc..96c50ace36a5 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 a0e988f015f2..c5fdc1f1d40f 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 c3740b8719fe..8604f3bdcb72 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 288d6165f999..27c7c0bddf02 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 ac58f5e9e2c3..70aaf4ced70e 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 ac58f5e9e2c3..70aaf4ced70e 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 000000000000..7b4ccb839cd6 --- /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 000000000000..90af0ce830c9 --- /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 000000000000..4aa797aeaee3 --- /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 000000000000..83dc8c219e84 --- /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 000000000000..c3425c7e64af --- /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 000000000000..362e634644eb --- /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 000000000000..1b8b01526a3c --- /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 000000000000..f3563039b63b --- /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 000000000000..d555009acfcd --- /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 000000000000..e58276b47a76 --- /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 000000000000..bf065da478ba --- /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 000000000000..e44edfd79523 --- /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 000000000000..444f1ee8b498 --- /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 000000000000..cf78549b9a7c --- /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 000000000000..62ebdcadf426 --- /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 4783ccbdadb7..deb87bcba88d 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 31f0f204a9d3..8371b4c194f7 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', +}