fix(utils): made runtime module resolver uninstallable
installRuntimeModuleResolver patched Module._resolveFilename process-wide with no way back. In the shared bun test process the leaked patch broke createRequire relative requires for every later test file (Bun 1.3.14 calls a JS _resolveFilename override with parent === undefined, so './x' resolves 'from ""'), failing legacy-pi-inplace-load only in full-suite runs. The installer now returns an uninstaller that drops the registration and restores the pristine resolver when no runtime roots remain; the known Bun limitation is documented so the patch stays scoped to worker runtimes.
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- `installRuntimeModuleResolver` now returns an uninstaller that removes the registration and restores the stock `node:module` resolver once no runtime roots remain registered. Documented the Bun 1.3.14 limitation the patch inherits: while any JS `_resolveFilename` override is installed, `createRequire(...)` relative requires fail because Bun invokes the override without the requester context — keep the patch scoped to dedicated worker/runtime processes.
|
||||
|
||||
## [17.0.2] - 2026-07-17
|
||||
|
||||
### Added
|
||||
|
||||
@@ -194,24 +194,41 @@ export interface RuntimeResolverOptions {
|
||||
* runtime caches. Stock resolution is tried first and kept for anything
|
||||
* outside the registered roots (bundled imports, node builtins, host or
|
||||
* extension trees). Multiple runtime roots may register; they are consulted
|
||||
* in registration order.
|
||||
* in registration order. Returns an uninstaller that drops the registration
|
||||
* and restores the stock resolver once no registrations remain.
|
||||
*
|
||||
* One stock "success" is distrusted: the compiled-binary resolver ignores
|
||||
* `main`/`exports` for real-FS packages (Bun #1763), so a package shipping
|
||||
* its TS source next to `dist/` (e.g. `@huggingface/hub`'s root `index.ts`)
|
||||
* resolves to the wrong file. When the stock hit lands inside a registered
|
||||
* runtime root, the manifest-aware resolution wins.
|
||||
*
|
||||
* KNOWN LIMITATION (Bun 1.3.14): while any JS override of
|
||||
* `Module._resolveFilename` is installed, Bun routes `createRequire(...)`
|
||||
* resolution through it with `parent === undefined` — the requester context is
|
||||
* never passed, so relative requires from a `createRequire` require fail with
|
||||
* "Cannot find module './x' from ''". The override cannot recover what it is
|
||||
* never given. Keep this patch scoped to dedicated worker/runtime processes
|
||||
* (tiny-inference, fastembed); never install it in the main agent process,
|
||||
* where legacy-pi extensions rely on `createRequire` relative requires.
|
||||
*/
|
||||
export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }: RuntimeResolverOptions): void {
|
||||
export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }: RuntimeResolverOptions): () => void {
|
||||
const registry = resolverRegistry();
|
||||
const existing = registry.find(entry => entry.runtimeNodeModules === runtimeNodeModules);
|
||||
if (existing) Object.assign(existing.stubs, stubs);
|
||||
else registry.push({ runtimeNodeModules, stubs: { ...stubs } });
|
||||
|
||||
const resolver = (Module as unknown as { default?: ModuleResolver } & ModuleResolver).default ?? Module;
|
||||
const target = resolver as unknown as ModuleResolver & { [PATCHED]?: boolean };
|
||||
if (target[PATCHED]) return;
|
||||
const original = target._resolveFilename.bind(target);
|
||||
const target = resolver as unknown as ModuleResolver & { [PATCHED]?: () => void };
|
||||
const uninstall = (): void => {
|
||||
const entries = resolverRegistry();
|
||||
const index = entries.findIndex(entry => entry.runtimeNodeModules === runtimeNodeModules);
|
||||
if (index !== -1) entries.splice(index, 1);
|
||||
if (entries.length === 0) target[PATCHED]?.();
|
||||
};
|
||||
if (target[PATCHED]) return uninstall;
|
||||
const pristine = target._resolveFilename;
|
||||
const original = pristine.bind(target);
|
||||
target._resolveFilename = (request: string, parent: unknown, isMain: boolean, options?: unknown): string => {
|
||||
let stockResolved: string | null = null;
|
||||
let stockError: unknown;
|
||||
@@ -256,7 +273,11 @@ export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }:
|
||||
if (stockResolved) return stockResolved;
|
||||
throw stockError;
|
||||
};
|
||||
target[PATCHED] = true;
|
||||
target[PATCHED] = () => {
|
||||
target._resolveFilename = pristine;
|
||||
delete target[PATCHED];
|
||||
};
|
||||
return uninstall;
|
||||
}
|
||||
|
||||
/** Pinned dependency set materialized into a runtime cache directory. */
|
||||
|
||||
@@ -17,8 +17,13 @@ import {
|
||||
// stock compiled-binary resolver gets wrong (Bun #1763).
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const resolverUninstalls: Array<() => void> = [];
|
||||
|
||||
afterEach(async () => {
|
||||
// Restore the process-wide module resolver first: a leaked patch breaks
|
||||
// `createRequire` relative requires for every later test file (Bun invokes
|
||||
// a JS `_resolveFilename` override with `parent === undefined`).
|
||||
for (const uninstall of resolverUninstalls.splice(0)) uninstall();
|
||||
await Promise.all(tempDirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
@@ -162,7 +167,9 @@ describe("installRuntimeModuleResolver", () => {
|
||||
const sharpStub = path.join(runtimeDir, "sharp-stub.cjs");
|
||||
await Bun.write(sharpStub, "module.exports = {};\n");
|
||||
|
||||
installRuntimeModuleResolver({ runtimeNodeModules: nodeModules, stubs: { sharp: sharpStub } });
|
||||
resolverUninstalls.push(
|
||||
installRuntimeModuleResolver({ runtimeNodeModules: nodeModules, stubs: { sharp: sharpStub } }),
|
||||
);
|
||||
|
||||
const moduleWithResolver = Module as unknown as { default?: ResolveFilenameModule } & ResolveFilenameModule;
|
||||
const resolver = moduleWithResolver.default ?? moduleWithResolver;
|
||||
@@ -172,6 +179,38 @@ describe("installRuntimeModuleResolver", () => {
|
||||
);
|
||||
expect(resolver._resolveFilename("sharp", runtimeParent, false)).toBe(sharpStub);
|
||||
});
|
||||
|
||||
test("uninstall restores the stock resolver and createRequire relative requires", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "omp-runtime-uninstall-"));
|
||||
tempDirs.push(root);
|
||||
await fs.writeFile(path.join(root, "config.js"), 'module.exports = { value: "config-ok" };\n');
|
||||
await fs.writeFile(
|
||||
path.join(root, "entry.mjs"),
|
||||
[
|
||||
'import { createRequire } from "node:module";',
|
||||
"const req = createRequire(import.meta.url);",
|
||||
'const { value } = req("./config.js");',
|
||||
"export { value };",
|
||||
].join("\n"),
|
||||
);
|
||||
const runtimeNodeModules = path.join(root, "runtime", "node_modules");
|
||||
await fs.mkdir(runtimeNodeModules, { recursive: true });
|
||||
|
||||
const moduleWithResolver = Module as unknown as { default?: ResolveFilenameModule } & ResolveFilenameModule;
|
||||
const resolver = moduleWithResolver.default ?? moduleWithResolver;
|
||||
const pristine = resolver._resolveFilename;
|
||||
|
||||
const uninstall = installRuntimeModuleResolver({ runtimeNodeModules });
|
||||
expect(resolver._resolveFilename).not.toBe(pristine);
|
||||
uninstall();
|
||||
expect(resolver._resolveFilename).toBe(pristine);
|
||||
|
||||
// With the stock resolver restored, createRequire-relative requires work.
|
||||
// Dynamic import: the module is a runtime-generated temp file, and the test
|
||||
// intentionally exercises the module-loading boundary the patch breaks.
|
||||
const mod = (await import(path.join(root, "entry.mjs"))) as { value: string };
|
||||
expect(mod.value).toBe("config-ok");
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeRuntimeManifest", () => {
|
||||
|
||||
Reference in New Issue
Block a user