diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 1f4fb64e6b07d9..9a0e0cbb8e95bb 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -502,6 +502,7 @@ jobs: export NEXT_TEST_MODE=start export NEXT_TEST_WASM=true + export IS_WEBPACK_TEST=1 node run-tests.js \ test/production/pages-dir/production/test/index.test.ts \ test/e2e/streaming-ssr/index.test.ts @@ -711,7 +712,7 @@ jobs: secrets: inherit - test-dev: + test-dev: # TODO: rename to include webpack name: test dev needs: ['optimize-ci', 'changes', 'build-native', 'build-next'] if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} @@ -728,6 +729,7 @@ jobs: uses: ./.github/workflows/build_reusable.yml with: afterBuild: | + export IS_WEBPACK_TEST=1 export NEXT_TEST_MODE=dev export NEXT_TEST_REACT_VERSION="${{ matrix.react }}" @@ -752,8 +754,10 @@ jobs: uses: ./.github/workflows/build_reusable.yml with: + # Should this be using turbopack? a variation? afterBuild: | export NEXT_TEST_MODE=dev + export IS_WEBPACK_TEST=1 node run-tests.js \ test/e2e/app-dir/app/index.test.ts \ @@ -780,6 +784,7 @@ jobs: with: nodeVersion: 20.9.0 afterBuild: | + export IS_WEBPACK_TEST=1 node run-tests.js \ --concurrency 4 \ test/production/pages-dir/production/test/index.test.ts \ @@ -808,6 +813,7 @@ jobs: with: afterBuild: | export NEXT_TEST_MODE=start + export IS_WEBPACK_TEST=1 node run-tests.js --type production \ test/e2e/app-dir/app/index.test.ts \ @@ -836,6 +842,7 @@ jobs: uses: ./.github/workflows/build_reusable.yml with: afterBuild: | + export IS_WEBPACK_TEST=1 export NEXT_TEST_MODE=start export NEXT_TEST_REACT_VERSION="${{ matrix.react }}" @@ -875,6 +882,7 @@ jobs: with: nodeVersion: 20.9.0 afterBuild: | + export IS_WEBPACK_TEST=1 export NEXT_TEST_REACT_VERSION="${{ matrix.react }}" node run-tests.js \ @@ -896,6 +904,7 @@ jobs: # these all run without concurrency because they're heavier export TEST_CONCURRENCY=1 + export IS_WEBPACK_TEST=1 BROWSER_NAME=firefox node run-tests.js \ test/production/pages-dir/production/test/index.test.ts @@ -923,6 +932,7 @@ jobs: afterBuild: | export __NEXT_EXPERIMENTAL_PPR=true export NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" + export IS_WEBPACK_TEST=1 node run-tests.js \ --timings \ @@ -945,6 +955,7 @@ jobs: export __NEXT_EXPERIMENTAL_PPR=true export NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" export NEXT_TEST_MODE=dev + export IS_WEBPACK_TEST=1 node run-tests.js \ --timings \ @@ -968,6 +979,7 @@ jobs: export __NEXT_EXPERIMENTAL_PPR=true export NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" export NEXT_TEST_MODE=start + export IS_WEBPACK_TEST=1 node run-tests.js \ --timings \ @@ -990,6 +1002,7 @@ jobs: export __NEXT_EXPERIMENTAL_PPR=true # for compatibility with the existing tests export __NEXT_EXPERIMENTAL_CACHE_COMPONENTS=true export NEXT_EXTERNAL_TESTS_FILTERS="test/experimental-tests-manifest.json" + export IS_WEBPACK_TEST=1 node run-tests.js \ --timings \ @@ -1014,6 +1027,7 @@ jobs: export NEXT_EXTERNAL_TESTS_FILTERS="test/experimental-tests-manifest.json" export NEXT_TEST_MODE=dev export __NEXT_EXPERIMENTAL_ISOLATED_DEV_BUILD=true + export IS_WEBPACK_TEST=1 node run-tests.js \ --timings \ @@ -1038,6 +1052,7 @@ jobs: export __NEXT_EXPERIMENTAL_CACHE_COMPONENTS=true export NEXT_EXTERNAL_TESTS_FILTERS="test/experimental-tests-manifest.json" export NEXT_TEST_MODE=start + export IS_WEBPACK_TEST=1 node run-tests.js \ --timings \ diff --git a/.github/workflows/pull_request_stats.yml b/.github/workflows/pull_request_stats.yml index 3b3afeee5da3b3..9fe1fc91701bc8 100644 --- a/.github/workflows/pull_request_stats.yml +++ b/.github/workflows/pull_request_stats.yml @@ -60,3 +60,7 @@ jobs: - uses: ./.github/actions/next-stats-action if: ${{ steps.docs-change.outputs.DOCS_CHANGE == 'nope' }} + env: + # This uses the webpack bundle analyzer and for consistent results we need to use webpack. + # Once there is an equivalent analyzer for turbopack, we can remove this. + IS_WEBPACK_TEST: 1 diff --git a/.github/workflows/test_e2e_deploy_release.yml b/.github/workflows/test_e2e_deploy_release.yml index 631b0a2b43f462..9c0d33eeefde9b 100644 --- a/.github/workflows/test_e2e_deploy_release.yml +++ b/.github/workflows/test_e2e_deploy_release.yml @@ -78,7 +78,15 @@ jobs: matrix: group: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8] with: - afterBuild: npm i -g vercel@latest && NEXT_E2E_TEST_TIMEOUT=240000 NEXT_TEST_MODE=deploy NEXT_EXTERNAL_TESTS_FILTERS="test/deploy-tests-manifest.json" NEXT_TEST_VERSION="${{ github.event.inputs.nextVersion || 'canary' }}" VERCEL_CLI_VERSION="${{ github.event.inputs.vercelCliVersion || 'vercel@latest' }}" node run-tests.js --timings -g ${{ matrix.group }} -c 2 --type e2e + afterBuild: | + npm i -g vercel@latest && \ + NEXT_E2E_TEST_TIMEOUT=240000 \ + NEXT_TEST_MODE=deploy \ + IS_WEBPACK_TEST=1 \ + NEXT_EXTERNAL_TESTS_FILTERS="test/deploy-tests-manifest.json" \ + NEXT_TEST_VERSION="${{ github.event.inputs.nextVersion || 'canary' }}" \ + VERCEL_CLI_VERSION="${{ github.event.inputs.vercelCliVersion || 'vercel@latest' }}" \ + node run-tests.js --timings -g ${{ matrix.group }} -c 2 --type e2e skipNativeBuild: 'yes' skipNativeInstall: 'no' stepName: 'test-deploy-${{ matrix.group }}' diff --git a/bench/heavy-npm-deps/package.json b/bench/heavy-npm-deps/package.json index 2428e307218e85..8bd442a10f8a5e 100644 --- a/bench/heavy-npm-deps/package.json +++ b/bench/heavy-npm-deps/package.json @@ -4,9 +4,9 @@ "private": true, "scripts": { "dev-turbopack": "next dev --turbopack", - "dev-webpack": "next dev", + "dev-webpack": "next dev --webpack", "build-turbopack": "next build --turbopack", - "build-webpack": "next build", + "build-webpack": "next build --webpack", "start-turbopack": "next start", "start-webpack": "next start", "build-application": "next build", diff --git a/bench/module-cost/package.json b/bench/module-cost/package.json index eb6e8a7478d92a..96814e0e6e8759 100644 --- a/bench/module-cost/package.json +++ b/bench/module-cost/package.json @@ -3,10 +3,10 @@ "scripts": { "prepare-bench": "node scripts/prepare-bench.mjs", "benchmark": "node scripts/benchmark-runner.mjs", - "dev-webpack": "next dev", - "dev-turbopack": "next dev --turbo", - "build-webpack": "next build", - "build-turbopack": "next build --turbo", + "dev-webpack": "next dev --webpack", + "dev-turbopack": "next dev --turbopack", + "build-webpack": "next build --webpack", + "build-turbopack": "next build --turbopack", "start": "next start" }, "devDependencies": { diff --git a/package.json b/package.json index bd4eb8ca637c44..b36ba0bcd23c32 100644 --- a/package.json +++ b/package.json @@ -17,29 +17,49 @@ "ci:publish": "tsx ./scripts/release/publish-npm.ts", "test-types": "tsc", "test-unit": "jest test/unit/ packages/next/ packages/font", - "test-dev": "cross-env NEXT_TEST_MODE=dev pnpm testheadless", - "test-dev-experimental": "cross-env NEXT_TEST_MODE=dev pnpm run with-experimental pnpm testheadless", - "test-dev-rspack": "pnpm run with-rspack pnpm run test-dev", - "test-dev-experimental-rspack": "pnpm run with-rspack pnpm run test-dev-experimental", - "test-dev-turbo": "cross-env NEXT_TEST_MODE=dev IS_TURBOPACK_TEST=1 TURBOPACK_DEV=1 pnpm testheadless", - "test-dev-experimental-turbo": "cross-env NEXT_TEST_MODE=dev IS_TURBOPACK_TEST=1 TURBOPACK_DEV=1 pnpm run with-experimental pnpm testheadless", - "test-start": "cross-env NEXT_TEST_MODE=start pnpm testheadless", - "test-start-experimental": "cross-env NEXT_TEST_MODE=start pnpm run with-experimental pnpm testheadless", - "test-start-rspack": "pnpm run with-rspack pnpm run test-start", - "test-start-experimental-rspack": "pnpm run with-rspack pnpm run test-start-experimental", - "test-start-turbo": "cross-env NEXT_TEST_MODE=start IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1 pnpm testheadless", - "test-start-experimental-turbo": "cross-env NEXT_TEST_MODE=start IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1 pnpm run with-experimental pnpm testheadless", - "test-deploy": "cross-env NEXT_TEST_MODE=deploy pnpm testheadless", - "testonly-dev": "cross-env NEXT_TEST_MODE=dev pnpm testonly", - "testonly-dev-rspack": "pnpm run with-rspack pnpm run testonly-dev", - "testonly-dev-turbo": "cross-env NEXT_TEST_MODE=dev IS_TURBOPACK_TEST=1 TURBOPACK_DEV=1 pnpm testonly", - "testonly-start": "cross-env NEXT_TEST_MODE=start pnpm testonly", - "testonly-start-rspack": "pnpm run with-rspack pnpm run testonly-start", - "testonly-start-turbo": "cross-env NEXT_TEST_MODE=start IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1 pnpm testonly", - "testonly-deploy": "cross-env NEXT_TEST_MODE=deploy pnpm testonly", - "test": "pnpm testheadless", - "test-rspack": "pnpm run with-rspack pnpm run test", - "test-turbo": "cross-env IS_TURBOPACK_TEST=1 TURBOPACK_DEV=1 TURBOPACK_BUILD=1 pnpm testheadless", + "test-dev-inner": "cross-env NEXT_TEST_MODE=dev pnpm testheadless", + "test-dev": "pnpm run test-dev-webpack", + "test-dev-webpack": "pnpm run with-webpack pnpm test-dev-inner", + "test-dev-experimental-inner": "pnpm run with-experimental pnpm test-dev-inner", + "test-dev-experimental": "pnpm run test-dev-experimental-webpack", + "test-dev-experimental-webpack": "pnpm run with-webpack pnpm test-dev-experimental-inner", + "test-dev-rspack": "pnpm run with-rspack pnpm run test-dev-inner", + "test-dev-experimental-rspack": "pnpm run with-rspack pnpm run test-dev-experimental-inner", + "test-dev-turbo": "cross-env IS_TURBOPACK_TEST=1 TURBOPACK_DEV=1 pnpm test-dev-inner", + "test-dev-experimental-turbo": "cross-env IS_TURBOPACK_TEST=1 TURBOPACK_DEV=1 pnpm run with-experimental pnpm test-dev-inner", + "test-start-inner": "cross-env NEXT_TEST_MODE=start pnpm testheadless", + "test-start": "pnpm run test-start-webpack", + "test-start-webpack": "pnpm run with-webpack pnpm run test-start-inner", + "test-start-experimental-inner": "pnpm run with-experimental pnpm test-start-inner", + "test-start-experimental": "pnpm run test-start-experimental-webpack", + "test-start-experimental-webpack": "pnpm run with-webpack pnpm run test-start-experimental-inner", + "test-start-rspack": "pnpm run with-rspack pnpm run test-start-inner", + "test-start-experimental-rspack": "pnpm run with-rspack pnpm run test-start-experimental-inner", + "test-start-turbo": "cross-env IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1 pnpm test-start-inner", + "test-start-experimental-turbo": "cross-env IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1 pnpm run with-experimental pnpm test-start-experimental-inner", + "test-deploy-inner": "cross-env NEXT_TEST_MODE=deploy pnpm testheadless", + "test-deploy": "pnpm run test-deploy-webpack", + "test-deploy-webpack": "pnpm run with-webpack pnpm test-deploy-inner", + "test-deploy-turbo": "cross-env IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1 pnpm test-deploy-inner", + "testonly-dev-inner": "cross-env NEXT_TEST_MODE=dev pnpm testonly", + "testonly-dev": "pnpm run testonly-dev-webpack", + "testonly-dev-webpack": "pnpm run with-webpack pnpm run testonly-dev-inner", + "testonly-dev-rspack": "pnpm run with-rspack pnpm run testonly-dev-inner", + "testonly-dev-turbo": "cross-env IS_TURBOPACK_TEST=1 TURBOPACK_DEV=1 pnpm testonly-dev-inner", + "testonly-start-inner": "cross-env NEXT_TEST_MODE=start pnpm testonly", + "testonly-start": "pnpm run testonly-start-webpack", + "testonly-start-webpack": "pnpm run with-webpack pnpm run testonly-start-inner", + "testonly-start-rspack": "pnpm run with-rspack pnpm run testonly-start-inner", + "testonly-start-turbo": "cross-env IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1 pnpm testonly-start-inner", + "testonly-deploy-inner": "cross-env NEXT_TEST_MODE=deploy pnpm testonly", + "testonly-deploy": "pnpm run testonly-deploy-webpack", + "testonly-deploy-webpack": "pnpm run with-webpack pnpm run testonly-deploy-inner", + "testonly-deploy-turbo": "cross-env IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1 pnpm testonly-deploy-inner", + "test-inner": "pnpm testheadless", + "test": "pnpm test-webpack", + "test-webpack": "pnpm run with-webpack pnpm run test-inner", + "test-rspack": "pnpm run with-rspack pnpm run test-inner", + "test-turbo": "cross-env IS_TURBOPACK_TEST=1 TURBOPACK_DEV=1 TURBOPACK_BUILD=1 pnpm test-inner", "testonly": "jest --runInBand", "testheadless": "cross-env HEADLESS=true pnpm testonly", "genstats": "cross-env LOCAL_STATS=true node .github/actions/next-stats-action/src/index.js", @@ -89,6 +109,7 @@ "build-storybook": "turbo run build-storybook", "test-storybook": "turbo run test-storybook", "with-rspack": "cross-env NEXT_RSPACK=1 NEXT_TEST_USE_RSPACK=1", + "with-webpack": "cross-env IS_WEBPACK_TEST=1", "with-experimental": "cross-env __NEXT_EXPERIMENTAL_CACHE_COMPONENTS=true __NEXT_EXPERIMENTAL_PPR=true" }, "devDependencies": { diff --git a/packages/next-rspack/index.js b/packages/next-rspack/index.js index e0ce02d637d847..59bf459282dd60 100644 --- a/packages/next-rspack/index.js +++ b/packages/next-rspack/index.js @@ -1,5 +1,15 @@ module.exports = function withRspack(config) { process.env.NEXT_RSPACK = 'true' process.env.RSPACK_CONFIG_VALIDATE = 'loose-silent' + if (process.env.TURBOPACK === 'auto') { + // If next has defaulted to turbopack, override it. + delete process.env.TURBOPACK + } else { + console.error( + `Cannot call withRspack and pass the ${process.env.TURBOPACK ? '--turbopack' : '--webpack'} flag.` + ) + console.error('Please configure only one bundler.') + process.exit(1) + } return config } diff --git a/packages/next/src/bin/next.ts b/packages/next/src/bin/next.ts index cfcb4b5b355b72..a947d1ad95aeed 100755 --- a/packages/next/src/bin/next.ts +++ b/packages/next/src/bin/next.ts @@ -133,8 +133,9 @@ program .option('--no-mangling', 'Disables mangling.') .option('--profile', 'Enables production profiling for React.') .option('--experimental-app-only', 'Builds only App Router routes.') - .option('--turbo', 'Starts development mode using Turbopack.') - .option('--turbopack', 'Starts development mode using Turbopack.') + .option('--turbo', 'Builds using Turbopack.') + .option('--turbopack', 'Builds using Turbopack.') + .option('--webpack', 'Builds using webpack.') .addOption( new Option( '--experimental-build-mode [mode]', @@ -173,6 +174,7 @@ program ) .option('--turbo', 'Starts development mode using Turbopack.') .option('--turbopack', 'Starts development mode using Turbopack.') + .option('--webpack', 'Starts development mode using webpack.') .addOption( new Option( '-p, --port ', diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 9225b231b9d6ff..5394a9b2bd5938 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -83,6 +83,7 @@ import { UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY, } from '../shared/lib/entry-constants' import { isDynamicRoute } from '../shared/lib/router/utils' +import { Bundler, finalizeBundlerFromConfig } from '../lib/bundler' import type { __ApiPreviewProps } from '../server/api-utils' import loadConfig from '../server/config' import type { BuildManifest } from '../server/get-page-files' @@ -901,7 +902,7 @@ export default async function build( runLint = true, noMangling = false, appDirOnly = false, - isTurbopack = false, + bundler = Bundler.Turbopack, experimentalBuildMode: 'default' | 'compile' | 'generate' | 'generate-env', traceUploadUrl: string | undefined ): Promise { @@ -914,7 +915,6 @@ export default async function build( try { const nextBuildSpan = trace('next-build', undefined, { buildMode: experimentalBuildMode, - isTurboBuild: String(isTurbopack), version: process.env.__NEXT_VERSION as string, }) @@ -949,6 +949,10 @@ export default async function build( ) loadedConfig = config + // Reading the config can modify environment variables that influence the bundler selection. + bundler = finalizeBundlerFromConfig(bundler) + nextBuildSpan.setAttribute('bundler', getBundlerForTelemetry(bundler)) + process.env.NEXT_DEPLOYMENT_ID = config.deploymentId || '' NextBuildContext.config = config @@ -971,7 +975,7 @@ export default async function build( NextBuildContext.buildId = buildId if (experimentalBuildMode === 'generate-env') { - if (isTurbopack) { + if (bundler === Bundler.Turbopack) { Log.warn('generate-env is not needed with turbopack') process.exit(0) } @@ -1398,7 +1402,7 @@ export default async function build( }) // Turbopack already handles conflicting app and page routes. - if (!isTurbopack) { + if (bundler !== Bundler.Turbopack) { const numConflictingAppPaths = conflictingAppPagePaths.length if (mappedAppPages && numConflictingAppPaths > 0) { Log.error( @@ -1681,7 +1685,7 @@ export default async function build( let shutdownPromise = Promise.resolve() if (!isGenerateMode) { - if (isTurbopack) { + if (bundler === Bundler.Turbopack) { const { duration: compilerDuration, shutdownPromise: p, @@ -1795,7 +1799,7 @@ export default async function build( telemetry.record( eventBuildCompleted(pagesPaths, { - bundler: getBundlerForTelemetry(isTurbopack), + bundler: getBundlerForTelemetry(bundler), durationInSeconds, totalAppPagesCount, }) @@ -1811,7 +1815,7 @@ export default async function build( telemetry.record( eventBuildCompleted(pagesPaths, { - bundler: getBundlerForTelemetry(isTurbopack), + bundler: getBundlerForTelemetry(bundler), durationInSeconds: compilerDuration, totalAppPagesCount, }) @@ -2429,7 +2433,10 @@ export default async function build( ) // If there's edge routes, append the edge instrumentation hook // Turbopack generates this chunk with a hashed name and references it in middleware-manifest. - if (!isTurbopack && (edgeRuntimeAppCount || edgeRuntimePagesCount)) { + if ( + bundler !== Bundler.Turbopack && + (edgeRuntimeAppCount || edgeRuntimePagesCount) + ) { instrumentationHookEntryFiles.push( path.join( SERVER_DIRECTORY, @@ -2482,7 +2489,7 @@ export default async function build( path.join(SERVER_DIRECTORY, FUNCTIONS_CONFIG_MANIFEST), path.join(SERVER_DIRECTORY, MIDDLEWARE_MANIFEST), path.join(SERVER_DIRECTORY, MIDDLEWARE_BUILD_MANIFEST + '.js'), - ...(!isTurbopack + ...(bundler !== Bundler.Turbopack ? [ path.join( SERVER_DIRECTORY, @@ -2517,7 +2524,7 @@ export default async function build( ), ] : []), - ...(pagesDir && !isTurbopack + ...(pagesDir && bundler !== Bundler.Turbopack ? [ DYNAMIC_CSS_MANIFEST + '.json', path.join(SERVER_DIRECTORY, DYNAMIC_CSS_MANIFEST + '.js'), @@ -2569,7 +2576,7 @@ export default async function build( ], } - if (isTurbopack) { + if (bundler === Bundler.Turbopack) { await writeManifest( path.join( distDir, @@ -2585,7 +2592,11 @@ export default async function build( await writeFunctionsConfigManifest(distDir, functionsConfigManifest) - if (!isTurbopack && !isGenerateMode && !buildTracesPromise) { + if ( + bundler !== Bundler.Turbopack && + !isGenerateMode && + !buildTracesPromise + ) { buildTracesPromise = nextBuildSpan .traceChild('collect-build-traces') .traceAsyncFn(() => { @@ -2726,7 +2737,7 @@ export default async function build( // we don't need to inline for turbopack build as // it will handle it's own caching separate of compile - if (isGenerateMode && !isTurbopack) { + if (isGenerateMode && bundler !== Bundler.Turbopack) { Log.info('Inlining static env ...') await nextBuildSpan @@ -4132,7 +4143,7 @@ export default async function build( if (telemetry) { telemetry.record( eventBuildFailed({ - bundler: getBundlerForTelemetry(isTurbopack), + bundler: getBundlerForTelemetry(bundler), errorCode: getErrorCodeForTelemetry(e), durationInSeconds: Math.floor((Date.now() - buildStartTime) / 1000), }) @@ -4153,7 +4164,7 @@ export default async function build( mode: 'build', projectDir: dir, distDir: loadedConfig.distDir, - isTurboSession: isTurbopack, + isTurboSession: bundler === Bundler.Turbopack, sync: true, }) } @@ -4167,16 +4178,17 @@ function errorFromUnsupportedSegmentConfig(): never { process.exit(1) } -function getBundlerForTelemetry(isTurbopack: boolean) { - if (isTurbopack) { - return 'turbopack' +function getBundlerForTelemetry(bundler: Bundler) { + switch (bundler) { + case Bundler.Turbopack: + return 'turbopack' + case Bundler.Rspack: + return 'rspack' + case Bundler.Webpack: + return 'webpack' + default: + throw new Error(`unknown bundler: ${bundler}`) } - - if (process.env.NEXT_RSPACK) { - return 'rspack' - } - - return 'webpack' } function getErrorCodeForTelemetry(err: unknown) { diff --git a/packages/next/src/cli/next-build.ts b/packages/next/src/cli/next-build.ts index a2df58fdc41356..bea9c4900e8e68 100755 --- a/packages/next/src/cli/next-build.ts +++ b/packages/next/src/cli/next-build.ts @@ -10,6 +10,7 @@ import isError from '../lib/is-error' import { getProjectDir } from '../lib/get-project-dir' import { enableMemoryDebuggingMode } from '../lib/memory/startup' import { disableMemoryDebuggingMode } from '../lib/memory/shutdown' +import { parseBundlerArgs } from '../lib/bundler' export type NextBuildOptions = { debug?: boolean @@ -19,6 +20,7 @@ export type NextBuildOptions = { mangling: boolean turbo?: boolean turbopack?: boolean + webpack?: boolean experimentalDebugMemoryUsage: boolean experimentalAppOnly?: boolean experimentalTurbo?: boolean @@ -82,12 +84,7 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => { printAndExit(`> No such directory exists as the project root: ${dir}`) } - const isTurbopack = Boolean( - options.turbo || options.turbopack || process.env.IS_TURBOPACK_TEST - ) - if (isTurbopack) { - process.env.TURBOPACK = '1' - } + const bundler = parseBundlerArgs(options) return build( dir, @@ -97,7 +94,7 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => { lint, !mangling, experimentalAppOnly, - isTurbopack, + bundler, experimentalBuildMode, traceUploadUrl ) diff --git a/packages/next/src/cli/next-dev.ts b/packages/next/src/cli/next-dev.ts index 33e6e51bc6d881..b4f1e0db71f6dc 100644 --- a/packages/next/src/cli/next-dev.ts +++ b/packages/next/src/cli/next-dev.ts @@ -38,11 +38,17 @@ import { once } from 'node:events' import { clearTimeout } from 'timers' import { flushAllTraces, trace } from '../trace' import { traceId } from '../trace/shared' +import { + Bundler, + finalizeBundlerFromConfig, + parseBundlerArgs, +} from '../lib/bundler' export type NextDevOptions = { disableSourceMaps: boolean turbo?: boolean turbopack?: boolean + webpack?: boolean port: number hostname?: string experimentalHttps?: boolean @@ -58,11 +64,11 @@ let dir: string let child: undefined | ChildProcess // The config in next-dev is only used to access config.distDir for telemetry and trace. let config: NextConfigComplete -let isTurboSession = false +let bundler: Bundler let traceUploadUrl: string let sessionStopHandled = false -let sessionStarted = Date.now() -let sessionSpan = trace('next-dev') +const sessionStarted = Date.now() +const sessionSpan = trace('next-dev') // How long should we wait for the child to cleanly exit after sending // SIGINT/SIGTERM to the child process before sending SIGKILL? @@ -123,11 +129,13 @@ const handleSessionStop = async (signal: NodeJS.Signals | number | null) => { new Telemetry({ distDir: path.join(dir, config.distDir), }) + // Reading the config can modify environment variables that influence the bundler selection. + bundler = finalizeBundlerFromConfig(bundler) telemetry.record( eventCliSessionStopped({ cliCommand: 'dev', - turboFlag: isTurboSession, + turboFlag: bundler === Bundler.Turbopack, durationMilliseconds: Date.now() - sessionStarted, pagesDir, appDir, @@ -146,7 +154,7 @@ const handleSessionStop = async (signal: NodeJS.Signals | number | null) => { mode: 'dev', projectDir: dir, distDir: config.distDir, - isTurboSession, + isTurboSession: bundler === Bundler.Turbopack, }) } @@ -168,14 +176,7 @@ const nextDev = async ( portSource: PortSource, directory?: string ) => { - const isTurbopack = Boolean( - options.turbo || options.turbopack || process.env.IS_TURBOPACK_TEST - ) - if (isTurbopack) { - process.env.TURBOPACK = '1' - } - - isTurboSession = isTurbopack + bundler = parseBundlerArgs(options) dir = getProjectDir(process.env.NEXT_PRIVATE_DEV_DIR || directory) @@ -288,7 +289,9 @@ const nextDev = async ( stdio: 'inherit', env: { ...defaultEnv, - ...(isTurbopack ? { TURBOPACK: '1' } : undefined), + ...(bundler === Bundler.Turbopack + ? { TURBOPACK: process.env.TURBOPACK } + : undefined), NEXT_PRIVATE_WORKER: '1', NEXT_PRIVATE_TRACE_ID: traceId, NODE_EXTRA_CA_CERTS: startServerOptions.selfSignedCertificate @@ -336,12 +339,13 @@ const nextDev = async ( (await loadConfig(PHASE_DEVELOPMENT_SERVER, dir, { silent: true, })) + bundler = finalizeBundlerFromConfig(bundler) uploadTrace({ traceUploadUrl, mode: 'dev', projectDir: dir, distDir: config.distDir, - isTurboSession, + isTurboSession: bundler === Bundler.Turbopack, sync: true, }) } diff --git a/packages/next/src/lib/bundler.ts b/packages/next/src/lib/bundler.ts new file mode 100644 index 00000000000000..38eee0866d2e6f --- /dev/null +++ b/packages/next/src/lib/bundler.ts @@ -0,0 +1,99 @@ +/// Utilties for configuring the bundler to use. +export enum Bundler { + Turbopack, + Webpack, + Rspack, +} +/** + * Parse the bundler arguments and potentially sets the `TURBOPACK` environment variable. + * + * NOTE: rspack is configured via next config which is chaotic so it is possible for this to be overridden later. + * + * @param options The options to parse. + * @returns The bundler that was configured + */ +export function parseBundlerArgs(options: { + turbo?: boolean + turbopack?: boolean + webpack?: boolean +}): Bundler { + const bundlerFlags = new Map() + const setBundlerFlag = (bundler: Bundler, flag: string) => { + bundlerFlags.set(bundler, (bundlerFlags.get(bundler) ?? []).concat(flag)) + } + // What turbo flag was set? We allow multiple to be set, which is silly but not ambiguous, just pick the most relevant one. + if (options.turbopack) { + setBundlerFlag(Bundler.Turbopack, '--turbopack') + } + if (options.turbo) { + setBundlerFlag(Bundler.Turbopack, '--turbo') + } else if (process.env.TURBOPACK) { + // We don't really want to support this but it is trivial and not really confusing. + // If we don't support it and someone sets it, we would have inconsistent behavior + // since some parts of next would read the return value of this function and other + // parts will read the env variable. + setBundlerFlag(Bundler.Turbopack, `TURBOPACK=${process.env.TURBOPACK}`) + } else if (process.env.IS_TURBOPACK_TEST) { + setBundlerFlag( + Bundler.Turbopack, + `IS_TURBOPACK_TEST=${process.env.IS_TURBOPACK_TEST}` + ) + } + if (options.webpack) { + setBundlerFlag(Bundler.Webpack, '--webpack') + } + + if (process.env.IS_WEBPACK_TEST) { + setBundlerFlag( + Bundler.Webpack, + `IS_WEBPACK_TEST=${process.env.IS_WEBPACK_TEST}` + ) + } + + // Mostly this is set via the NextConfig but it can also be set via the command line which is + // common for testing. + if (process.env.NEXT_RSPACK) { + setBundlerFlag(Bundler.Rspack, `NEXT_RSPACK=${process.env.NEXT_RSPACK}`) + } + if (process.env.NEXT_TEST_USE_RSPACK) { + setBundlerFlag( + Bundler.Rspack, + `NEXT_TEST_USE_RSPACK=${process.env.NEXT_TEST_USE_RSPACK}` + ) + } + + if (bundlerFlags.size > 1) { + console.error( + `Multiple bundler flags set: ${Array.from(bundlerFlags.values()).flat().join(', ')}. Configure exactly one bundler.` + ) + process.exit(1) + } + // The default is turbopack when nothing is configured. + if (bundlerFlags.size === 0) { + process.env.TURBOPACK = 'auto' + return Bundler.Turbopack + } + if (bundlerFlags.has(Bundler.Turbopack)) { + // Only conditionally assign to the environment variable, preserving already set values. + // If it was set to 'auto' because no flag was set and this function is called a second time we + // would upgrade to '1' but we don't really want that. + process.env.TURBOPACK ??= '1' + return Bundler.Turbopack + } + // Otherwise it is one of rspack or webpack. At this point there must be exactly one key in the map. + return bundlerFlags.keys().next().value! +} + +/** + * Finalize the bundler based on the config. + * + * Rspack is configured via next config by setting an environment variable (yay, side effects) + * so this should only be called after parsing the config. + */ +export function finalizeBundlerFromConfig(fromOptions: Bundler) { + // Reading the next config can set NEXT_RSPACK environment variables. + if (process.env.NEXT_RSPACK) { + return Bundler.Rspack + } + return fromOptions +} diff --git a/packages/next/src/lib/turbopack-warning.ts b/packages/next/src/lib/turbopack-warning.ts index d737c3505a49a6..4858c0265b3dd4 100644 --- a/packages/next/src/lib/turbopack-warning.ts +++ b/packages/next/src/lib/turbopack-warning.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from '../server/config-shared' +import type { NextConfigComplete } from '../server/config-shared' import loadConfig from '../server/config' import * as Log from '../build/output/log' import { @@ -43,11 +43,9 @@ const unsupportedTurbopackNextConfigOptions = [ ] // The following will need to be supported by `next build --turbopack` -const unsupportedProductionSpecificTurbopackNextConfigOptions: string[] = [ - // TODO: Support disabling sourcemaps, currently they're always enabled. - // 'productionBrowserSourceMaps', -] +const unsupportedProductionSpecificTurbopackNextConfigOptions: string[] = [] +/** */ export async function validateTurboNextConfig({ dir, isDev, @@ -67,8 +65,8 @@ export async function validateTurboNextConfig({ let hasWebpackConfig = false let hasTurboConfig = false - let unsupportedConfig: string[] = [] - let rawNextConfig: NextConfig = {} + const unsupportedConfig: string[] = [] + let rawNextConfig: NextConfigComplete = {} as NextConfigComplete const phase = isDev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_BUILD try { @@ -76,7 +74,7 @@ export async function validateTurboNextConfig({ await loadConfig(phase, dir, { rawConfig: true, }) - ) as NextConfig + ) if (typeof rawNextConfig === 'function') { rawNextConfig = (rawNextConfig as any)(phase, { @@ -120,7 +118,7 @@ export async function validateTurboNextConfig({ const customKeys = flattenKeys(rawNextConfig) - let unsupportedKeys = isDev + const unsupportedKeys = isDev ? unsupportedTurbopackNextConfigOptions : [ ...unsupportedTurbopackNextConfigOptions, @@ -135,7 +133,7 @@ export async function validateTurboNextConfig({ hasTurboConfig = true } - let isUnsupported = + const isUnsupported = unsupportedKeys.some( (unsupportedKey) => // Either the key matches (or is a more specific subkey) of @@ -158,13 +156,24 @@ export async function validateTurboNextConfig({ Log.error('Unexpected error occurred while checking config', e) } - if (hasWebpackConfig && !hasTurboConfig) { - Log.warn( - `Webpack is configured while Turbopack is not, which may cause problems.` - ) - Log.warn( - `See instructions if you need to configure Turbopack:\n https://nextjs.org/docs/app/api-reference/next-config-js/turbopack\n` + // If the build was defaulted to Turbopack, we want to warn about possibly ignored webpack + // configuration. Otherwise the user explicitly picked turbopack and thus we expect that + // they have configured it correctly. + if (process.env.TURBOPACK === 'auto' && hasWebpackConfig && !hasTurboConfig) { + const configFile = rawNextConfig.configFileName ?? 'your Next config file' + Log.error( + `ERROR: This build is using Turbopack, with a \`webpack\` config and no \`turbopack\` config. This may be a mistake. + + As of Next.js 16 turbopack is enabled by default and custom webpack configurations may need to be migrated to Turbopack. + + NOTE: your \`webpack\` config may have been added by a configuration plugin. + + To configure Turbopack, see https://nextjs.org/docs/app/api-reference/next-config-js/turbopack + + TIP: Many applications work fine under Turbopack with no configuration, if that is the case for you, you can silence this error by passing the \`--turbopack\` or \`--webpack\` flag explicitly or simply setting an empty turbopack config in ${configFile} (e.g. \`turbopack: {}\`).` ) + + process.exit(1) } if (unsupportedConfig.length) { diff --git a/packages/next/src/server/next.ts b/packages/next/src/server/next.ts index 88de1cd32a45d3..2ca7d7b9bffb9a 100644 --- a/packages/next/src/server/next.ts +++ b/packages/next/src/server/next.ts @@ -540,7 +540,8 @@ function createServer( options && (options.turbo || options.turbopack || process.env.IS_TURBOPACK_TEST) ) { - process.env.TURBOPACK = '1' + // Configure TURBOPACK if it isn't already set + process.env.TURBOPACK ??= '1' } // The package is used as a TypeScript plugin. if ( diff --git a/scripts/test-new-tests.mjs b/scripts/test-new-tests.mjs index f62b101ea0f9e5..881ae9dc3b21a7 100644 --- a/scripts/test-new-tests.mjs +++ b/scripts/test-new-tests.mjs @@ -153,6 +153,7 @@ async function main() { NEXT_EXTERNAL_TESTS_FILTERS, NEXT_TEST_MODE: testMode, NEXT_TEST_VERSION: nextTestVersion, + IS_WEBPACK_TEST: '1', }, }) } @@ -170,6 +171,7 @@ async function main() { NEXT_TEST_VERSION: nextTestVersion, IS_TURBOPACK_TEST: '1', TURBOPACK_BUILD: testMode === 'start' ? '1' : undefined, + TURBOPACK_DEV: testMode === 'dev' ? '1' : undefined, }, }) } diff --git a/test/development/next-config-ts/turbo/index.test.ts b/test/development/next-config-ts/turbo/index.test.ts index f43c75d6176fdf..348568ae507ed3 100644 --- a/test/development/next-config-ts/turbo/index.test.ts +++ b/test/development/next-config-ts/turbo/index.test.ts @@ -1,13 +1,15 @@ import { nextTestSetup } from 'e2e-utils' - -describe('next-config-ts - turbopack', () => { - const { next } = nextTestSetup({ - files: __dirname, - // explicitly ensure that turbopack is used - startCommand: 'pnpm next dev --turbo', - }) - it('should work with Turbopack', async () => { - const $ = await next.render$('/') - expect($('p').text()).toBe('foo') - }) -}) +;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)( + 'next-config-ts - turbopack', + () => { + const { next } = nextTestSetup({ + files: __dirname, + // explicitly ensure that turbopack is used + startCommand: 'pnpm next dev --turbopack', + }) + it('should work with Turbopack', async () => { + const $ = await next.render$('/') + expect($('p').text()).toBe('foo') + }) + } +) diff --git a/test/e2e/config-turbopack/index.test.ts b/test/e2e/config-turbopack/index.test.ts index 36dacfd63a972f..64089fd3d01298 100644 --- a/test/e2e/config-turbopack/index.test.ts +++ b/test/e2e/config-turbopack/index.test.ts @@ -1,96 +1,142 @@ /* eslint-disable jest/no-standalone-expect */ import { nextTestSetup } from 'e2e-utils' -const WARNING_MESSAGE = 'Webpack is configured while Turbopack is not' +const WARNING_MESSAGE = + 'ERROR: This build is using Turbopack, with a `webpack` config and no `turbopack` config. This may be a mistake' const itif = (condition: boolean) => (condition ? it : it.skip) -describe('config-turbopack', () => { - describe('when webpack is configured but Turbopack is not', () => { - const { next, isTurbopack } = nextTestSetup({ - files: { - 'app/page.js': ` - export default function Page() { - return

hello world

- } - `, - 'next.config.js': ` +const page = { + 'app/page.js': ` +export default function Page() { + return

hello world

+} +`, +} + +;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)( + 'config-turbopack', + () => { + describe('when turbopack is auto selected', () => { + describe('when webpack is configured but Turbopack is not', () => { + const { next, isNextDev, isNextStart } = nextTestSetup({ + skipStart: Boolean(process.env.NEXT_TEST_MODE === 'start'), + turbo: false, + env: { + TURBOPACK: 'auto', + }, + files: { + ...page, + 'next.config.js': ` module.exports = { webpack: (config) => { return config }, } `, - }, - }) + }, + }) + + itif(isNextDev)('warns', async () => { + if (next) + try { + await next.render('/') + } catch (e) { + // we expect an error but this is the only way to get the server to crash + } - itif(isTurbopack)('warns', async () => { - if (next) await next.render('/') - expect(next.cliOutput).toContain(WARNING_MESSAGE) + expect(next.cliOutput).toContain(WARNING_MESSAGE) + }) + itif(isNextStart)('errors', async () => { + const { exitCode, cliOutput } = await next.build() + expect(exitCode).toBe(1) + expect(cliOutput).toContain(WARNING_MESSAGE) + }) + }) + // no warn cases work when auto selected too + noWarnCases() }) - }) - describe('when webpack is configured and config.turbopack is set', () => { - const { next, isTurbopack } = nextTestSetup({ - files: { - 'app/page.js': ` - export default function Page() { - return

hello world

- } - `, - 'next.config.js': ` - module.exports = { - turbopack: { - rules: { - '*.foo': { - loaders: ['foo-loader'] - } + describe('when turbopack is explicitly configured', () => { + describe('when webpack is configured but Turbopack is not', () => { + const { next } = nextTestSetup({ + files: { + ...page, + 'next.config.js': ` + module.exports = { + webpack: (config) => { + return config + }, } - }, - webpack: (config) => { - return config - }, - } - `, - }, - }) + `, + }, + }) - itif(isTurbopack)('does not warn', async () => { - if (next) await next.render('/') - expect(next.cliOutput).not.toContain(WARNING_MESSAGE) + it('does not warn', async () => { + if (next) await next.render('/') + expect(next.cliOutput).not.toContain(WARNING_MESSAGE) + }) + }) + noWarnCases() }) - }) - - describe('when webpack is configured and config.experimental.turbo is set', () => { - const { next, isTurbopack } = nextTestSetup({ - files: { - 'app/page.js': ` - export default function Page() { - return

hello world

- } - `, - 'next.config.js': ` - module.exports = { - experimental: { - turbo: { + /// These other cases don't warn because --turbopack is explicitly selected + function noWarnCases(env?: Record) { + describe('when webpack is configured and config.turbopack is set', () => { + const { next } = nextTestSetup({ + env, + files: { + ...page, + 'next.config.js': ` + module.exports = { + turbopack: { rules: { '*.foo': { loaders: ['foo-loader'] } } - } - }, - webpack: (config) => { - return config - }, - } - `, - }, - }) + }, + webpack: (config) => { + return config + }, + } + `, + }, + }) - itif(isTurbopack)('does not warn', async () => { - if (next) await next.render('/') - expect(next.cliOutput).not.toContain(WARNING_MESSAGE) - }) - }) -}) + it('does not warn', async () => { + if (next) await next.render('/') + expect(next.cliOutput).not.toContain(WARNING_MESSAGE) + }) + }) + + describe('when webpack is configured and config.experimental.turbo is set', () => { + const { next } = nextTestSetup({ + files: { + ...page, + 'next.config.js': ` + module.exports = { + experimental: { + turbo: { + rules: { + '*.foo': { + loaders: ['foo-loader'] + } + } + } + }, + webpack: (config) => { + return config + }, + } + `, + }, + }) + + it('does not warn', async () => { + if (next) await next.render('/') + expect(next.cliOutput).not.toContain(WARNING_MESSAGE) + }) + }) + } + } +)