fix(extensibility): read lazy graph modules from disk at import time

The consume-once source map kept entries for graph modules the initial
import never loaded (modules only reached via lazy dynamic imports).
Their first import - possibly long after load, and after an on-disk
edit - was served the boot-time snapshot instead of current file
content, and the unconsumed sources stayed in the plugin closure for
the process lifetime.

Clear the map once the entry import settles: everything Bun loaded at
startup was already consumed (keeping the read-once win), and anything
left must be read at its actual import time, matching pre-dedup
behavior for lazy modules. The new regression test passes on the
pre-dedup baseline and fails on the unfixed dedup.
This commit is contained in:
can1357
2026-07-02 10:29:57 +02:00
parent 29ddf875c7
commit 4e12e5bf76
2 changed files with 46 additions and 5 deletions
@@ -1005,10 +1005,14 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
* so the filter is an exact-path alternation of the graph's realpaths — it
* never matches the host, other extensions, `node_modules` deps, or unrelated
* project source.
*
* Returns the collected path→source map on first install so the caller can
* drop entries the initial import never consumed; `undefined` when the hook
* was already installed.
*/
async function ensureExtensionGraphHook(entryRealPath: string): Promise<void> {
async function ensureExtensionGraphHook(entryRealPath: string): Promise<Map<string, string> | undefined> {
if (hookedExtensionEntries.has(entryRealPath)) {
return;
return undefined;
}
hookedExtensionEntries.add(entryRealPath);
@@ -1032,6 +1036,7 @@ async function ensureExtensionGraphHook(entryRealPath: string): Promise<void> {
});
},
});
return modules;
}
/**
@@ -1050,9 +1055,16 @@ export async function loadLegacyPiModule(resolvedPath: string): Promise<unknown>
// `bun link`/pnpm installs) so the rewrite filter matches the path Bun
// actually hands the hook.
const entryRealPath = await realpathOrSelf(path.resolve(resolvedPath));
await ensureExtensionGraphHook(entryRealPath);
// `?mtime` busts Bun's module cache so repeat loads pick up edited source.
return import(`${toImportSpecifier(entryRealPath)}?mtime=${Date.now()}`);
const pendingSources = await ensureExtensionGraphHook(entryRealPath);
try {
// `?mtime` busts Bun's module cache so repeat loads pick up edited source.
return await import(`${toImportSpecifier(entryRealPath)}?mtime=${Date.now()}`);
} finally {
// Drop whatever the initial import didn't consume: graph modules only
// reached by lazy dynamic imports must be read from disk at their actual
// import time, not served from this load-time snapshot.
pendingSources?.clear();
}
}
function getLoader(path: string): "js" | "jsx" | "ts" | "tsx" {