diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 5c18d1c55518d..5f6883fc96984 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -3,6 +3,7 @@ import chalk from 'chalk' import crypto from 'crypto' import { readFileSync } from 'fs' import { codeFrameColumns } from 'next/dist/compiled/babel/code-frame' +import semver from 'next/dist/compiled/semver' import { isWebpack5, webpack } from 'next/dist/compiled/webpack/webpack' import path, { join as pathJoin, relative as relativePath } from 'path' import { @@ -12,6 +13,7 @@ import { PAGES_DIR_ALIAS, } from '../lib/constants' import { fileExists } from '../lib/file-exists' +import { getPackageVersion } from '../lib/get-package-version' import { CustomRoutes } from '../lib/load-custom-routes.js' import { getTypeScriptConfiguration } from '../lib/typescript/getTypeScriptConfiguration' import { @@ -230,6 +232,30 @@ export default async function getBaseWebpackConfig( rewrites.afterFiles.length > 0 || rewrites.fallback.length > 0 const hasReactRefresh: boolean = dev && !isServer + const reactDomVersion = await getPackageVersion({ + cwd: dir, + name: 'react-dom', + }) + const hasReact18: boolean = + Boolean(reactDomVersion) && + (semver.gte(reactDomVersion!, '18.0.0') || + semver.coerce(reactDomVersion)?.version === '18.0.0') + const hasReactPrerelease = + Boolean(reactDomVersion) && semver.prerelease(reactDomVersion!) != null + const hasReactRoot: boolean = config.experimental.reactRoot || hasReact18 + + // Only inform during one of the builds + if (!isServer) { + if (hasReactRoot) { + Log.info('Using the createRoot API for React') + } + if (hasReactPrerelease) { + Log.warn( + `You are using an unsupported prerelease of 'react-dom' which may cause ` + + `unexpected or broken application behavior. Continue at your own risk.` + ) + } + } const babelConfigFile = await [ '.babelrc', @@ -1076,9 +1102,7 @@ export default async function getBaseWebpackConfig( 'process.env.__NEXT_STRICT_MODE': JSON.stringify( config.reactStrictMode ), - 'process.env.__NEXT_REACT_ROOT': JSON.stringify( - config.experimental.reactRoot - ), + 'process.env.__NEXT_REACT_ROOT': JSON.stringify(hasReactRoot), 'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify( config.optimizeFonts && !dev ), diff --git a/test/integration/react-18/prerelease/.gitignore b/test/integration/react-18/prerelease/.gitignore new file mode 100644 index 0000000000000..736e8ae58ad87 --- /dev/null +++ b/test/integration/react-18/prerelease/.gitignore @@ -0,0 +1 @@ +!node_modules \ No newline at end of file diff --git a/test/integration/react-18/prerelease/node_modules/react-dom/index.js b/test/integration/react-18/prerelease/node_modules/react-dom/index.js new file mode 100644 index 0000000000000..27d7f6e85eb30 --- /dev/null +++ b/test/integration/react-18/prerelease/node_modules/react-dom/index.js @@ -0,0 +1 @@ +module.exports = { Suspense: true } diff --git a/test/integration/react-18/prerelease/node_modules/react-dom/package.json b/test/integration/react-18/prerelease/node_modules/react-dom/package.json new file mode 100644 index 0000000000000..f89a61e597a6e --- /dev/null +++ b/test/integration/react-18/prerelease/node_modules/react-dom/package.json @@ -0,0 +1,4 @@ +{ + "name": "react-dom", + "version": "18.0.0-alpha-43f4cc160" +} diff --git a/test/integration/react-18/prerelease/package.json b/test/integration/react-18/prerelease/package.json new file mode 100644 index 0000000000000..1e0b54f840c56 --- /dev/null +++ b/test/integration/react-18/prerelease/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "react-dom": "*" + } +} diff --git a/test/integration/react-18/prerelease/pages/index.js b/test/integration/react-18/prerelease/pages/index.js new file mode 100644 index 0000000000000..fb077e8078c9e --- /dev/null +++ b/test/integration/react-18/prerelease/pages/index.js @@ -0,0 +1,3 @@ +export default function Index() { + return

Hello

+} diff --git a/test/integration/react-18/supported/pages/index.js b/test/integration/react-18/supported/pages/index.js new file mode 100644 index 0000000000000..fb077e8078c9e --- /dev/null +++ b/test/integration/react-18/supported/pages/index.js @@ -0,0 +1,3 @@ +export default function Index() { + return

Hello

+} diff --git a/test/integration/react-18/test/index.test.js b/test/integration/react-18/test/index.test.js new file mode 100644 index 0000000000000..8395643309eb1 --- /dev/null +++ b/test/integration/react-18/test/index.test.js @@ -0,0 +1,70 @@ +/* eslint-env jest */ + +import { findPort, killApp, launchApp, runNextCommand } from 'next-test-utils' +import { join } from 'path' + +jest.setTimeout(1000 * 60 * 5) + +const dirSupported = join(__dirname, '../supported') +const dirPrerelease = join(__dirname, '../prerelease') + +const UNSUPPORTED_PRERELEASE = + "You are using an unsupported prerelease of 'react-dom'" +const USING_CREATE_ROOT = 'Using the createRoot API for React' + +async function getBuildOutput(dir) { + const { stdout, stderr } = await runNextCommand(['build', dir], { + stdout: true, + stderr: true, + }) + return stdout + stderr +} + +async function getDevOutput(dir) { + const port = await findPort() + + let stdout = '' + let stderr = '' + let instance = await launchApp(dir, port, { + stdout: true, + stderr: true, + onStdout(msg) { + stdout += msg + }, + onStderr(msg) { + stderr += msg + }, + }) + await killApp(instance) + return stdout + stderr +} + +describe('React 18 Support', () => { + describe('build', () => { + test('supported version of React', async () => { + const output = await getBuildOutput(dirSupported) + expect(output).not.toMatch(USING_CREATE_ROOT) + expect(output).not.toMatch(UNSUPPORTED_PRERELEASE) + }) + + test('prerelease version of React', async () => { + const output = await getBuildOutput(dirPrerelease) + expect(output).toMatch(USING_CREATE_ROOT) + expect(output).toMatch(UNSUPPORTED_PRERELEASE) + }) + }) + + describe('dev', () => { + test('supported version of React', async () => { + let output = await getDevOutput(dirSupported) + expect(output).not.toMatch(USING_CREATE_ROOT) + expect(output).not.toMatch(UNSUPPORTED_PRERELEASE) + }) + + test('prerelease version of React', async () => { + let output = await getDevOutput(dirPrerelease) + expect(output).toMatch(USING_CREATE_ROOT) + expect(output).toMatch(UNSUPPORTED_PRERELEASE) + }) + }) +})