From c6c97f325147b8b807b3844efbc9075172276b97 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 7 Sep 2026 09:50:59 +0200 Subject: [PATCH] fix(core): Guard `loadModule` default parameter against ESM scope Default parameters are evaluated before the function body, so the bare `module` default threw `ReferenceError: module is not defined` from the ESM build before reaching the try/catch that is meant to make `loadModule` degrade gracefully. Guard it with a `typeof module` check so ESM callers get `undefined` instead. Fixes #24117 Co-Authored-By: Claude Fable 5.1 --- .../suites/esm/load-module/app.mjs | 6 ++++++ .../suites/esm/load-module/test.ts | 12 +++++++++++ packages/core/src/utils/node.ts | 10 ++++++++-- packages/core/test/lib/utils/node.test.ts | 20 +++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/esm/load-module/app.mjs create mode 100644 dev-packages/node-integration-tests/suites/esm/load-module/test.ts create mode 100644 packages/core/test/lib/utils/node.test.ts diff --git a/dev-packages/node-integration-tests/suites/esm/load-module/app.mjs b/dev-packages/node-integration-tests/suites/esm/load-module/app.mjs new file mode 100644 index 000000000000..5cb01da1f62b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/esm/load-module/app.mjs @@ -0,0 +1,6 @@ +import { loadModule } from '@sentry/core/server'; + +// The default `existingModule` argument must not reference a CJS-only binding: default +// parameters are evaluated before the function body, so a bare `module` would throw +// outside the try/catch that is supposed to make this helper degrade gracefully. +loadModule('node:path'); diff --git a/dev-packages/node-integration-tests/suites/esm/load-module/test.ts b/dev-packages/node-integration-tests/suites/esm/load-module/test.ts new file mode 100644 index 000000000000..69f5c385829b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/esm/load-module/test.ts @@ -0,0 +1,12 @@ +import { afterAll, describe, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +describe('loadModule', () => { + test('does not throw when called without `existingModule` from ESM', async () => { + await createRunner(__dirname, 'app.mjs').ensureNoErrorOutput().start().completed(); + }); +}); diff --git a/packages/core/src/utils/node.ts b/packages/core/src/utils/node.ts index 6060700c2b03..235f65448279 100644 --- a/packages/core/src/utils/node.ts +++ b/packages/core/src/utils/node.ts @@ -44,8 +44,14 @@ function dynamicRequire(mod: any, request: string): any { * @param existingModule module to use for requiring * @returns possibly required module */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function loadModule(moduleName: string, existingModule: any = module): T | undefined { +export function loadModule( + moduleName: string, + // Default parameters are evaluated before the body runs, so a bare `module` would throw a + // ReferenceError in ESM before reaching the try/catch below that makes this helper degrade + // gracefully. Guard it so the ESM build resolves to `undefined` instead of crashing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + existingModule: any = typeof module !== 'undefined' ? module : undefined, +): T | undefined { let mod: T | undefined; try { diff --git a/packages/core/test/lib/utils/node.test.ts b/packages/core/test/lib/utils/node.test.ts new file mode 100644 index 000000000000..9cdb6f9b270f --- /dev/null +++ b/packages/core/test/lib/utils/node.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { loadModule } from '../../../src/utils/node'; + +// vitest's `module` shim has no `require`, so tests hand in an explicit CJS-like module object. +const cjsModule = { require }; + +describe('loadModule', () => { + it('loads a module via the given `existingModule`', () => { + const path = loadModule<{ join: unknown }>('path', cjsModule); + expect(path?.join).toBeTypeOf('function'); + }); + + it('returns undefined for a module that cannot be resolved', () => { + expect(loadModule('@sentry/this-module-does-not-exist', cjsModule)).toBeUndefined(); + }); + + it('returns undefined instead of throwing when `existingModule` cannot require', () => { + expect(loadModule('path', undefined)).toBeUndefined(); + }); +});