diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json index 2ac1965e180a..b5d2a9c143ff 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -4,12 +4,14 @@ "private": true, "scripts": { "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "build:webpack": "next build --webpack > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", + "test:dev-webpack": "TEST_ENV=development-webpack playwright test", "test:build": "pnpm install && pnpm build", "test:test-build": "pnpm ts-node --script-mode assert-build.ts", - "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@latest && pnpm add react-dom@latest && pnpm build:webpack", "test:build-latest": "pnpm install && pnpm add next@latest && pnpm build", "test:build-13": "pnpm install && pnpm add next@13.5.11 && pnpm build", "test:assert": "pnpm test:test-build && pnpm test:prod && pnpm test:dev" @@ -43,7 +45,8 @@ "optionalVariants": [ { "build-command": "pnpm test:build-canary", - "label": "nextjs-app-dir (canary)" + "label": "nextjs-app-dir (canary, webpack opt-in)", + "assert-command": "pnpm test:prod && pnpm test:dev-webpack" }, { "build-command": "pnpm test:build-latest", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.mjs index c675d003853a..494df5bc5432 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.mjs @@ -5,8 +5,16 @@ if (!testEnv) { throw new Error('No test env defined'); } +const getStartCommand = () => { + if (testEnv === 'development-webpack') { + return 'pnpm next dev -p 3030 --webpack'; + } + + return testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030'; +}; + const config = getPlaywrightConfig({ - startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', + startCommand: getStartCommand(), port: 3030, }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json index 0ef1d9bbcac7..c6ad1885aecb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json @@ -4,12 +4,14 @@ "private": true, "scripts": { "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "build:webpack": "next build --webpack > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", + "test:dev-webpack": "TEST_ENV=development-webpack playwright test", "test:build": "pnpm install && pnpm build", "test:test-build": "pnpm ts-node --script-mode assert-build.ts", - "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@latest && pnpm add react-dom@latest && pnpm build:webpack", "test:build-latest": "pnpm install && pnpm add next@latest && pnpm add react@latest && pnpm add react-dom@latest && pnpm build", "test:build-13": "pnpm install && pnpm add next@13.5.11 && pnpm build", "test:assert": "pnpm test:test-build && pnpm test:prod && pnpm test:dev" @@ -43,7 +45,8 @@ "optionalVariants": [ { "build-command": "pnpm test:build-canary", - "label": "nextjs-pages-dir (canary)" + "label": "nextjs-pages-dir (canary, webpack opt-in)", + "assert-command": "pnpm test:prod && pnpm test:dev-webpack" }, { "build-command": "pnpm test:build-latest", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/playwright.config.mjs index c675d003853a..494df5bc5432 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/playwright.config.mjs @@ -5,8 +5,16 @@ if (!testEnv) { throw new Error('No test env defined'); } +const getStartCommand = () => { + if (testEnv === 'development-webpack') { + return 'pnpm next dev -p 3030 --webpack'; + } + + return testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030'; +}; + const config = getPlaywrightConfig({ - startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', + startCommand: getStartCommand(), port: 3030, }); diff --git a/packages/nextjs/src/config/util.ts b/packages/nextjs/src/config/util.ts index de8ad68cac41..40eb65e4e1e9 100644 --- a/packages/nextjs/src/config/util.ts +++ b/packages/nextjs/src/config/util.ts @@ -65,3 +65,68 @@ export function supportsProductionCompileHook(version: string): boolean { return false; } + +/** + * Checks if the current Next.js version uses Turbopack as the default bundler. + * Starting from Next.js 15.6.0-canary.38, turbopack became the default for `next build`. + * + * @param version - Next.js version string to check. + * @returns true if the version uses Turbopack by default + */ +export function isTurbopackDefaultForVersion(version: string): boolean { + if (!version) { + return false; + } + + const { major, minor, prerelease } = parseSemver(version); + + if (major === undefined || minor === undefined) { + return false; + } + + // Next.js 16+ uses turbopack by default + if (major >= 16) { + return true; + } + + // For Next.js 15, only canary versions 15.6.0-canary.40+ use turbopack by default + // Stable 15.x releases still use webpack by default + if (major === 15 && minor >= 6 && prerelease && prerelease.startsWith('canary.')) { + if (minor >= 7) { + return true; + } + const canaryNumber = parseInt(prerelease.split('.')[1] || '0', 10); + if (canaryNumber >= 40) { + return true; + } + } + + return false; +} + +/** + * Determines which bundler is actually being used based on environment variables, + * CLI flags, and Next.js version. + * + * @param nextJsVersion - The Next.js version string + * @returns 'turbopack', 'webpack', or undefined if it cannot be determined + */ +export function detectActiveBundler(nextJsVersion: string | undefined): 'turbopack' | 'webpack' | undefined { + if (process.env.TURBOPACK || process.argv.includes('--turbo')) { + return 'turbopack'; + } + + // Explicit opt-in to webpack via --webpack flag + if (process.argv.includes('--webpack')) { + return 'webpack'; + } + + // Fallback to version-based default behavior + if (nextJsVersion) { + const turbopackIsDefault = isTurbopackDefaultForVersion(nextJsVersion); + return turbopackIsDefault ? 'turbopack' : 'webpack'; + } + + // Unlikely but at this point, we just assume webpack for older behavior + return 'webpack'; +} diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index ddf761998e50..8285dda46b46 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -15,7 +15,7 @@ import type { NextConfigObject, SentryBuildOptions, } from './types'; -import { getNextjsVersion, supportsProductionCompileHook } from './util'; +import { detectActiveBundler, getNextjsVersion, supportsProductionCompileHook } from './util'; import { constructWebpackConfigFunction } from './webpack'; let showedExportModeTunnelWarning = false; @@ -258,28 +258,24 @@ function getFinalConfigObject( nextMajor = major; } - const isTurbopack = process.env.TURBOPACK; + const activeBundler = detectActiveBundler(nextJsVersion); + const isTurbopack = activeBundler === 'turbopack'; + const isWebpack = activeBundler === 'webpack'; const isTurbopackSupported = supportsProductionCompileHook(nextJsVersion ?? ''); + // Warn if using turbopack with an unsupported Next.js version if (!isTurbopackSupported && isTurbopack) { - if (process.env.NODE_ENV === 'development') { - // eslint-disable-next-line no-console - console.warn( - `[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack (\`next dev --turbopack\`). The Sentry SDK is compatible with Turbopack on Next.js version 15.4.1 or later. You are currently on ${nextJsVersion}. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack.`, - ); - } else if (process.env.NODE_ENV === 'production') { - // eslint-disable-next-line no-console - console.warn( - `[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack (\`next build --turbopack\`). The Sentry SDK is compatible with Turbopack on Next.js version 15.4.1 or later. You are currently on ${nextJsVersion}. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack.`, - ); - } + // eslint-disable-next-line no-console + console.warn( + `[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack. The Sentry SDK is compatible with Turbopack on Next.js version 15.4.1 or later. You are currently on ${nextJsVersion}. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack.`, + ); } - // webpack case + // Webpack case - warn if trying to use runAfterProductionCompile hook with unsupported Next.js version if ( userSentryOptions.useRunAfterProductionCompileHook && !supportsProductionCompileHook(nextJsVersion ?? '') && - !isTurbopack + isWebpack ) { // eslint-disable-next-line no-console console.warn( @@ -367,10 +363,9 @@ function getFinalConfigObject( ], }, }), - webpack: - isTurbopack || userSentryOptions.disableSentryWebpackConfig - ? incomingUserNextConfigObject.webpack // just return the original webpack config - : constructWebpackConfigFunction({ + ...(isWebpack && !userSentryOptions.disableSentryWebpackConfig + ? { + webpack: constructWebpackConfigFunction({ userNextConfig: incomingUserNextConfigObject, userSentryOptions, releaseName, @@ -378,6 +373,8 @@ function getFinalConfigObject( nextJsVersion, useRunAfterProductionCompileHook: shouldUseRunAfterProductionCompileHook, }), + } + : {}), ...(isTurbopackSupported && isTurbopack ? { turbopack: constructTurbopackConfig({ diff --git a/packages/nextjs/test/config/util.test.ts b/packages/nextjs/test/config/util.test.ts index b31f71705029..2dcff9889364 100644 --- a/packages/nextjs/test/config/util.test.ts +++ b/packages/nextjs/test/config/util.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import * as util from '../../src/config/util'; describe('util', () => { @@ -96,4 +96,181 @@ describe('util', () => { }); }); }); + + describe('isTurbopackDefaultForVersion', () => { + describe('returns true for versions where turbopack is default', () => { + it.each([ + // Next.js 16+ stable versions + ['16.0.0', 'Next.js 16.0.0 stable'], + ['16.0.1', 'Next.js 16.0.1 stable'], + ['16.1.0', 'Next.js 16.1.0 stable'], + ['16.2.5', 'Next.js 16.2.5 stable'], + + // Next.js 16+ pre-release versions + ['16.0.0-rc.1', 'Next.js 16.0.0-rc.1'], + ['16.0.0-canary.1', 'Next.js 16.0.0-canary.1'], + ['16.1.0-beta.2', 'Next.js 16.1.0-beta.2'], + + // Next.js 17+ + ['17.0.0', 'Next.js 17.0.0'], + ['18.0.0', 'Next.js 18.0.0'], + ['20.0.0', 'Next.js 20.0.0'], + + // Next.js 15.6.0-canary.40+ (boundary case) + ['15.6.0-canary.40', 'Next.js 15.6.0-canary.40 (exact threshold)'], + ['15.6.0-canary.41', 'Next.js 15.6.0-canary.41'], + ['15.6.0-canary.42', 'Next.js 15.6.0-canary.42'], + ['15.6.0-canary.100', 'Next.js 15.6.0-canary.100'], + + // Next.js 15.7+ canary versions + ['15.7.0-canary.1', 'Next.js 15.7.0-canary.1'], + ['15.7.0-canary.50', 'Next.js 15.7.0-canary.50'], + ['15.8.0-canary.1', 'Next.js 15.8.0-canary.1'], + ['15.10.0-canary.1', 'Next.js 15.10.0-canary.1'], + ])('returns true for %s (%s)', version => { + expect(util.isTurbopackDefaultForVersion(version)).toBe(true); + }); + }); + + describe('returns false for versions where webpack is still default', () => { + it.each([ + // Next.js 15.6.0-canary.39 and below + ['15.6.0-canary.39', 'Next.js 15.6.0-canary.39 (just below threshold)'], + ['15.6.0-canary.36', 'Next.js 15.6.0-canary.36'], + ['15.6.0-canary.38', 'Next.js 15.6.0-canary.38'], + ['15.6.0-canary.0', 'Next.js 15.6.0-canary.0'], + + // Next.js 15.6.x stable releases (NOT canary) + ['15.6.0', 'Next.js 15.6.0 stable'], + ['15.6.1', 'Next.js 15.6.1 stable'], + ['15.6.2', 'Next.js 15.6.2 stable'], + ['15.6.10', 'Next.js 15.6.10 stable'], + + // Next.js 15.6.x rc releases (NOT canary) + ['15.6.0-rc.1', 'Next.js 15.6.0-rc.1'], + ['15.6.0-rc.2', 'Next.js 15.6.0-rc.2'], + + // Next.js 15.7+ stable releases (NOT canary) + ['15.7.0', 'Next.js 15.7.0 stable'], + ['15.8.0', 'Next.js 15.8.0 stable'], + ['15.10.0', 'Next.js 15.10.0 stable'], + + // Next.js 15.5 and below (all versions) + ['15.5.0', 'Next.js 15.5.0'], + ['15.5.0-canary.100', 'Next.js 15.5.0-canary.100'], + ['15.4.1', 'Next.js 15.4.1'], + ['15.0.0', 'Next.js 15.0.0'], + ['15.0.0-canary.1', 'Next.js 15.0.0-canary.1'], + + // Next.js 14.x and below + ['14.2.0', 'Next.js 14.2.0'], + ['14.0.0', 'Next.js 14.0.0'], + ['14.0.0-canary.50', 'Next.js 14.0.0-canary.50'], + ['13.5.0', 'Next.js 13.5.0'], + ['13.0.0', 'Next.js 13.0.0'], + ['12.0.0', 'Next.js 12.0.0'], + ])('returns false for %s (%s)', version => { + expect(util.isTurbopackDefaultForVersion(version)).toBe(false); + }); + }); + + describe('edge cases', () => { + it.each([ + ['', 'empty string'], + ['invalid', 'invalid version string'], + ['15', 'missing minor and patch'], + ['15.6', 'missing patch'], + ['not.a.version', 'completely invalid'], + ['15.6.0-alpha.1', 'alpha prerelease (not canary)'], + ['15.6.0-beta.1', 'beta prerelease (not canary)'], + ])('returns false for %s (%s)', version => { + expect(util.isTurbopackDefaultForVersion(version)).toBe(false); + }); + }); + + describe('canary number parsing edge cases', () => { + it.each([ + ['15.6.0-canary.', 'canary with no number'], + ['15.6.0-canary.abc', 'canary with non-numeric value'], + ['15.6.0-canary.38.extra', 'canary with extra segments'], + ])('handles malformed canary versions: %s (%s)', version => { + // Should not throw, just return appropriate boolean + expect(() => util.isTurbopackDefaultForVersion(version)).not.toThrow(); + }); + + it('handles canary.40 exactly (boundary)', () => { + expect(util.isTurbopackDefaultForVersion('15.6.0-canary.40')).toBe(true); + }); + + it('handles canary.39 exactly (boundary)', () => { + expect(util.isTurbopackDefaultForVersion('15.6.0-canary.39')).toBe(false); + }); + }); + }); + + describe('detectActiveBundler', () => { + const originalArgv = process.argv; + const originalEnv = process.env; + + beforeEach(() => { + process.argv = [...originalArgv]; + process.env = { ...originalEnv }; + delete process.env.TURBOPACK; + }); + + afterEach(() => { + process.argv = originalArgv; + process.env = originalEnv; + }); + + it('returns turbopack when TURBOPACK env var is set', () => { + process.env.TURBOPACK = '1'; + expect(util.detectActiveBundler('15.5.0')).toBe('turbopack'); + }); + + it('returns webpack when --webpack flag is present', () => { + process.argv.push('--webpack'); + expect(util.detectActiveBundler('16.0.0')).toBe('webpack'); + }); + + it('returns turbopack for Next.js 16+ by default', () => { + expect(util.detectActiveBundler('16.0.0')).toBe('turbopack'); + expect(util.detectActiveBundler('17.0.0')).toBe('turbopack'); + }); + + it('returns turbopack for Next.js 15.6.0-canary.40+', () => { + expect(util.detectActiveBundler('15.6.0-canary.40')).toBe('turbopack'); + expect(util.detectActiveBundler('15.6.0-canary.50')).toBe('turbopack'); + }); + + it('returns webpack for Next.js 15.6.0 stable', () => { + expect(util.detectActiveBundler('15.6.0')).toBe('webpack'); + }); + + it('returns webpack for Next.js 15.5.x and below', () => { + expect(util.detectActiveBundler('15.5.0')).toBe('webpack'); + expect(util.detectActiveBundler('15.0.0')).toBe('webpack'); + expect(util.detectActiveBundler('14.2.0')).toBe('webpack'); + }); + + it('returns webpack when version is undefined', () => { + expect(util.detectActiveBundler(undefined)).toBe('webpack'); + }); + + it('prioritizes TURBOPACK env var over version detection', () => { + process.env.TURBOPACK = '1'; + expect(util.detectActiveBundler('14.0.0')).toBe('turbopack'); + }); + + it('prioritizes --webpack flag over version detection', () => { + process.argv.push('--webpack'); + expect(util.detectActiveBundler('16.0.0')).toBe('webpack'); + }); + + it('prioritizes TURBOPACK env var over --webpack flag', () => { + process.env.TURBOPACK = '1'; + process.argv.push('--webpack'); + expect(util.detectActiveBundler('15.5.0')).toBe('turbopack'); + }); + }); }); diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index b437e73dfe75..f1f46c6fc6f2 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as util from '../../src/config/util'; import { DEFAULT_SERVER_EXTERNAL_PACKAGES } from '../../src/config/withSentryConfig'; import { defaultRuntimePhase, defaultsObject, exportedNextConfig, userNextConfig } from './fixtures'; @@ -269,6 +269,280 @@ describe('withSentryConfig', () => { }); }); + describe('bundler detection with version-based defaults', () => { + const originalTurbopack = process.env.TURBOPACK; + const originalArgv = process.argv; + + beforeEach(() => { + process.argv = [...originalArgv]; + delete process.env.TURBOPACK; + }); + + afterEach(() => { + vi.restoreAllMocks(); + process.env.TURBOPACK = originalTurbopack; + process.argv = originalArgv; + }); + + describe('Next.js 16+ defaults to turbopack', () => { + it('uses turbopack config by default for Next.js 16.0.0', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeDefined(); + expect(finalConfig.webpack).toBe(exportedNextConfig.webpack); + }); + + it('uses turbopack config by default for Next.js 17.0.0', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('17.0.0'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeDefined(); + expect(finalConfig.webpack).toBe(exportedNextConfig.webpack); + }); + + it('uses webpack when --webpack flag is present on Next.js 16.0.0', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); + process.argv.push('--webpack'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }); + + it('prioritizes TURBOPACK env var over --webpack flag', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); + process.env.TURBOPACK = '1'; + process.argv.push('--webpack'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeDefined(); + expect(finalConfig.webpack).toBe(exportedNextConfig.webpack); + }); + }); + + describe('Next.js 15.6.0-canary.40+ defaults to turbopack', () => { + it('uses turbopack config by default for 15.6.0-canary.40', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.6.0-canary.40'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeDefined(); + expect(finalConfig.webpack).toBe(exportedNextConfig.webpack); + }); + + it('uses turbopack config by default for 15.6.0-canary.50', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.6.0-canary.50'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeDefined(); + expect(finalConfig.webpack).toBe(exportedNextConfig.webpack); + }); + + it('uses turbopack config by default for 15.7.0-canary.1', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.7.0-canary.1'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeDefined(); + expect(finalConfig.webpack).toBe(exportedNextConfig.webpack); + }); + + it('uses webpack when --webpack flag is present on 15.6.0-canary.40', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.6.0-canary.40'); + process.argv.push('--webpack'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }); + + it('uses webpack when --webpack flag is present on 15.7.0-canary.1', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.7.0-canary.1'); + process.argv.push('--webpack'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }); + }); + + describe('Next.js 15.6.0-canary.37 and below defaults to webpack', () => { + it('uses webpack config by default for 15.6.0-canary.37', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.6.0-canary.37'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }); + + it('uses webpack config by default for 15.6.0-canary.1', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.6.0-canary.1'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }); + + it('uses turbopack when TURBOPACK env var is set on 15.6.0-canary.37', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.6.0-canary.37'); + process.env.TURBOPACK = '1'; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeDefined(); + expect(finalConfig.webpack).toBe(exportedNextConfig.webpack); + }); + }); + + describe('Next.js 15.6.x stable releases default to webpack', () => { + it('uses webpack config by default for 15.6.0 stable', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.6.0'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }); + + it('uses webpack config by default for 15.6.1 stable', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.6.1'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }); + + it('uses webpack config by default for 15.7.0 stable', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.7.0'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }); + + it('uses turbopack when explicitly requested via env var on 15.6.0 stable', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.6.0'); + process.env.TURBOPACK = '1'; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeDefined(); + expect(finalConfig.webpack).toBe(exportedNextConfig.webpack); + }); + }); + + describe('older Next.js versions default to webpack', () => { + it.each([['15.5.0'], ['15.0.0'], ['14.2.0'], ['13.5.0']])( + 'uses webpack config by default for Next.js %s', + version => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue(version); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }, + ); + + it.each([['15.5.0-canary.100'], ['15.0.0-canary.1'], ['14.2.0-canary.50']])( + 'uses webpack config by default for Next.js %s canary', + version => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue(version); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }, + ); + }); + + describe('warnings are shown for unsupported turbopack usage', () => { + let consoleWarnSpy: ReturnType; + + beforeEach(() => { + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + it('warns when using turbopack on unsupported version', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0'); + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(false); + process.env.TURBOPACK = '1'; + + materializeFinalNextConfig(exportedNextConfig); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('WARNING: You are using the Sentry SDK with Turbopack'), + ); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('15.0.0')); + }); + + it('does not warn when using turbopack on supported version', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + process.env.TURBOPACK = '1'; + + materializeFinalNextConfig(exportedNextConfig); + + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + + it('does not warn when using webpack', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0'); + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(false); + + materializeFinalNextConfig(exportedNextConfig); + + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + }); + + describe('edge cases', () => { + it('defaults to webpack when Next.js version cannot be determined', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue(undefined); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }); + + it('uses turbopack when TURBOPACK env var is set even when version is undefined', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue(undefined); + process.env.TURBOPACK = '1'; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + // Note: turbopack config won't be added when version is undefined because + // isTurbopackSupported will be false, but webpack config should still be skipped + expect(finalConfig.webpack).toBe(exportedNextConfig.webpack); + // Turbopack config is only added when both isTurbopack AND isTurbopackSupported are true + expect(finalConfig.turbopack).toBeUndefined(); + }); + + it('handles malformed version strings gracefully', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('not.a.version'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.turbopack).toBeUndefined(); + expect(finalConfig.webpack).toBeInstanceOf(Function); + }); + }); + }); + describe('turbopack sourcemap configuration', () => { const originalTurbopack = process.env.TURBOPACK; @@ -994,7 +1268,7 @@ describe('withSentryConfig', () => { materializeFinalNextConfig(exportedNextConfig); expect(consoleWarnSpy).toHaveBeenCalledWith( - '[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack (`next dev --turbopack`). The Sentry SDK is compatible with Turbopack on Next.js version 15.4.1 or later. You are currently on 15.4.0. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack.', + '[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack. The Sentry SDK is compatible with Turbopack on Next.js version 15.4.1 or later. You are currently on 15.4.0. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack.', ); consoleWarnSpy.mockRestore(); @@ -1011,7 +1285,7 @@ describe('withSentryConfig', () => { materializeFinalNextConfig(exportedNextConfig); expect(consoleWarnSpy).toHaveBeenCalledWith( - '[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack (`next build --turbopack`). The Sentry SDK is compatible with Turbopack on Next.js version 15.4.1 or later. You are currently on 15.3.9. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack.', + '[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack. The Sentry SDK is compatible with Turbopack on Next.js version 15.4.1 or later. You are currently on 15.3.9. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack.', ); consoleWarnSpy.mockRestore(); @@ -1115,24 +1389,7 @@ describe('withSentryConfig', () => { materializeFinalNextConfig(exportedNextConfig); expect(consoleWarnSpy).toHaveBeenCalledWith( - '[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack (`next dev --turbopack`). The Sentry SDK is compatible with Turbopack on Next.js version 15.4.1 or later. You are currently on 15.4.0-canary.15. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack.', - ); - - consoleWarnSpy.mockRestore(); - }); - - it('does not warn in other environments besides development and production', () => { - process.env.TURBOPACK = '1'; - // @ts-expect-error - NODE_ENV is read-only in types but we need to set it for testing - process.env.NODE_ENV = 'test'; - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1'); - vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(false); - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - materializeFinalNextConfig(exportedNextConfig); - - expect(consoleWarnSpy).not.toHaveBeenCalledWith( - expect.stringContaining('WARNING: You are using the Sentry SDK with Turbopack'), + '[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack. The Sentry SDK is compatible with Turbopack on Next.js version 15.4.1 or later. You are currently on 15.4.0-canary.15. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack.', ); consoleWarnSpy.mockRestore();