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]
|
## [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
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user