From e4c1926cd24acf9457ff94b8ba831c1c3fbd3535 Mon Sep 17 00:00:00 2001 From: Pavel Safronov Date: Wed, 24 Jun 2026 11:16:48 -0700 Subject: [PATCH 1/6] fix(NODE-7603): load os runtime adapter via dynamic import for ESM bundle safety resolveRuntimeAdapters no longer calls require('os'), which throws in bundled ESM output (no `require` in module scope). It resolves the default os adapter via a dynamic import() constructed with `new Function`, so it survives TypeScript's module:commonjs downleveling and downstream bundlers. Resolution is now async: resolveRuntimeAdapters returns Promise with concrete adapters, and the two consumers (makeClientMetadata, makeKerberosClient) await it. The CJS-bundled test sandbox (vm) is allowed to use dynamic import so it keeps working with the new import(). Adds a focused ESM bundling smoke test: bundle resolveRuntimeAdapters to ESM and run it in Node with no global require. --- src/cmap/auth/gssapi.ts | 7 ++- src/cmap/connection.ts | 2 +- src/cmap/handshake/client_metadata.ts | 3 +- src/mongo_client.ts | 2 +- src/runtime_adapters.ts | 56 +++++++++++++++----- test/tools/runner/vm_context_helper.ts | 9 +++- test/tools/utils.ts | 12 +++-- test/unit/bundling.test.ts | 73 ++++++++++++++++++++++++++ test/unit/runtime_adapters.test.ts | 15 ++++-- 9 files changed, 149 insertions(+), 30 deletions(-) create mode 100644 test/unit/bundling.test.ts diff --git a/src/cmap/auth/gssapi.ts b/src/cmap/auth/gssapi.ts index 12595242f38..99892a337d7 100644 --- a/src/cmap/auth/gssapi.ts +++ b/src/cmap/auth/gssapi.ts @@ -69,10 +69,7 @@ export class GSSAPI extends AuthProvider { } async function makeKerberosClient({ - options: { - hostAddress, - runtime: { os } - }, + options: { hostAddress, runtime }, credentials }: AuthContext): Promise { if (!hostAddress || typeof hostAddress.host !== 'string' || !credentials) { @@ -81,6 +78,8 @@ async function makeKerberosClient({ ); } + const { os } = await runtime; + loadKrb(); if ('kModuleError' in krb) { throw krb['kModuleError']; diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 0c099b8bd8c..f8cc3b2d95e 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -146,7 +146,7 @@ export interface ConnectionOptions /** @internal */ mongoLogger?: MongoLogger | undefined; /** @internal */ - runtime: Runtime; + runtime: Promise; } /** @public */ diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index 00780fbe0ff..587d3420b93 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -107,8 +107,9 @@ type MakeClientMetadataOptions = Pick; */ export async function makeClientMetadata( driverInfoList: DriverInfo[], - { appName = '', runtime: { os } }: MakeClientMetadataOptions + { appName = '', runtime }: MakeClientMetadataOptions ): Promise { + const { os } = await runtime; const metadataDocument = new LimitedSizeDocument(512); // Add app name first, it must be sent diff --git a/src/mongo_client.ts b/src/mongo_client.ts index d32ad8554af..2192c80bdbb 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -1175,5 +1175,5 @@ export interface MongoOptions __skipPingOnConnect?: boolean; /** @internal */ - runtime: Runtime; + runtime: Promise; } diff --git a/src/runtime_adapters.ts b/src/runtime_adapters.ts index 3969e0fc049..2eb1f8645f7 100644 --- a/src/runtime_adapters.ts +++ b/src/runtime_adapters.ts @@ -37,7 +37,7 @@ export interface RuntimeAdapters { /** * @internal * - * Represents a complete, parsed set of runtime adapters. After options parsing, all adapters + * Represents a complete, parsed set of runtime adapters. After resolution, all adapters * are always present (either using the user's provided adapter, or defaulting to the Node.js module). */ export interface Runtime { @@ -48,17 +48,47 @@ export interface Runtime { * @internal * * Given a MongoClientOptions, this function resolves the set of runtime options, providing Nodejs implementations if - * not provided by in `options`, and returns a `Runtime`. + * not provided in `options`, and returns a `Runtime`. + * + * Resolution is asynchronous because the default adapters are loaded from Node.js built-ins via a + * dynamic `import()` (see `loadNodeOsAdapter`). The resulting promise is created during synchronous + * options parsing and awaited later by consumers, so the public constructor stays synchronous while + * the `Runtime` itself exposes fully-resolved, concrete adapters. + */ +export async function resolveRuntimeAdapters(options: MongoClientOptions): Promise { + return { + os: options.runtimeAdapters?.os ?? (await loadNodeOsAdapter()) + }; +} + +/** + * @internal + */ +function loadNodeOsAdapter(): Promise { + return dynamicImport('os'); +} + +/** + * @internal + * + * Dynamically imports a module at runtime in a way that survives bundling and TypeScript's + * downleveling. We deliberately avoid both `require(specifier)` and a static `await import(...)`: + * - a raw `require` throws in bundled ESM output, where there is no `require` in module scope + * (NODE-7603), and + * - a literal `import(...)` is downleveled back to `require(...)` by TypeScript under + * `module: commonjs`, which reintroduces the same problem in the published CommonJS build. + * + * Constructing the dynamic `import` through `new Function` hides it from both the TypeScript + * compiler and downstream bundlers, so it survives as a real runtime `import()` in every + * environment. Call this lazily (never at module load) so strict-CSP runtimes that forbid + * `new Function` are unaffected unless they actually need the default adapter. + * + * NODE-7133 (ESM-only packages) will eventually let us use `import(...)` directly and drop this. + * + * @param specifier - The module specifier to import. + * @returns A promise that resolves to the imported module. */ -export function resolveRuntimeAdapters(options: MongoClientOptions): Runtime { - (globalThis as any)[ALLOWED_DRIVER_REQUIRE_PROPERTY_NAME] = true; - try { - const runtime = { - // eslint-disable-next-line @typescript-eslint/no-require-imports - os: options.runtimeAdapters?.os ?? require('os') - }; - return runtime; - } finally { - (globalThis as any)[ALLOWED_DRIVER_REQUIRE_PROPERTY_NAME] = false; - } +function dynamicImport(specifier: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-implied-eval + return new Function('specifier', 'return import(specifier)')(specifier); } diff --git a/test/tools/runner/vm_context_helper.ts b/test/tools/runner/vm_context_helper.ts index b308b6c42d0..705971a85af 100644 --- a/test/tools/runner/vm_context_helper.ts +++ b/test/tools/runner/vm_context_helper.ts @@ -119,7 +119,14 @@ export function loadContextifiedMongoDBModule(): typeof import('../../mongodb_al // Wrap the bundle in a CommonJS-style wrapper const wrapper = `(function(exports, module, require) {${bundleCode}})`; - const script = new vm.Script(wrapper, { filename: bundlePath }); + // The driver loads Node built-ins (e.g. `os`) via a dynamic `import()` rather than `require` + // (NODE-7603). vm scripts have no dynamic-import callback by default, so route any `import()` in + // the sandbox through the main context's loader; otherwise it throws "A dynamic import callback + // was not specified". + const script = new vm.Script(wrapper, { + filename: bundlePath, + importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER + }); const fn = script.runInContext(sandbox); // Execute the bundle with the restricted require from the sandbox diff --git a/test/tools/utils.ts b/test/tools/utils.ts index 5fecb7cf07d..0a575ac404b 100644 --- a/test/tools/utils.ts +++ b/test/tools/utils.ts @@ -1,7 +1,7 @@ import * as child_process from 'node:child_process'; import { on, once } from 'node:events'; import * as fs from 'node:fs/promises'; -import { tmpdir } from 'node:os'; +import * as os from 'node:os'; import * as path from 'node:path'; import * as BSON from 'bson'; @@ -20,7 +20,6 @@ import { type MongoClientOptions, OP_MSG, processTimeMS, - resolveRuntimeAdapters, runNodelessTests, type Runtime, type ServerApiVersion, @@ -594,7 +593,8 @@ export function configureMongocryptdSpawnHooks( options: { port?: string; pidfilepath?: string } = {} ): { port: string } { const port = options.port ?? '27022'; - const pidfilepath = options.pidfilepath ?? path.join(tmpdir(), new BSON.ObjectId().toHexString()); + const pidfilepath = + options.pidfilepath ?? path.join(os.tmpdir(), new BSON.ObjectId().toHexString()); let childProcess: child_process.ChildProcess; @@ -621,9 +621,11 @@ export function configureMongocryptdSpawnHooks( } /** - * A `Runtime` that resolves to entirely Nodejs modules, useful when tests must provide a default `runtime` object to an API. + * A resolved `Runtime` backed by Node's own modules, useful when tests must provide a default + * `runtime` to an API. The `os` adapter is the live `os` module (rather than the driver's lazily + * imported default) so tests can `sinon.stub(os, ...)` it. */ -export const runtime: Runtime = resolveRuntimeAdapters({}); +export const runtime: Promise = Promise.resolve({ os }); /** * Metadata that can be used to skip tests in nodeless environments. diff --git a/test/unit/bundling.test.ts b/test/unit/bundling.test.ts new file mode 100644 index 00000000000..6e430df4c62 --- /dev/null +++ b/test/unit/bundling.test.ts @@ -0,0 +1,73 @@ +import { spawnSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { expect } from 'chai'; +import * as esbuild from 'esbuild'; +import * as process from 'process'; +import * as ts from 'typescript'; + +const repoRoot = path.resolve(__dirname, '..', '..'); + +describe('bundling the runtime adapters into ESM output', function () { + // Transpiling + bundling with esbuild and spawning a fresh node process can exceed the default timeout. + this.timeout(120_000); + + let tmpDir: string; + let esmBundlePath: string; + + before('compile and bundle resolveRuntimeAdapters into ESM', async function () { + // NODE-7603: resolveRuntimeAdapters used to call require('os'), which throws in bundled ESM + // output (no `require` in module scope). This reproduces the published artifact and a downstream + // ESM bundler: transpile the source the way the build does (module: commonjs) so any literal + // `import()` would be downleveled back to require(), then bundle that to ESM. The fix must + // therefore survive both steps and resolve `os` via a real runtime import(). + const source = fs.readFileSync(path.join(repoRoot, 'src', 'runtime_adapters.ts'), 'utf8'); + const { outputText: compiledCjs } = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2023 } + }); + + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mongodb-esm-bundle-')); + fs.writeFileSync(path.join(tmpDir, 'runtime_adapters.js'), compiledCjs); + esmBundlePath = path.join(tmpDir, 'app.mjs'); + + await esbuild.build({ + stdin: { + // No user-provided os adapter, so resolveRuntimeAdapters falls back to loading Node's os. + contents: ` + import { resolveRuntimeAdapters } from './runtime_adapters.js'; + const runtime = await resolveRuntimeAdapters({}); + const osAdapter = await runtime.os; + if (typeof osAdapter.platform !== 'function') { + throw new Error('resolved os adapter is missing platform()'); + } + console.log('resolved os adapter'); + `, + resolveDir: tmpDir, + loader: 'js' + }, + bundle: true, + outfile: esmBundlePath, + platform: 'node', + format: 'esm', + target: 'node20', + logLevel: 'silent' + }); + }); + + after(function () { + if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('resolves the default os adapter without a global require', function () { + const { status, stdout, stderr } = spawnSync(process.execPath, [esmBundlePath], { + encoding: 'utf8' + }); + + // Surface the child's stderr in the failure message so a regression is easy to diagnose. + expect(stderr, stderr).to.not.match(/require/i); + expect(stdout).to.include('resolved os adapter'); + expect(status).to.equal(0); + }); +}); diff --git a/test/unit/runtime_adapters.test.ts b/test/unit/runtime_adapters.test.ts index 3980f9f1d71..5fba034d776 100644 --- a/test/unit/runtime_adapters.test.ts +++ b/test/unit/runtime_adapters.test.ts @@ -6,15 +6,21 @@ import { MongoClient, type OsAdapter } from '../../src'; describe('Runtime Adapters tests', function () { describe('`os`', function () { describe('when no os adapter is provided', function () { - it(`defaults to Node's os module`, function () { + it(`defaults to Node's os module, resolved asynchronously`, async function () { const client = new MongoClient('mongodb://localhost:27017'); - expect(client.options.runtime.os).to.equal(os); + // The runtime is resolved asynchronously because the default adapters are loaded from + // Node.js built-ins via a dynamic import (NODE-7603). + const { os: resolved } = await client.options.runtime; + expect(resolved.platform()).to.equal(os.platform()); + expect(resolved.arch()).to.equal(os.arch()); + expect(resolved.release()).to.equal(os.release()); + expect(resolved.type()).to.equal(os.type()); }); }); describe('when an os adapter is provided', function () { - it(`uses the user provided adapter`, function () { + it(`uses the user provided adapter`, async function () { const osAdapter: OsAdapter = { ...os }; @@ -24,7 +30,8 @@ describe('Runtime Adapters tests', function () { } }); - expect(client.options.runtime.os).to.equal(osAdapter); + const { os: resolved } = await client.options.runtime; + expect(resolved).to.equal(osAdapter); }); }); }); From e6f8d635e8f37481f6cbf71caf093294747c8f99 Mon Sep 17 00:00:00 2001 From: Pavel Safronov Date: Mon, 29 Jun 2026 10:51:08 -0700 Subject: [PATCH 2/6] pr feedback: try using require when available --- src/runtime_adapters.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/runtime_adapters.ts b/src/runtime_adapters.ts index 2eb1f8645f7..fa5a2edd004 100644 --- a/src/runtime_adapters.ts +++ b/src/runtime_adapters.ts @@ -65,6 +65,15 @@ export async function resolveRuntimeAdapters(options: MongoClientOptions): Promi * @internal */ function loadNodeOsAdapter(): Promise { + if (typeof require === 'function') { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const osModule = require('os') as typeof os; + return Promise.resolve(osModule); + } catch { + // If require fails, we fall back to dynamic import below. + } + } return dynamicImport('os'); } From dbddcacf3de46240559c413ca9f626984ca5fb09 Mon Sep 17 00:00:00 2001 From: Pavel Safronov Date: Tue, 30 Jun 2026 15:03:42 -0700 Subject: [PATCH 3/6] pr feedback: clarify require-first fallback comments --- src/runtime_adapters.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/runtime_adapters.ts b/src/runtime_adapters.ts index fa5a2edd004..b5dc7c6fcbc 100644 --- a/src/runtime_adapters.ts +++ b/src/runtime_adapters.ts @@ -66,12 +66,14 @@ export async function resolveRuntimeAdapters(options: MongoClientOptions): Promi */ function loadNodeOsAdapter(): Promise { if (typeof require === 'function') { + // Some environments (plain Node, CJS bundling, native ESM), have a `require` function available, we try that first. try { // eslint-disable-next-line @typescript-eslint/no-require-imports const osModule = require('os') as typeof os; return Promise.resolve(osModule); } catch { // If require fails, we fall back to dynamic import below. + // This can happen in ESM bundles where `require` may be available, but will always throw. } } return dynamicImport('os'); From ec2729c2d872d2422c868aefebe19ad5f5b53c4b Mon Sep 17 00:00:00 2001 From: Pavel Safronov Date: Wed, 1 Jul 2026 13:35:47 -0700 Subject: [PATCH 4/6] pr feedback: avoid eval and drop our hand-written import shim in the lib folder --- .gitignore | 3 +++ package.json | 1 + shims/runtime_import.d.ts | 11 +++++++++++ shims/runtime_import.js | 22 ++++++++++++++++++++++ src/runtime_adapters.ts | 30 +++++------------------------- test/unit/bundling.test.ts | 15 +++++++++++++-- 6 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 shims/runtime_import.d.ts create mode 100644 shims/runtime_import.js diff --git a/.gitignore b/.gitignore index ff5518925b1..283708c7a42 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,9 @@ lib/ # type definition tests !test/types !global.d.ts +# hand-written type declaration for the runtime_import.js CommonJS shim (NODE-7603); must be +# tracked because it is authored, not generated by tsc like every other *.d.ts. +!shims/runtime_import.d.ts .vscode output diff --git a/package.json b/package.json index 4da3355ea7f..8e55705dad2 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "files": [ "lib", "src", + "shims", "etc/prepare.js", "mongodb.d.ts", "tsconfig.json" diff --git a/shims/runtime_import.d.ts b/shims/runtime_import.d.ts new file mode 100644 index 00000000000..5d793b784ad --- /dev/null +++ b/shims/runtime_import.d.ts @@ -0,0 +1,11 @@ +/** + * @internal + * + * Type declarations for the hand-written CommonJS shim in `runtime_import.js`. That file lives + * outside `src/` so the TypeScript build never compiles it, which would downlevel its dynamic + * `import()` to `require()`. + * + * @param specifier - The module specifier to import. + * @returns A promise that resolves to the imported module namespace. + */ +export declare function dynamicImport(specifier: string): Promise; diff --git a/shims/runtime_import.js b/shims/runtime_import.js new file mode 100644 index 00000000000..69a071269b4 --- /dev/null +++ b/shims/runtime_import.js @@ -0,0 +1,22 @@ +// This file is hand-written CommonJS. It lives OUTSIDE `src/` on purpose: the TypeScript build +// only compiles `src/**/*` (see tsconfig.json "include"), so tsc never touches this file. If it +// did, it would downlevel `import(specifier)` into `Promise.resolve().then(() => require(specifier))` +// under `module: commonjs`. +// +// Keeping the dynamic import in a file the compiler never sees preserves it as a genuine runtime +// `import()`, which: +// - survives TypeScript downleveling (the file is not compiled), and +// - survives downstream bundlers as a real dynamic import (they can see and alias the +// specifier), unlike a `new Function('return import(...)')` trick. +// +// It ships as-is via the package.json "files" array and is required by the compiled +// `lib/runtime_adapters.js` as `../shims/runtime_import`. +// +// NODE-7133 (ESM-only packages) will eventually let us use `import(...)` directly from TypeScript +// source and delete this shim. + +Object.defineProperty(exports, '__esModule', { value: true }); + +exports.dynamicImport = function dynamicImport(specifier) { + return import(specifier); +}; diff --git a/src/runtime_adapters.ts b/src/runtime_adapters.ts index b5dc7c6fcbc..66792dc01eb 100644 --- a/src/runtime_adapters.ts +++ b/src/runtime_adapters.ts @@ -7,6 +7,7 @@ import type * as os from 'os'; import { type MongoClientOptions } from './mongo_client'; +import { dynamicImport } from '../shims/runtime_import'; /** * @internal @@ -76,30 +77,9 @@ function loadNodeOsAdapter(): Promise { // This can happen in ESM bundles where `require` may be available, but will always throw. } } - return dynamicImport('os'); -} -/** - * @internal - * - * Dynamically imports a module at runtime in a way that survives bundling and TypeScript's - * downleveling. We deliberately avoid both `require(specifier)` and a static `await import(...)`: - * - a raw `require` throws in bundled ESM output, where there is no `require` in module scope - * (NODE-7603), and - * - a literal `import(...)` is downleveled back to `require(...)` by TypeScript under - * `module: commonjs`, which reintroduces the same problem in the published CommonJS build. - * - * Constructing the dynamic `import` through `new Function` hides it from both the TypeScript - * compiler and downstream bundlers, so it survives as a real runtime `import()` in every - * environment. Call this lazily (never at module load) so strict-CSP runtimes that forbid - * `new Function` are unaffected unless they actually need the default adapter. - * - * NODE-7133 (ESM-only packages) will eventually let us use `import(...)` directly and drop this. - * - * @param specifier - The module specifier to import. - * @returns A promise that resolves to the imported module. - */ -function dynamicImport(specifier: string): Promise { - // eslint-disable-next-line @typescript-eslint/no-implied-eval - return new Function('specifier', 'return import(specifier)')(specifier); + // Fall back to a genuine dynamic `import()`. This lives in the hand-written CommonJS shim + // `../shims/runtime_import`, which is kept out of the TypeScript build so the `import()` is not + // downleveled to `require()`. + return dynamicImport('os'); } diff --git a/test/unit/bundling.test.ts b/test/unit/bundling.test.ts index 6e430df4c62..d38182e9a29 100644 --- a/test/unit/bundling.test.ts +++ b/test/unit/bundling.test.ts @@ -28,15 +28,26 @@ describe('bundling the runtime adapters into ESM output', function () { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2023 } }); + // Mirror the published package layout: the compiled module lives in lib/ and requires the + // hand-written shim as `../shims/runtime_import`, so reproduce those sibling directories. tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mongodb-esm-bundle-')); - fs.writeFileSync(path.join(tmpDir, 'runtime_adapters.js'), compiledCjs); + fs.mkdirSync(path.join(tmpDir, 'lib')); + fs.mkdirSync(path.join(tmpDir, 'shims')); + fs.writeFileSync(path.join(tmpDir, 'lib', 'runtime_adapters.js'), compiledCjs); + // runtime_import.js is the hand-written CommonJS shim that resolveRuntimeAdapters imports for + // its dynamic import() fallback. It is deliberately NOT compiled (that is the whole point of + // NODE-7603), so copy it verbatim into the sibling shims/ dir, exactly as it ships. + fs.copyFileSync( + path.join(repoRoot, 'shims', 'runtime_import.js'), + path.join(tmpDir, 'shims', 'runtime_import.js') + ); esmBundlePath = path.join(tmpDir, 'app.mjs'); await esbuild.build({ stdin: { // No user-provided os adapter, so resolveRuntimeAdapters falls back to loading Node's os. contents: ` - import { resolveRuntimeAdapters } from './runtime_adapters.js'; + import { resolveRuntimeAdapters } from './lib/runtime_adapters.js'; const runtime = await resolveRuntimeAdapters({}); const osAdapter = await runtime.os; if (typeof osAdapter.platform !== 'function') { From 31d97a7200beb6f94c81aad98f9379c3fde6e7dd Mon Sep 17 00:00:00 2001 From: Pavel Safronov Date: Wed, 1 Jul 2026 14:01:25 -0700 Subject: [PATCH 5/6] lint --- src/runtime_adapters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime_adapters.ts b/src/runtime_adapters.ts index 66792dc01eb..db46dd9c5cd 100644 --- a/src/runtime_adapters.ts +++ b/src/runtime_adapters.ts @@ -6,8 +6,8 @@ import type * as os from 'os'; -import { type MongoClientOptions } from './mongo_client'; import { dynamicImport } from '../shims/runtime_import'; +import { type MongoClientOptions } from './mongo_client'; /** * @internal From 3f0e0619f16fe2908b0e34d0122476e1d4284649 Mon Sep 17 00:00:00 2001 From: Pavel Safronov Date: Thu, 2 Jul 2026 08:19:34 -0700 Subject: [PATCH 6/6] pr feedback: keep `import('os')` in the bundle --- shims/runtime_import.d.ts | 5 ++--- shims/runtime_import.js | 14 +++++++++----- src/runtime_adapters.ts | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/shims/runtime_import.d.ts b/shims/runtime_import.d.ts index 5d793b784ad..5d8b769fb7c 100644 --- a/shims/runtime_import.d.ts +++ b/shims/runtime_import.d.ts @@ -5,7 +5,6 @@ * outside `src/` so the TypeScript build never compiles it, which would downlevel its dynamic * `import()` to `require()`. * - * @param specifier - The module specifier to import. - * @returns A promise that resolves to the imported module namespace. + * @returns A promise that resolves to the `os` module namespace. */ -export declare function dynamicImport(specifier: string): Promise; +export declare function importOs(): Promise; diff --git a/shims/runtime_import.js b/shims/runtime_import.js index 69a071269b4..aa5575624e9 100644 --- a/shims/runtime_import.js +++ b/shims/runtime_import.js @@ -1,13 +1,17 @@ // This file is hand-written CommonJS. It lives OUTSIDE `src/` on purpose: the TypeScript build // only compiles `src/**/*` (see tsconfig.json "include"), so tsc never touches this file. If it -// did, it would downlevel `import(specifier)` into `Promise.resolve().then(() => require(specifier))` +// did, it would downlevel `import('os')` into `Promise.resolve().then(() => require('os'))` // under `module: commonjs`. // // Keeping the dynamic import in a file the compiler never sees preserves it as a genuine runtime // `import()`, which: // - survives TypeScript downleveling (the file is not compiled), and -// - survives downstream bundlers as a real dynamic import (they can see and alias the -// specifier), unlike a `new Function('return import(...)')` trick. +// - survives downstream bundlers as a real dynamic import, unlike a +// `new Function('return import(...)')` trick. +// +// The specifier is deliberately a literal, not a parameter: a literal `import('os')` remains +// statically analyzable, so bundlers can see, resolve, and alias the target (NODE-3199), whereas +// `import(someVariable)` is opaque to static analysis. // // It ships as-is via the package.json "files" array and is required by the compiled // `lib/runtime_adapters.js` as `../shims/runtime_import`. @@ -17,6 +21,6 @@ Object.defineProperty(exports, '__esModule', { value: true }); -exports.dynamicImport = function dynamicImport(specifier) { - return import(specifier); +exports.importOs = function importOs() { + return import('os'); }; diff --git a/src/runtime_adapters.ts b/src/runtime_adapters.ts index db46dd9c5cd..022f30cb811 100644 --- a/src/runtime_adapters.ts +++ b/src/runtime_adapters.ts @@ -6,7 +6,7 @@ import type * as os from 'os'; -import { dynamicImport } from '../shims/runtime_import'; +import { importOs } from '../shims/runtime_import'; import { type MongoClientOptions } from './mongo_client'; /** @@ -81,5 +81,5 @@ function loadNodeOsAdapter(): Promise { // Fall back to a genuine dynamic `import()`. This lives in the hand-written CommonJS shim // `../shims/runtime_import`, which is kept out of the TypeScript build so the `import()` is not // downleveled to `require()`. - return dynamicImport('os'); + return importOs(); }