Skip to content
Next Next commit
add new bundler detection
  • Loading branch information
chargome committed Oct 6, 2025
commit d209facae4b09681516590d3c319fc88d414b041
65 changes: 65 additions & 0 deletions packages/nextjs/src/config/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.38+ 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 >= 38) {
return true;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Turbopack Version Threshold Mismatch

The isTurbopackDefaultForVersion function has inconsistencies in determining Turbopack as default for Next.js 15.x canary versions. The code's 15.6.0-canary.40+ threshold conflicts with a comment stating 15.6.0-canary.38, leading to incorrect identification for 15.6.0-canary.38 and 15.6.0-canary.39. Additionally, a comment inaccurately restricts the default to 15.6.0-canary.40+, while the logic correctly includes all 15.7.x-canary versions.

Additional Locations (1)

Fix in Cursor Fix in Web


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';
}
Comment on lines +114 to +132
Copy link
Copy Markdown

@kevva kevva Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chargome, this is incorrectly returning turbopack for me. I'm running Next.js with Rspack and had to do some shenanigans to make it work after they changed the default bundler to Turbopack. So what I'm running is this:

NEXT_RSPACK=true next dev
NEXT_RSPACK=true next build
NEXT_RSPACK=true next start

I noticed our source maps stopped working in 10.20.0 and narrowed it down to this.

Copy link
Copy Markdown

@kevva kevva Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a PR for this in #17971.

179 changes: 178 additions & 1 deletion packages/nextjs/test/config/util.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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.38+ (boundary case)
['15.6.0-canary.38', 'Next.js 15.6.0-canary.38 (exact threshold)'],
['15.6.0-canary.39', 'Next.js 15.6.0-canary.39'],
['15.6.0-canary.40', 'Next.js 15.6.0-canary.40'],
['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.37 and below
['15.6.0-canary.37', 'Next.js 15.6.0-canary.37 (just below threshold)'],
['15.6.0-canary.36', 'Next.js 15.6.0-canary.36'],
['15.6.0-canary.1', 'Next.js 15.6.0-canary.1'],
['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.38 exactly (boundary)', () => {
expect(util.isTurbopackDefaultForVersion('15.6.0-canary.38')).toBe(true);
});

it('handles canary.37 exactly (boundary)', () => {
expect(util.isTurbopackDefaultForVersion('15.6.0-canary.37')).toBe(false);
});
});
});

describe('detectActiveBundler', () => {
const originalArgv = process.argv;
const originalEnv = process.env;

beforeEach(() => {
process.argv = [...originalArgv];
Comment thread
chargome marked this conversation as resolved.
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.38+', () => {
expect(util.detectActiveBundler('15.6.0-canary.38')).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');
});
});
});