Skip to content

Commit d209fac

Browse files
committed
add new bundler detection
1 parent a524022 commit d209fac

File tree

2 files changed

+243
-1
lines changed

2 files changed

+243
-1
lines changed

packages/nextjs/src/config/util.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,68 @@ export function supportsProductionCompileHook(version: string): boolean {
6565

6666
return false;
6767
}
68+
69+
/**
70+
* Checks if the current Next.js version uses Turbopack as the default bundler.
71+
* Starting from Next.js 15.6.0-canary.38, turbopack became the default for `next build`.
72+
*
73+
* @param version - Next.js version string to check.
74+
* @returns true if the version uses Turbopack by default
75+
*/
76+
export function isTurbopackDefaultForVersion(version: string): boolean {
77+
if (!version) {
78+
return false;
79+
}
80+
81+
const { major, minor, prerelease } = parseSemver(version);
82+
83+
if (major === undefined || minor === undefined) {
84+
return false;
85+
}
86+
87+
// Next.js 16+ uses turbopack by default
88+
if (major >= 16) {
89+
return true;
90+
}
91+
92+
// For Next.js 15, only canary versions 15.6.0-canary.38+ use turbopack by default
93+
// Stable 15.x releases still use webpack by default
94+
if (major === 15 && minor >= 6 && prerelease && prerelease.startsWith('canary.')) {
95+
if (minor >= 7) {
96+
return true;
97+
}
98+
const canaryNumber = parseInt(prerelease.split('.')[1] || '0', 10);
99+
if (canaryNumber >= 38) {
100+
return true;
101+
}
102+
}
103+
104+
return false;
105+
}
106+
107+
/**
108+
* Determines which bundler is actually being used based on environment variables,
109+
* CLI flags, and Next.js version.
110+
*
111+
* @param nextJsVersion - The Next.js version string
112+
* @returns 'turbopack', 'webpack', or undefined if it cannot be determined
113+
*/
114+
export function detectActiveBundler(nextJsVersion: string | undefined): 'turbopack' | 'webpack' | undefined {
115+
if (process.env.TURBOPACK || process.argv.includes('--turbo')) {
116+
return 'turbopack';
117+
}
118+
119+
// Explicit opt-in to webpack via --webpack flag
120+
if (process.argv.includes('--webpack')) {
121+
return 'webpack';
122+
}
123+
124+
// Fallback to version-based default behavior
125+
if (nextJsVersion) {
126+
const turbopackIsDefault = isTurbopackDefaultForVersion(nextJsVersion);
127+
return turbopackIsDefault ? 'turbopack' : 'webpack';
128+
}
129+
130+
// Unlikely but at this point, we just assume webpack for older behavior
131+
return 'webpack';
132+
}

packages/nextjs/test/config/util.test.ts

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
22
import * as util from '../../src/config/util';
33

44
describe('util', () => {
@@ -96,4 +96,181 @@ describe('util', () => {
9696
});
9797
});
9898
});
99+
100+
describe('isTurbopackDefaultForVersion', () => {
101+
describe('returns true for versions where turbopack is default', () => {
102+
it.each([
103+
// Next.js 16+ stable versions
104+
['16.0.0', 'Next.js 16.0.0 stable'],
105+
['16.0.1', 'Next.js 16.0.1 stable'],
106+
['16.1.0', 'Next.js 16.1.0 stable'],
107+
['16.2.5', 'Next.js 16.2.5 stable'],
108+
109+
// Next.js 16+ pre-release versions
110+
['16.0.0-rc.1', 'Next.js 16.0.0-rc.1'],
111+
['16.0.0-canary.1', 'Next.js 16.0.0-canary.1'],
112+
['16.1.0-beta.2', 'Next.js 16.1.0-beta.2'],
113+
114+
// Next.js 17+
115+
['17.0.0', 'Next.js 17.0.0'],
116+
['18.0.0', 'Next.js 18.0.0'],
117+
['20.0.0', 'Next.js 20.0.0'],
118+
119+
// Next.js 15.6.0-canary.38+ (boundary case)
120+
['15.6.0-canary.38', 'Next.js 15.6.0-canary.38 (exact threshold)'],
121+
['15.6.0-canary.39', 'Next.js 15.6.0-canary.39'],
122+
['15.6.0-canary.40', 'Next.js 15.6.0-canary.40'],
123+
['15.6.0-canary.100', 'Next.js 15.6.0-canary.100'],
124+
125+
// Next.js 15.7+ canary versions
126+
['15.7.0-canary.1', 'Next.js 15.7.0-canary.1'],
127+
['15.7.0-canary.50', 'Next.js 15.7.0-canary.50'],
128+
['15.8.0-canary.1', 'Next.js 15.8.0-canary.1'],
129+
['15.10.0-canary.1', 'Next.js 15.10.0-canary.1'],
130+
])('returns true for %s (%s)', version => {
131+
expect(util.isTurbopackDefaultForVersion(version)).toBe(true);
132+
});
133+
});
134+
135+
describe('returns false for versions where webpack is still default', () => {
136+
it.each([
137+
// Next.js 15.6.0-canary.37 and below
138+
['15.6.0-canary.37', 'Next.js 15.6.0-canary.37 (just below threshold)'],
139+
['15.6.0-canary.36', 'Next.js 15.6.0-canary.36'],
140+
['15.6.0-canary.1', 'Next.js 15.6.0-canary.1'],
141+
['15.6.0-canary.0', 'Next.js 15.6.0-canary.0'],
142+
143+
// Next.js 15.6.x stable releases (NOT canary)
144+
['15.6.0', 'Next.js 15.6.0 stable'],
145+
['15.6.1', 'Next.js 15.6.1 stable'],
146+
['15.6.2', 'Next.js 15.6.2 stable'],
147+
['15.6.10', 'Next.js 15.6.10 stable'],
148+
149+
// Next.js 15.6.x rc releases (NOT canary)
150+
['15.6.0-rc.1', 'Next.js 15.6.0-rc.1'],
151+
['15.6.0-rc.2', 'Next.js 15.6.0-rc.2'],
152+
153+
// Next.js 15.7+ stable releases (NOT canary)
154+
['15.7.0', 'Next.js 15.7.0 stable'],
155+
['15.8.0', 'Next.js 15.8.0 stable'],
156+
['15.10.0', 'Next.js 15.10.0 stable'],
157+
158+
// Next.js 15.5 and below (all versions)
159+
['15.5.0', 'Next.js 15.5.0'],
160+
['15.5.0-canary.100', 'Next.js 15.5.0-canary.100'],
161+
['15.4.1', 'Next.js 15.4.1'],
162+
['15.0.0', 'Next.js 15.0.0'],
163+
['15.0.0-canary.1', 'Next.js 15.0.0-canary.1'],
164+
165+
// Next.js 14.x and below
166+
['14.2.0', 'Next.js 14.2.0'],
167+
['14.0.0', 'Next.js 14.0.0'],
168+
['14.0.0-canary.50', 'Next.js 14.0.0-canary.50'],
169+
['13.5.0', 'Next.js 13.5.0'],
170+
['13.0.0', 'Next.js 13.0.0'],
171+
['12.0.0', 'Next.js 12.0.0'],
172+
])('returns false for %s (%s)', version => {
173+
expect(util.isTurbopackDefaultForVersion(version)).toBe(false);
174+
});
175+
});
176+
177+
describe('edge cases', () => {
178+
it.each([
179+
['', 'empty string'],
180+
['invalid', 'invalid version string'],
181+
['15', 'missing minor and patch'],
182+
['15.6', 'missing patch'],
183+
['not.a.version', 'completely invalid'],
184+
['15.6.0-alpha.1', 'alpha prerelease (not canary)'],
185+
['15.6.0-beta.1', 'beta prerelease (not canary)'],
186+
])('returns false for %s (%s)', version => {
187+
expect(util.isTurbopackDefaultForVersion(version)).toBe(false);
188+
});
189+
});
190+
191+
describe('canary number parsing edge cases', () => {
192+
it.each([
193+
['15.6.0-canary.', 'canary with no number'],
194+
['15.6.0-canary.abc', 'canary with non-numeric value'],
195+
['15.6.0-canary.38.extra', 'canary with extra segments'],
196+
])('handles malformed canary versions: %s (%s)', version => {
197+
// Should not throw, just return appropriate boolean
198+
expect(() => util.isTurbopackDefaultForVersion(version)).not.toThrow();
199+
});
200+
201+
it('handles canary.38 exactly (boundary)', () => {
202+
expect(util.isTurbopackDefaultForVersion('15.6.0-canary.38')).toBe(true);
203+
});
204+
205+
it('handles canary.37 exactly (boundary)', () => {
206+
expect(util.isTurbopackDefaultForVersion('15.6.0-canary.37')).toBe(false);
207+
});
208+
});
209+
});
210+
211+
describe('detectActiveBundler', () => {
212+
const originalArgv = process.argv;
213+
const originalEnv = process.env;
214+
215+
beforeEach(() => {
216+
process.argv = [...originalArgv];
217+
process.env = { ...originalEnv };
218+
delete process.env.TURBOPACK;
219+
});
220+
221+
afterEach(() => {
222+
process.argv = originalArgv;
223+
process.env = originalEnv;
224+
});
225+
226+
it('returns turbopack when TURBOPACK env var is set', () => {
227+
process.env.TURBOPACK = '1';
228+
expect(util.detectActiveBundler('15.5.0')).toBe('turbopack');
229+
});
230+
231+
it('returns webpack when --webpack flag is present', () => {
232+
process.argv.push('--webpack');
233+
expect(util.detectActiveBundler('16.0.0')).toBe('webpack');
234+
});
235+
236+
it('returns turbopack for Next.js 16+ by default', () => {
237+
expect(util.detectActiveBundler('16.0.0')).toBe('turbopack');
238+
expect(util.detectActiveBundler('17.0.0')).toBe('turbopack');
239+
});
240+
241+
it('returns turbopack for Next.js 15.6.0-canary.38+', () => {
242+
expect(util.detectActiveBundler('15.6.0-canary.38')).toBe('turbopack');
243+
expect(util.detectActiveBundler('15.6.0-canary.50')).toBe('turbopack');
244+
});
245+
246+
it('returns webpack for Next.js 15.6.0 stable', () => {
247+
expect(util.detectActiveBundler('15.6.0')).toBe('webpack');
248+
});
249+
250+
it('returns webpack for Next.js 15.5.x and below', () => {
251+
expect(util.detectActiveBundler('15.5.0')).toBe('webpack');
252+
expect(util.detectActiveBundler('15.0.0')).toBe('webpack');
253+
expect(util.detectActiveBundler('14.2.0')).toBe('webpack');
254+
});
255+
256+
it('returns webpack when version is undefined', () => {
257+
expect(util.detectActiveBundler(undefined)).toBe('webpack');
258+
});
259+
260+
it('prioritizes TURBOPACK env var over version detection', () => {
261+
process.env.TURBOPACK = '1';
262+
expect(util.detectActiveBundler('14.0.0')).toBe('turbopack');
263+
});
264+
265+
it('prioritizes --webpack flag over version detection', () => {
266+
process.argv.push('--webpack');
267+
expect(util.detectActiveBundler('16.0.0')).toBe('webpack');
268+
});
269+
270+
it('prioritizes TURBOPACK env var over --webpack flag', () => {
271+
process.env.TURBOPACK = '1';
272+
process.argv.push('--webpack');
273+
expect(util.detectActiveBundler('15.5.0')).toBe('turbopack');
274+
});
275+
});
99276
});

0 commit comments

Comments
 (0)