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:
can1357
2026-07-18 22:17:04 +02:00
parent 29aefbca4a
commit ac8e9e05bb
3 changed files with 71 additions and 7 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased] ## [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 ## [17.0.2] - 2026-07-17
### Added ### Added
+27 -6
View File
@@ -194,24 +194,41 @@ export interface RuntimeResolverOptions {
* runtime caches. Stock resolution is tried first and kept for anything * runtime caches. Stock resolution is tried first and kept for anything
* outside the registered roots (bundled imports, node builtins, host or * outside the registered roots (bundled imports, node builtins, host or
* extension trees). Multiple runtime roots may register; they are consulted * 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 * One stock "success" is distrusted: the compiled-binary resolver ignores
* `main`/`exports` for real-FS packages (Bun #1763), so a package shipping * `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`) * 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 * resolves to the wrong file. When the stock hit lands inside a registered
* runtime root, the manifest-aware resolution wins. * 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 registry = resolverRegistry();
const existing = registry.find(entry => entry.runtimeNodeModules === runtimeNodeModules); const existing = registry.find(entry => entry.runtimeNodeModules === runtimeNodeModules);
if (existing) Object.assign(existing.stubs, stubs); if (existing) Object.assign(existing.stubs, stubs);
else registry.push({ runtimeNodeModules, stubs: { ...stubs } }); else registry.push({ runtimeNodeModules, stubs: { ...stubs } });
const resolver = (Module as unknown as { default?: ModuleResolver } & ModuleResolver).default ?? Module; const resolver = (Module as unknown as { default?: ModuleResolver } & ModuleResolver).default ?? Module;
const target = resolver as unknown as ModuleResolver & { [PATCHED]?: boolean }; const target = resolver as unknown as ModuleResolver & { [PATCHED]?: () => void };
if (target[PATCHED]) return; const uninstall = (): void => {
const original = target._resolveFilename.bind(target); 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 => { target._resolveFilename = (request: string, parent: unknown, isMain: boolean, options?: unknown): string => {
let stockResolved: string | null = null; let stockResolved: string | null = null;
let stockError: unknown; let stockError: unknown;
@@ -256,7 +273,11 @@ export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }:
if (stockResolved) return stockResolved; if (stockResolved) return stockResolved;
throw stockError; throw stockError;
}; };
target[PATCHED] = true; target[PATCHED] = () => {
target._resolveFilename = pristine;
delete target[PATCHED];
};
return uninstall;
} }
/** Pinned dependency set materialized into a runtime cache directory. */ /** Pinned dependency set materialized into a runtime cache directory. */
+40 -1
View File
@@ -17,8 +17,13 @@ import {
// stock compiled-binary resolver gets wrong (Bun #1763). // stock compiled-binary resolver gets wrong (Bun #1763).
const tempDirs: string[] = []; const tempDirs: string[] = [];
const resolverUninstalls: Array<() => void> = [];
afterEach(async () => { 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 }))); 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"); const sharpStub = path.join(runtimeDir, "sharp-stub.cjs");
await Bun.write(sharpStub, "module.exports = {};\n"); 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 moduleWithResolver = Module as unknown as { default?: ResolveFilenameModule } & ResolveFilenameModule;
const resolver = moduleWithResolver.default ?? moduleWithResolver; const resolver = moduleWithResolver.default ?? moduleWithResolver;
@@ -172,6 +179,38 @@ describe("installRuntimeModuleResolver", () => {
); );
expect(resolver._resolveFilename("sharp", runtimeParent, false)).toBe(sharpStub); 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", () => { describe("writeRuntimeManifest", () => {