Skip to content

Commit 952dae3

Browse files
authored
fix(nextjs): Populate __SENTRY_SERVER_MODULES__ in Turbopack (#19231)
Turbopack was missing `__SENTRY_SERVER_MODULES__` injection, causing modulesIntegration to return empty in Next.js v16. This broke auto detection for integrations that check module availability. Changes matche webpack's existing behavior by reading package.json at build time and injecting dependencies into the bundle. closes: #19147
1 parent 618420b commit 952dae3

File tree

5 files changed

+268
-36
lines changed

5 files changed

+268
-36
lines changed

packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from 'path';
22
import type { VercelCronsConfig } from '../../common/types';
33
import type { RouteManifest } from '../manifest/types';
44
import type { JSONValue, TurbopackMatcherWithRule } from '../types';
5+
import { getPackageModules } from '../util';
56

67
/**
78
* Generate the value injection rules for client and server in turbopack config.
@@ -40,6 +41,9 @@ export function generateValueInjectionRules({
4041
if (vercelCronsConfig) {
4142
serverValues._sentryVercelCronsConfig = JSON.stringify(vercelCronsConfig);
4243
}
44+
// Inject server modules (matching webpack's __SENTRY_SERVER_MODULES__ behavior)
45+
// Use process.cwd() to get the project directory at build time
46+
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());
4347

4448
if (Object.keys(isomorphicValues).length > 0) {
4549
clientValues = { ...clientValues, ...isomorphicValues };

packages/nextjs/src/config/util.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { parseSemver } from '@sentry/core';
22
import * as fs from 'fs';
33
import { createRequire } from 'module';
4+
import * as path from 'path';
45

56
/**
67
* Returns the version of Next.js installed in the project, or undefined if it cannot be determined.
@@ -181,3 +182,24 @@ export function detectActiveBundler(): 'turbopack' | 'webpack' {
181182
return 'webpack';
182183
}
183184
}
185+
186+
/**
187+
* Extract modules from project directory's package.json
188+
*/
189+
export function getPackageModules(projectDir: string): Record<string, string> {
190+
try {
191+
const packageJson = path.join(projectDir, 'package.json');
192+
const packageJsonContent = fs.readFileSync(packageJson, 'utf8');
193+
const packageJsonObject = JSON.parse(packageJsonContent) as {
194+
dependencies?: Record<string, string>;
195+
devDependencies?: Record<string, string>;
196+
};
197+
198+
return {
199+
...packageJsonObject.dependencies,
200+
...packageJsonObject.devDependencies,
201+
};
202+
} catch {
203+
return {};
204+
}
205+
}

packages/nextjs/src/config/webpack.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type {
2121
WebpackConfigObjectWithModuleRules,
2222
WebpackEntryProperty,
2323
} from './types';
24-
import { getNextjsVersion } from './util';
24+
import { getNextjsVersion, getPackageModules } from './util';
2525
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';
2626

2727
// Next.js runs webpack 3 times, once for the client, the server, and for edge. Because we don't want to print certain
@@ -425,7 +425,7 @@ export function constructWebpackConfigFunction({
425425
newConfig.plugins = newConfig.plugins || [];
426426
newConfig.plugins.push(
427427
new buildContext.webpack.DefinePlugin({
428-
__SENTRY_SERVER_MODULES__: JSON.stringify(_getModules(projectDir)),
428+
__SENTRY_SERVER_MODULES__: JSON.stringify(getPackageModules(projectDir)),
429429
}),
430430
);
431431

@@ -883,24 +883,6 @@ function addEdgeRuntimePolyfills(newConfig: WebpackConfigObjectWithModuleRules,
883883
};
884884
}
885885

886-
function _getModules(projectDir: string): Record<string, string> {
887-
try {
888-
const packageJson = path.join(projectDir, 'package.json');
889-
const packageJsonContent = fs.readFileSync(packageJson, 'utf8');
890-
const packageJsonObject = JSON.parse(packageJsonContent) as {
891-
dependencies?: Record<string, string>;
892-
devDependencies?: Record<string, string>;
893-
};
894-
895-
return {
896-
...packageJsonObject.dependencies,
897-
...packageJsonObject.devDependencies,
898-
};
899-
} catch {
900-
return {};
901-
}
902-
}
903-
904886
/**
905887
* Sets up the tree-shaking flags based on the user's configuration.
906888
* https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/

packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts

Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe('constructTurbopackConfig', () => {
2323
{ path: '/users', regex: '/users' },
2424
{ path: '/api/health', regex: '/api/health' },
2525
],
26+
isrRoutes: [],
2627
};
2728

2829
const mockSentryOptions = {};
@@ -36,7 +37,22 @@ describe('constructTurbopackConfig', () => {
3637
userSentryOptions: mockSentryOptions,
3738
});
3839

39-
expect(result).toEqual({});
40+
expect(result).toEqual({
41+
rules: {
42+
'**/instrumentation.*': {
43+
loaders: [
44+
{
45+
loader: '/mocked/path/to/valueInjectionLoader.js',
46+
options: {
47+
values: {
48+
__SENTRY_SERVER_MODULES__: expect.any(Object),
49+
},
50+
},
51+
},
52+
],
53+
},
54+
},
55+
});
4056
});
4157

4258
it('should create turbopack config with instrumentation rule when manifest is provided', () => {
@@ -61,6 +77,18 @@ describe('constructTurbopackConfig', () => {
6177
},
6278
],
6379
},
80+
'**/instrumentation.*': {
81+
loaders: [
82+
{
83+
loader: '/mocked/path/to/valueInjectionLoader.js',
84+
options: {
85+
values: {
86+
__SENTRY_SERVER_MODULES__: expect.any(Object),
87+
},
88+
},
89+
},
90+
],
91+
},
6492
},
6593
});
6694
});
@@ -135,6 +163,18 @@ describe('constructTurbopackConfig', () => {
135163
},
136164
rules: {
137165
'*.test.js': ['jest-loader'],
166+
'**/instrumentation.*': {
167+
loaders: [
168+
{
169+
loader: '/mocked/path/to/valueInjectionLoader.js',
170+
options: {
171+
values: {
172+
__SENTRY_SERVER_MODULES__: expect.any(Object),
173+
},
174+
},
175+
},
176+
],
177+
},
138178
},
139179
});
140180
});
@@ -174,6 +214,18 @@ describe('constructTurbopackConfig', () => {
174214
},
175215
],
176216
},
217+
'**/instrumentation.*': {
218+
loaders: [
219+
{
220+
loader: '/mocked/path/to/valueInjectionLoader.js',
221+
options: {
222+
values: {
223+
__SENTRY_SERVER_MODULES__: expect.any(Object),
224+
},
225+
},
226+
},
227+
],
228+
},
177229
},
178230
});
179231
});
@@ -224,7 +276,7 @@ describe('constructTurbopackConfig', () => {
224276
describe('with edge cases', () => {
225277
it('should handle empty route manifest', () => {
226278
const userNextConfig: NextConfigObject = {};
227-
const emptyManifest: RouteManifest = { dynamicRoutes: [], staticRoutes: [] };
279+
const emptyManifest: RouteManifest = { dynamicRoutes: [], staticRoutes: [], isrRoutes: [] };
228280

229281
const result = constructTurbopackConfig({
230282
userNextConfig,
@@ -245,13 +297,26 @@ describe('constructTurbopackConfig', () => {
245297
},
246298
],
247299
},
300+
'**/instrumentation.*': {
301+
loaders: [
302+
{
303+
loader: '/mocked/path/to/valueInjectionLoader.js',
304+
options: {
305+
values: {
306+
__SENTRY_SERVER_MODULES__: expect.any(Object),
307+
},
308+
},
309+
},
310+
],
311+
},
248312
},
249313
});
250314
});
251315

252316
it('should handle complex route manifest', () => {
253317
const userNextConfig: NextConfigObject = {};
254318
const complexManifest: RouteManifest = {
319+
isrRoutes: [],
255320
dynamicRoutes: [
256321
{ path: '/users/[id]/posts/[postId]', regex: '/users/([^/]+)/posts/([^/]+)', paramNames: ['id', 'postId'] },
257322
{ path: '/api/[...params]', regex: '/api/(.+)', paramNames: ['params'] },
@@ -278,6 +343,18 @@ describe('constructTurbopackConfig', () => {
278343
},
279344
],
280345
},
346+
'**/instrumentation.*': {
347+
loaders: [
348+
{
349+
loader: '/mocked/path/to/valueInjectionLoader.js',
350+
options: {
351+
values: {
352+
__SENTRY_SERVER_MODULES__: expect.any(Object),
353+
},
354+
},
355+
},
356+
],
357+
},
281358
},
282359
});
283360
});
@@ -308,6 +385,18 @@ describe('constructTurbopackConfig', () => {
308385
},
309386
],
310387
},
388+
'**/instrumentation.*': {
389+
loaders: [
390+
{
391+
loader: '/mocked/path/to/valueInjectionLoader.js',
392+
options: {
393+
values: {
394+
__SENTRY_SERVER_MODULES__: expect.any(Object),
395+
},
396+
},
397+
},
398+
],
399+
},
311400
},
312401
});
313402
});
@@ -342,6 +431,7 @@ describe('constructTurbopackConfig', () => {
342431
loader: '/mocked/path/to/valueInjectionLoader.js',
343432
options: {
344433
values: {
434+
__SENTRY_SERVER_MODULES__: expect.any(Object),
345435
_sentryNextJsVersion: '15.0.0',
346436
},
347437
},
@@ -397,6 +487,7 @@ describe('constructTurbopackConfig', () => {
397487
loader: '/mocked/path/to/valueInjectionLoader.js',
398488
options: {
399489
values: {
490+
__SENTRY_SERVER_MODULES__: expect.any(Object),
400491
_sentryNextJsVersion: '14.0.0',
401492
},
402493
},
@@ -433,6 +524,18 @@ describe('constructTurbopackConfig', () => {
433524
},
434525
],
435526
},
527+
'**/instrumentation.*': {
528+
loaders: [
529+
{
530+
loader: '/mocked/path/to/valueInjectionLoader.js',
531+
options: {
532+
values: {
533+
__SENTRY_SERVER_MODULES__: expect.any(Object),
534+
},
535+
},
536+
},
537+
],
538+
},
436539
},
437540
});
438541
});
@@ -493,6 +596,7 @@ describe('constructTurbopackConfig', () => {
493596
loader: '/mocked/path/to/valueInjectionLoader.js',
494597
options: {
495598
values: {
599+
__SENTRY_SERVER_MODULES__: expect.any(Object),
496600
_sentryNextJsVersion: nextJsVersion,
497601
},
498602
},
@@ -534,6 +638,7 @@ describe('constructTurbopackConfig', () => {
534638
loader: '/mocked/path/to/valueInjectionLoader.js',
535639
options: {
536640
values: {
641+
__SENTRY_SERVER_MODULES__: expect.any(Object),
537642
_sentryNextJsVersion: nextJsVersion,
538643
},
539644
},
@@ -586,6 +691,7 @@ describe('constructTurbopackConfig', () => {
586691
loader: '/mocked/path/to/valueInjectionLoader.js',
587692
options: {
588693
values: {
694+
__SENTRY_SERVER_MODULES__: expect.any(Object),
589695
_sentryNextJsVersion: nextJsVersion,
590696
},
591697
},
@@ -624,7 +730,22 @@ describe('constructTurbopackConfig', () => {
624730
nextJsVersion: undefined,
625731
});
626732

627-
expect(result).toEqual({});
733+
expect(result).toEqual({
734+
rules: {
735+
'**/instrumentation.*': {
736+
loaders: [
737+
{
738+
loader: '/mocked/path/to/valueInjectionLoader.js',
739+
options: {
740+
values: {
741+
__SENTRY_SERVER_MODULES__: expect.any(Object),
742+
},
743+
},
744+
},
745+
],
746+
},
747+
},
748+
});
628749
});
629750

630751
it('should not create Next.js version rule when nextJsVersion is empty string', () => {
@@ -635,7 +756,22 @@ describe('constructTurbopackConfig', () => {
635756
nextJsVersion: '',
636757
});
637758

638-
expect(result).toEqual({});
759+
expect(result).toEqual({
760+
rules: {
761+
'**/instrumentation.*': {
762+
loaders: [
763+
{
764+
loader: '/mocked/path/to/valueInjectionLoader.js',
765+
options: {
766+
values: {
767+
__SENTRY_SERVER_MODULES__: expect.any(Object),
768+
},
769+
},
770+
},
771+
],
772+
},
773+
},
774+
});
639775
});
640776

641777
it('should not override existing instrumentation rule when nextJsVersion is provided', () => {
@@ -725,6 +861,7 @@ describe('constructTurbopackConfig', () => {
725861
loader: '/mocked/path/to/valueInjectionLoader.js',
726862
options: {
727863
values: {
864+
__SENTRY_SERVER_MODULES__: expect.any(Object),
728865
_sentryNextJsVersion: nextJsVersion,
729866
},
730867
},

0 commit comments

Comments
 (0)