Skip to content

Commit b2deefd

Browse files
nicohrubecJPeer264codex
authored
fix(v10/node): Read module versions from the isolated layouts of pnpm and Bun (#25698)
Backport of: #25526 ## Differences to the original PR - Applied the source change in `packages/node-core/src/integrations/modules.ts`, where v10 defines `modulesIntegration`. The pnpm/Bun isolated-layout regression scenarios are unchanged. Co-authored-by: Jan Peer Stöcklmair <jan.peer@sentry.io> Co-authored-by: GPT-6 <codex@openai.com>
1 parent a1c0bd8 commit b2deefd

3 files changed

Lines changed: 98 additions & 5 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
transport: loggingTransport,
8+
});
9+
10+
// `test.ts` writes these packages in the folders where the isolated layouts of pnpm and Bun keep them
11+
require('pnpm-isolated-package');
12+
require('@pnpm-isolated/scoped-package');
13+
require('bun-isolated-package');
14+
require('@bun-isolated/scoped-package');
15+
// Another version of `pnpm-isolated-package`, loaded last as a dependency of the app would load it
16+
require('./node_modules/.pnpm/pnpm-isolated-package@1.0.0/node_modules/pnpm-isolated-package');
17+
18+
Sentry.captureException(new Error('error_1'));

‎dev-packages/node-integration-tests/suites/modules/test.ts‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SDK_VERSION } from '@sentry/core';
2-
import { join } from 'path';
2+
import { mkdirSync, rmSync, symlinkSync, writeFileSync } from 'fs';
3+
import { dirname, join } from 'path';
34
import { afterAll, describe, expect, test } from 'vitest';
45
import { cleanupChildProcesses, createRunner } from '../../utils/runner';
56

@@ -27,6 +28,46 @@ describe('modulesIntegration', () => {
2728
await runner.completed();
2829
});
2930

31+
test('CJS with the isolated layouts of pnpm and Bun', async () => {
32+
const nodeModulesDir = join(__dirname, 'node_modules');
33+
for (const [storeDir, name, version, linked] of [
34+
['.pnpm', 'pnpm-isolated-package', '1.2.3', true],
35+
// Only a dependency of the app uses this version, so it is not linked into `node_modules`
36+
['.pnpm', 'pnpm-isolated-package', '1.0.0', false],
37+
['.pnpm', '@pnpm-isolated/scoped-package', '2.3.4', true],
38+
['.bun', 'bun-isolated-package', '4.5.6', true],
39+
['.bun', '@bun-isolated/scoped-package', '5.6.7', true],
40+
] as const) {
41+
const packageDir = join(nodeModulesDir, storeDir, `${name.replace('/', '+')}@${version}`, 'node_modules', name);
42+
mkdirSync(packageDir, { recursive: true });
43+
writeFileSync(join(packageDir, 'package.json'), JSON.stringify({ name, version }));
44+
writeFileSync(join(packageDir, 'index.js'), 'module.exports = {};');
45+
46+
if (linked) {
47+
mkdirSync(dirname(join(nodeModulesDir, name)), { recursive: true });
48+
symlinkSync(packageDir, join(nodeModulesDir, name), 'junction');
49+
}
50+
}
51+
52+
try {
53+
await createRunner(__dirname, 'scenario-isolated.js')
54+
.expect({
55+
event: {
56+
modules: {
57+
'pnpm-isolated-package': '1.2.3',
58+
'@pnpm-isolated/scoped-package': '2.3.4',
59+
'bun-isolated-package': '4.5.6',
60+
'@bun-isolated/scoped-package': '5.6.7',
61+
},
62+
},
63+
})
64+
.start()
65+
.completed();
66+
} finally {
67+
rmSync(nodeModulesDir, { recursive: true, force: true });
68+
}
69+
});
70+
3071
test('ESM', async () => {
3172
const runner = createRunner(__dirname, 'server.mjs')
3273
.withInstrument(join(__dirname, 'instrument.mjs'))

‎packages/node-core/src/integrations/modules.ts‎

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { existsSync, readFileSync } from 'node:fs';
2-
import { dirname, join } from 'node:path';
1+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
2+
import { basename, dirname, join } from 'node:path';
33
import { GLOBAL_OBJ, type IntegrationFn } from '@sentry/core';
44

55
type ModuleInfo = Record<string, string>;
@@ -73,6 +73,34 @@ function collectModules(): ModuleInfo {
7373
};
7474
}
7575

76+
/**
77+
* Whether `dir` is a node_modules folder of the main module, a folder
78+
* `<that node_modules>/<.pnpm or .bun>/<package id>/node_modules`, where the isolated layouts of pnpm and Bun keep
79+
* each package, or a scope folder (`@scope`) in one of these folders.
80+
*/
81+
function isMainModulesDir(dir: string, mainPaths: string[]): boolean {
82+
if (basename(dir).startsWith('@')) {
83+
return isMainModulesDir(dirname(dir), mainPaths);
84+
}
85+
86+
if (mainPaths.includes(dir)) {
87+
return true;
88+
}
89+
90+
const storeDir = dirname(dirname(dir));
91+
return (
92+
basename(dir) === 'node_modules' &&
93+
['.pnpm', '.bun'].includes(basename(storeDir)) &&
94+
mainPaths.includes(dirname(storeDir))
95+
);
96+
}
97+
98+
/** Whether `require(name)` in the main module loads the package in `packageDir`. */
99+
function isResolvedFromMain(name: string, packageDir: string, mainPaths: string[]): boolean {
100+
const resolvedDir = mainPaths.map(dir => join(dir, name)).find(dir => existsSync(dir));
101+
return !!resolvedDir && realpathSync(resolvedDir) === realpathSync(packageDir);
102+
}
103+
76104
/** Extract information about package.json modules from require.cache */
77105
function collectRequireModules(): ModuleInfo {
78106
const mainPaths = require.main?.paths || [];
@@ -81,6 +109,7 @@ function collectRequireModules(): ModuleInfo {
81109
// We start with the modules from package.json (if possible)
82110
// These may be overwritten by more specific versions from the require.cache
83111
const infos: ModuleInfo = {};
112+
const packageDirs: Record<string, string> = {};
84113
const seen = new Set<string>();
85114

86115
paths.forEach(path => {
@@ -94,7 +123,7 @@ function collectRequireModules(): ModuleInfo {
94123
if (!dir || orig === dir || seen.has(orig)) {
95124
return undefined;
96125
}
97-
if (mainPaths.indexOf(dir) < 0) {
126+
if (!isMainModulesDir(dir, mainPaths)) {
98127
return updir();
99128
}
100129

@@ -110,7 +139,12 @@ function collectRequireModules(): ModuleInfo {
110139
name: string;
111140
version: string;
112141
};
113-
infos[info.name] = info.version;
142+
// The isolated layouts of pnpm and Bun keep every version of a package. Keep the one that the main module loads.
143+
const previousDir = packageDirs[info.name];
144+
if (!previousDir || !isResolvedFromMain(info.name, previousDir, mainPaths)) {
145+
infos[info.name] = info.version;
146+
packageDirs[info.name] = orig;
147+
}
114148
} catch {
115149
// no-empty
116150
}

0 commit comments

Comments
 (0)