87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
/**
|
|
* bun-napi-shim — intercept native-module loads that would otherwise crash
|
|
* Bun's NAPI loader before testcontainers' fallback paths run.
|
|
*
|
|
* Background: `dockerode` (which testcontainers uses for the Docker socket)
|
|
* pulls `ssh2` and `cpu-features`. Both packages load `.node` binaries via
|
|
* a wrapped `require()` with a `try { require(...) } catch {}` fallback to
|
|
* pure-JS implementations. Under Bun, however, the underlying NAPI loader
|
|
* panics on `uv_version_string` (oven-sh/bun#18546) BEFORE the catch block
|
|
* runs, killing the runner.
|
|
*
|
|
* This shim intercepts `Module._resolveFilename` for the two known offending
|
|
* `.node` paths and throws a plain JS error instead. ssh2/cpu-features then
|
|
* catch it and fall back to pure-JS code paths. Safe no-op under Node.
|
|
*
|
|
* Loaded as a side-effect by `src/setup/global-setup.ts` so every consumer
|
|
* who wires `createIntegrationSetup`'s returned `globalSetup` path gets the
|
|
* shim automatically; no per-consumer wiring needed.
|
|
*
|
|
* @module
|
|
*/
|
|
import { createRequire } from "node:module";
|
|
|
|
interface NodeModule {
|
|
_resolveFilename: (request: string, ...rest: unknown[]) => string;
|
|
}
|
|
|
|
const BLOCKED_NATIVE_SUFFIXES = [
|
|
"/sshcrypto.node",
|
|
"/cpufeatures.node",
|
|
] as const;
|
|
|
|
let shimInstalled = false;
|
|
|
|
/**
|
|
* Install the Module._resolveFilename interceptor when running under Bun.
|
|
*
|
|
* Idempotent: subsequent calls are no-ops. Returns `true` if the shim was
|
|
* installed (either now or previously), `false` if not running under Bun.
|
|
*
|
|
* @param options.moduleRef - Optional override for `node:module`. Used by
|
|
* tests to inject a fake `Module` whose `_resolveFilename` can be inspected.
|
|
* @param options.processRef - Optional override for the global `process`.
|
|
* Used by tests to simulate Bun vs Node detection without actually running
|
|
* under Bun.
|
|
*/
|
|
export function installBunNapiShim(
|
|
options: {
|
|
moduleRef?: NodeModule;
|
|
processRef?: { versions?: { bun?: string } };
|
|
} = {}
|
|
): boolean {
|
|
const proc =
|
|
options.processRef ??
|
|
/* istanbul ignore next */ (typeof process !== "undefined"
|
|
? (process as unknown as { versions?: { bun?: string } })
|
|
: undefined);
|
|
if (proc?.versions?.bun === undefined) return false;
|
|
if (shimInstalled) return true;
|
|
|
|
const mod =
|
|
options.moduleRef ??
|
|
/* istanbul ignore next */ (createRequire(import.meta.url)(
|
|
"node:module"
|
|
) as NodeModule);
|
|
const original = mod._resolveFilename;
|
|
mod._resolveFilename = function patched(
|
|
this: unknown,
|
|
request: string,
|
|
...rest: unknown[]
|
|
): string {
|
|
if (BLOCKED_NATIVE_SUFFIXES.some((s) => request.endsWith(s))) {
|
|
throw new Error(
|
|
`[bun-napi-shim] blocked native module load (${request}); pure-JS fallback engaged`
|
|
);
|
|
}
|
|
return original.call(this, request, ...rest);
|
|
};
|
|
shimInstalled = true;
|
|
return true;
|
|
}
|
|
|
|
/** Test-only: reset the install flag so a second `installBunNapiShim` call patches again. */
|
|
export function _resetForTests(): void {
|
|
shimInstalled = false;
|
|
}
|