Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as path from 'path';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type { JSONValue, TurbopackMatcherWithRule } from '../types';
import { getPackageModules } from '../util';
import { getPackageModules, supportsTurbopackRuleCondition } from '../util';

/**
* Generate the value injection rules for client and server in turbopack config.
Expand Down Expand Up @@ -50,11 +50,16 @@ export function generateValueInjectionRules({
serverValues = { ...serverValues, ...isomorphicValues };
}

const hasConditionSupport = nextJsVersion ? supportsTurbopackRuleCondition(nextJsVersion) : false;

// Client value injection
if (Object.keys(clientValues).length > 0) {
rules.push({
matcher: '**/instrumentation-client.*',
rule: {
// Only run on user code, not node_modules or Next.js internals
// condition field is only supported in Next.js 16+
...(hasConditionSupport ? { condition: { not: 'foreign' } } : {}),
loaders: [
{
loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
Expand All @@ -72,6 +77,9 @@ export function generateValueInjectionRules({
rules.push({
matcher: '**/instrumentation.*',
rule: {
// Only run on user code, not node_modules or Next.js internals
// condition field is only supported in Next.js 16+
...(hasConditionSupport ? { condition: { not: 'foreign' } } : {}),
loaders: [
{
loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
Expand Down
12 changes: 12 additions & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,17 @@ type TurbopackRuleCondition = {
path: string | RegExp;
};

// Condition used to filter when a loader rule applies.
// Supports built-in string conditions ('foreign', 'browser', 'development', 'production', 'node', 'edge-light')
// and boolean operators matching the Turbopack advanced condition syntax.
type TurbopackRuleConditionFilter =
| string
| { not: TurbopackRuleConditionFilter }
| { all: TurbopackRuleConditionFilter[] }
| { any: TurbopackRuleConditionFilter[] }
| { path: string | RegExp }
| { content: RegExp };

export type TurbopackRuleConfigItemOrShortcut = TurbopackLoaderItem[] | TurbopackRuleConfigItem;

export type TurbopackMatcherWithRule = {
Expand All @@ -859,6 +870,7 @@ export type TurbopackMatcherWithRule = {
type TurbopackRuleConfigItemOptions = {
loaders: TurbopackLoaderItem[];
as?: string;
condition?: TurbopackRuleConditionFilter;
};

type TurbopackRuleConfigItem =
Expand Down
21 changes: 21 additions & 0 deletions packages/nextjs/src/config/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,27 @@ export function supportsProductionCompileHook(version: string): boolean {
return false;
}

/**
* Checks if the current Next.js version supports the `condition` field in Turbopack rules.
* This field was introduced in Next.js 16.
*
* @param version - version string to check.
* @returns true if Next.js version is 16 or higher
*/
export function supportsTurbopackRuleCondition(version: string): boolean {
if (!version) {
return false;
}

const { major } = parseSemver(version);

if (major === undefined) {
return false;
}

return major >= 16;
}
Copy link

Choose a reason for hiding this comment

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

Condition gating may fail on canaries

Medium Severity

supportsTurbopackRuleCondition gates on parseSemver(version).major >= 16, which may return undefined for common Next.js version formats like canary/prerelease strings, causing the condition field to be omitted even when Turbopack supports it.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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


/**
* Checks if the current Next.js version supports native debug ids for turbopack.
* This feature was first introduced in Next.js v15.6.0-canary.36 and marked stable in Next.js v16
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,68 @@ describe('constructTurbopackConfig', () => {
});
});

describe('condition field version gating', () => {
it('should include condition field for Next.js 16+', () => {
const userNextConfig: NextConfigObject = {};

const result = constructTurbopackConfig({
userNextConfig,
nextJsVersion: '16.0.0',
});

const serverRule = result.rules!['**/instrumentation.*'] as { condition?: unknown; loaders: unknown[] };
expect(serverRule.condition).toEqual({ not: 'foreign' });
});

it('should include condition field for Next.js 17+', () => {
const userNextConfig: NextConfigObject = {};

const result = constructTurbopackConfig({
userNextConfig,
routeManifest: { dynamicRoutes: [], staticRoutes: [], isrRoutes: [] },
nextJsVersion: '17.0.0',
});

const clientRule = result.rules!['**/instrumentation-client.*'] as { condition?: unknown; loaders: unknown[] };
const serverRule = result.rules!['**/instrumentation.*'] as { condition?: unknown; loaders: unknown[] };
expect(clientRule.condition).toEqual({ not: 'foreign' });
expect(serverRule.condition).toEqual({ not: 'foreign' });
});

it('should not include condition field for Next.js 15.x', () => {
const userNextConfig: NextConfigObject = {};

const result = constructTurbopackConfig({
userNextConfig,
nextJsVersion: '15.4.1',
});

const serverRule = result.rules!['**/instrumentation.*'] as { condition?: unknown; loaders: unknown[] };
expect(serverRule).not.toHaveProperty('condition');
});

it('should not include condition field for Next.js 14.x', () => {
const userNextConfig: NextConfigObject = {};

const result = constructTurbopackConfig({
userNextConfig,
nextJsVersion: '14.2.0',
});

const serverRule = result.rules!['**/instrumentation.*'] as { condition?: unknown; loaders: unknown[] };
expect(serverRule).not.toHaveProperty('condition');
});

it('should not include condition field when nextJsVersion is undefined', () => {
const userNextConfig: NextConfigObject = {};

const result = constructTurbopackConfig({ userNextConfig });

const serverRule = result.rules!['**/instrumentation.*'] as { condition?: unknown; loaders: unknown[] };
expect(serverRule).not.toHaveProperty('condition');
});
});

describe('safelyAddTurbopackRule', () => {
const mockRule = {
loaders: [
Expand Down
35 changes: 35 additions & 0 deletions packages/nextjs/test/config/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,39 @@ describe('util', () => {
expect(util.detectActiveBundler()).toBe('webpack');
});
});

describe('supportsTurbopackRuleCondition', () => {
describe('supported versions (returns true)', () => {
it.each([
['16.0.0', 'Next.js 16.0.0'],
['16.1.0', 'Next.js 16.1.0'],
['17.0.0', 'Next.js 17.0.0'],
['20.0.0', 'Next.js 20.0.0'],
])('returns true for %s (%s)', version => {
expect(util.supportsTurbopackRuleCondition(version)).toBe(true);
});
});

describe('unsupported versions (returns false)', () => {
it.each([
['15.9.9', 'Next.js 15.9.9'],
['15.4.1', 'Next.js 15.4.1 (min Turbopack version)'],
['15.0.0', 'Next.js 15.0.0'],
['14.2.0', 'Next.js 14.2.0'],
['13.0.0', 'Next.js 13.0.0'],
])('returns false for %s (%s)', version => {
expect(util.supportsTurbopackRuleCondition(version)).toBe(false);
});
});

describe('edge cases', () => {
it('returns false for empty string', () => {
expect(util.supportsTurbopackRuleCondition('')).toBe(false);
});

it('returns false for invalid version string', () => {
expect(util.supportsTurbopackRuleCondition('invalid')).toBe(false);
});
});
});
});
Loading