diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 6bebb05ea..cf264b957 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -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 diff --git a/packages/utils/src/runtime-install.ts b/packages/utils/src/runtime-install.ts index c290b0eee..aced56e9a 100644 --- a/packages/utils/src/runtime-install.ts +++ b/packages/utils/src/runtime-install.ts @@ -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. */ diff --git a/packages/utils/test/runtime-install.test.ts b/packages/utils/test/runtime-install.test.ts index ecef20455..8d8533fdc 100644 --- a/packages/utils/test/runtime-install.test.ts +++ b/packages/utils/test/runtime-install.test.ts @@ -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", () => {