diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index d1291eaea505a..de1537cd0d98d 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -66,6 +66,11 @@ import { NextFontManifestPlugin } from './webpack/plugins/next-font-manifest-plu import { getSupportedBrowsers } from './utils' import { METADATA_RESOURCE_QUERY } from './webpack/loaders/metadata/discover' +type ExcludesFalse = (x: T | false) => x is T +type ClientEntries = { + [key: string]: string | string[] +} + const EXTERNAL_PACKAGES = require('../lib/server-external-packages.json') as string[] @@ -384,7 +389,42 @@ export function getDefineEnv({ } } -type ExcludesFalse = (x: T | false) => x is T +function createReactAliases( + bundledReactChannel: string, + opts: { + reactSharedSubset: boolean + reactDomServerRenderingStub: boolean + } +) { + const alias = { + react$: `next/dist/compiled/react${bundledReactChannel}`, + 'react-dom$': `next/dist/compiled/react-dom${bundledReactChannel}`, + 'react/jsx-runtime$': `next/dist/compiled/react${bundledReactChannel}/jsx-runtime`, + 'react/jsx-dev-runtime$': `next/dist/compiled/react${bundledReactChannel}/jsx-dev-runtime`, + 'react-dom/server$': `next/dist/compiled/react-dom${bundledReactChannel}/server$`, + 'react-dom/server.edge$': `next/dist/compiled/react-dom${bundledReactChannel}/server.edge`, + 'react-dom/server.browser$': `next/dist/compiled/react-dom${bundledReactChannel}/server.browser`, + 'react-server-dom-webpack/client$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client`, + 'react-server-dom-webpack/client.edge$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client.edge`, + 'react-server-dom-webpack/server.edge$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/server.edge`, + 'react-server-dom-webpack/server.node$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/server.node`, + } + + if (opts.reactSharedSubset) { + alias[ + 'react$' + ] = `next/dist/compiled/react${bundledReactChannel}/react.shared-subset` + } + // Use server rendering stub for RSC + // x-ref: https://github.com/facebook/react/pull/25436 + if (opts.reactDomServerRenderingStub) { + alias[ + 'react-dom$' + ] = `next/dist/compiled/react-dom${bundledReactChannel}/server-rendering-stub` + } + + return alias +} const devtoolRevertWarning = execOnce( (devtool: webpack.Configuration['devtool']) => { @@ -436,10 +476,6 @@ function getOptimizedAliases(): { [pkg: string]: string } { ) } -type ClientEntries = { - [key: string]: string | string[] -} - export function attachReactRefresh( webpackConfig: webpack.Configuration, targetLoader: webpack.RuleSetUseItem @@ -1519,10 +1555,6 @@ export default async function getBaseWebpackConfig( '@builder.io/partytown': '{}', 'next/dist/compiled/etag': '{}', 'next/dist/compiled/chalk': '{}', - './cjs/react-dom-server-legacy.browser.production.min.js': - '{}', - './cjs/react-dom-server-legacy.browser.development.js': - '{}', }, getEdgePolyfilledModules(), handleWebpackExternalForEdgeRuntime, @@ -1822,13 +1854,10 @@ export default async function getBaseWebpackConfig( [require.resolve('next/dynamic')]: require.resolve( 'next/dist/shared/lib/app-dynamic' ), - 'react/jsx-runtime$': `next/dist/compiled/react${bundledReactChannel}/jsx-runtime`, - 'react/jsx-dev-runtime$': `next/dist/compiled/react${bundledReactChannel}/jsx-dev-runtime`, - 'react-dom/server.edge$': `next/dist/compiled/react-dom${bundledReactChannel}/server.edge`, - 'react-server-dom-webpack/client$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client`, - 'react-server-dom-webpack/client.edge$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client.edge`, - 'react-server-dom-webpack/server.edge$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/server.edge`, - 'react-server-dom-webpack/server.node$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/server.node`, + ...createReactAliases(bundledReactChannel, { + reactSharedSubset: false, + reactDomServerRenderingStub: false, + }), }, }, }, @@ -1856,8 +1885,10 @@ export default async function getBaseWebpackConfig( // If missing the alias override here, the default alias will be used which aliases // react to the direct file path, not the package name. In that case the condition // will be ignored completely. - react: `next/dist/compiled/react${bundledReactChannel}/react.shared-subset`, - 'react-dom$': `next/dist/compiled/react-dom${bundledReactChannel}/server-rendering-stub`, + ...createReactAliases(bundledReactChannel, { + reactSharedSubset: true, + reactDomServerRenderingStub: true, + }), }, }, use: { @@ -1920,37 +1951,40 @@ export default async function getBaseWebpackConfig( // It needs `conditionNames` here to require the proper asset, // when react is acting as dependency of compiled/react-dom. alias: { - react: `next/dist/compiled/react${bundledReactChannel}/react.shared-subset`, - // Use server rendering stub for RSC - // x-ref: https://github.com/facebook/react/pull/25436 - 'react-dom$': `next/dist/compiled/react-dom${bundledReactChannel}/server-rendering-stub`, - }, - }, - }, - { - issuerLayer: WEBPACK_LAYERS.client, - test: codeCondition.test, - resolve: { - alias: { - react: `next/dist/compiled/react${bundledReactChannel}`, - 'react-dom$': `next/dist/compiled/react-dom${bundledReactChannel}/server-rendering-stub`, + ...createReactAliases(bundledReactChannel, { + reactSharedSubset: true, + reactDomServerRenderingStub: true, + }), }, }, }, { test: codeCondition.test, + issuerLayer: WEBPACK_LAYERS.client, resolve: { alias: { - react: `next/dist/compiled/react${bundledReactChannel}`, - 'react-dom$': reactProductionProfiling - ? `next/dist/compiled/react-dom${bundledReactChannel}/cjs/react-dom.profiling.min` - : `next/dist/compiled/react-dom${bundledReactChannel}`, - 'react-dom/client$': `next/dist/compiled/react-dom${bundledReactChannel}/client`, + ...createReactAliases(bundledReactChannel, { + reactSharedSubset: false, + reactDomServerRenderingStub: true, + }), }, }, }, ], }, + { + test: codeCondition.test, + issuerLayer: WEBPACK_LAYERS.appClient, + resolve: { + alias: { + ...createReactAliases(bundledReactChannel, { + // Only alias server rendering stub in client SSR layer. + reactSharedSubset: false, + reactDomServerRenderingStub: false, + }), + }, + }, + }, ] : []), { diff --git a/packages/next/src/server/dev/hot-reloader.ts b/packages/next/src/server/dev/hot-reloader.ts index 4a545d9327bc6..780d07c50a377 100644 --- a/packages/next/src/server/dev/hot-reloader.ts +++ b/packages/next/src/server/dev/hot-reloader.ts @@ -180,6 +180,7 @@ export default class HotReloader { private clientError: Error | null = null private serverError: Error | null = null private serverPrevDocumentHash: string | null + private serverChunkNames?: Set private prevChunkNames?: Set private onDemandEntries?: ReturnType private previewProps: __ApiPreviewProps @@ -1050,6 +1051,7 @@ export default class HotReloader { (err: Error) => { this.serverError = err this.serverStats = null + this.serverChunkNames = undefined } ) @@ -1093,16 +1095,38 @@ export default class HotReloader { return } + // As document chunk will change if new app pages are joined, + // since react bundle is different it will effect the chunk hash. + // So we diff the chunk changes, if there's only new app page chunk joins, + // then we don't trigger a reload by checking pages/_document chunk change. + if (this.appDir) { + const chunkNames = new Set(compilation.namedChunks.keys()) + const diffChunkNames = difference( + this.serverChunkNames || new Set(), + chunkNames + ) + + if ( + diffChunkNames.length === 0 || + diffChunkNames.every((chunkName) => chunkName.startsWith('app/')) + ) { + return + } + this.serverChunkNames = chunkNames + } + // Notify reload to reload the page, as _document.js was changed (different hash) this.send('reloadPage') this.serverPrevDocumentHash = documentChunk.hash || null } ) + this.multiCompiler.hooks.done.tap('NextjsHotReloaderForServer', () => { const serverOnlyChanges = difference( changedServerPages, changedClientPages ) + const edgeServerOnlyChanges = difference( changedEdgeServerPages, changedClientPages diff --git a/packages/next/src/server/require-hook.ts b/packages/next/src/server/require-hook.ts index 64c287dab0a70..a44778d035cb3 100644 --- a/packages/next/src/server/require-hook.ts +++ b/packages/next/src/server/require-hook.ts @@ -54,6 +54,16 @@ function overrideReact() { `next/dist/compiled/react-dom-experimental/server-rendering-stub` ), ], + [ + 'react/package.json', + require.resolve(`next/dist/compiled/react-experimental/package.json`), + ], + [ + 'react-dom/package.json', + require.resolve( + `next/dist/compiled/react-dom-experimental/package.json` + ), + ], [ 'react-dom/client', require.resolve(`next/dist/compiled/react-dom-experimental/client`), @@ -102,6 +112,10 @@ function overrideReact() { } else { addHookAliases([ ['react', require.resolve(`next/dist/compiled/react`)], + [ + 'react/package.json', + require.resolve(`next/dist/compiled/react/package.json`), + ], [ 'react/jsx-runtime', require.resolve(`next/dist/compiled/react/jsx-runtime`), @@ -114,6 +128,10 @@ function overrideReact() { 'react-dom', require.resolve(`next/dist/compiled/react-dom/server-rendering-stub`), ], + [ + 'react-dom/package.json', + require.resolve(`next/dist/compiled/react-dom/package.json`), + ], [ 'react-dom/client', require.resolve(`next/dist/compiled/react-dom/client`), diff --git a/test/e2e/app-dir/app-middleware/pages/api/dump-headers-edge.js b/test/e2e/app-dir/app-middleware/pages/api/dump-headers-edge.js index c0c7e56067455..15fa2581745ee 100644 --- a/test/e2e/app-dir/app-middleware/pages/api/dump-headers-edge.js +++ b/test/e2e/app-dir/app-middleware/pages/api/dump-headers-edge.js @@ -1,4 +1,4 @@ -export const runtime = 'experimental-edge' +export const runtime = 'edge' export default (req) => { return Response.json(Object.fromEntries(req.headers.entries()), { diff --git a/test/e2e/app-dir/rsc-basic/app/app-react/page.js b/test/e2e/app-dir/rsc-basic/app/app-react/page.js index eb47848407816..aa357909a23f5 100644 --- a/test/e2e/app-dir/rsc-basic/app/app-react/page.js +++ b/test/e2e/app-dir/rsc-basic/app/app-react/page.js @@ -1,5 +1,15 @@ import React from 'react' +import ReactDOM from 'react-dom' +import ReactDOMServer from 'react-dom/server.browser' export default function Page() { - return
{'version=' + React.version}
+ return ( +
+

{'React.version=' + React.version}

+

{'ReactDOM.version=' + ReactDOM.version}

+

+ {'ReactDOMServer.version=' + ReactDOMServer.version} +

+
+ ) } diff --git a/test/e2e/app-dir/rsc-basic/pages/edge-pages-react.js b/test/e2e/app-dir/rsc-basic/pages/edge-pages-react.js new file mode 100644 index 0000000000000..4eb54e29ec1cb --- /dev/null +++ b/test/e2e/app-dir/rsc-basic/pages/edge-pages-react.js @@ -0,0 +1,17 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import ReactDOMServer from 'react-dom/server' + +export default function Page() { + return ( +
+

{'React.version=' + React.version}

+

{'ReactDOM.version=' + ReactDOM.version}

+

+ {'ReactDOMServer.version=' + ReactDOMServer.version} +

+
+ ) +} + +export const runtime = 'experimental-edge' diff --git a/test/e2e/app-dir/rsc-basic/pages/pages-react.js b/test/e2e/app-dir/rsc-basic/pages/pages-react.js index eb47848407816..d98cf85f3d147 100644 --- a/test/e2e/app-dir/rsc-basic/pages/pages-react.js +++ b/test/e2e/app-dir/rsc-basic/pages/pages-react.js @@ -1,5 +1,15 @@ import React from 'react' +import ReactDOM from 'react-dom' +import ReactDOMServer from 'react-dom/server' export default function Page() { - return
{'version=' + React.version}
+ return ( +
+

{'React.version=' + React.version}

+

{'ReactDOM.version=' + ReactDOM.version}

+

+ {'ReactDOMServer.version=' + ReactDOMServer.version} +

+
+ ) } diff --git a/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts index ccf3db8ccdfc2..be2878f77b7fa 100644 --- a/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts @@ -19,19 +19,9 @@ createNextDescribe( { files: __dirname, dependencies: { - 'styled-components': '6.0.0-beta.5', + 'styled-components': 'latest', 'server-only': 'latest', }, - packageJson: { - scripts: { - build: 'next build', - dev: 'next dev', - start: 'next start', - }, - }, - installCommand: 'yarn', - startCommand: (global as any).isNextDev ? 'yarn dev' : 'yarn start', - buildCommand: 'yarn build', }, ({ next, isNextDev, isNextStart }) => { if (isNextDev) { @@ -437,18 +427,72 @@ createNextDescribe( }) it('should use stable react for pages', async () => { - const resPages = await next.fetch('/pages-react') - const versionPages = (await resPages.text()).match( - /
version=([^<]+)<\/div>/ - )?.[1] - - const resApp = await next.fetch('/app-react') - const versionApp = (await resApp.text()).match( - /
version=([^<]+)<\/div>/ - )?.[1] - - expect(versionPages).not.toInclude('-canary-') - expect(versionApp).toInclude('-canary-') + const ssrPaths = ['/pages-react', '/pages-react-edge', '/app-react'] + const promises = ssrPaths.map(async (pathname) => { + const resPages$ = await next.render$(pathname) + const ssrPagesReactVersions = [ + await resPages$('#react').text(), + await resPages$('#react-dom').text(), + await resPages$('#react-dom-server').text(), + ] + + ssrPagesReactVersions.forEach((version) => { + if (pathname === '/app-react') { + expect(version).toMatch('-canary-') + } else { + expect(version).not.toMatch('-canary-') + } + }) + }) + await Promise.all(promises) + + const resApp$ = await next.render$('/app-react') + const ssrAppReactVersions = [ + await resApp$('#react').text(), + await resApp$('#react-dom').text(), + await resApp$('#react-dom-server').text(), + ] + + ssrAppReactVersions.forEach((version) => + expect(version).toMatch('-canary-') + ) + + const browser = await next.browser('/pages-react') + const browserPagesReactVersions = await browser.eval(` + [ + document.querySelector('#react').innerText, + document.querySelector('#react-dom').innerText, + document.querySelector('#react-dom-server').innerText, + ] + `) + + await browser.loadPage(next.url + '/edge-pages-react') + const browserEdgePagesReactVersions = await browser.eval(` + [ + document.querySelector('#react').innerText, + document.querySelector('#react-dom').innerText, + document.querySelector('#react-dom-server').innerText, + ] + `) + + await browser.loadPage(next.url + '/app-react') + const browserAppReactVersions = await browser.eval(` + [ + document.querySelector('#react').innerText, + document.querySelector('#react-dom').innerText, + document.querySelector('#react-dom-server').innerText, + ] + `) + + browserPagesReactVersions.forEach((version) => + expect(version).not.toMatch('-canary-') + ) + browserEdgePagesReactVersions.forEach((version) => + expect(version).not.toMatch('-canary-') + ) + browserAppReactVersions.forEach((version) => + expect(version).toMatch('-canary-') + ) }) // disable this flaky test