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', +}