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(); + }); +});