fix(extensions): refreshed formerly synchronous sources on reload walks

- A reload that drops a module's last require() edge leaves the permanent
  hooks serving it from the synchronous snapshot map, which was only
  refreshed while the path stayed flagged; an edit after the downgrade
  replayed stale bytes. Ensure now re-rewrites and refreshes the snapshot
  for every ever-synchronous path on each graph walk.
- Added the mirror reload regression (require edge dropped + source edited).
This commit is contained in:
can1357
2026-08-03 05:42:07 +02:00
parent 03c07f89ec
commit 0244e18e43
2 changed files with 55 additions and 0 deletions
@@ -2427,6 +2427,12 @@ async function ensureExtensionGraphHook(entryRealPath: string): Promise<{ clear(
}
if (synchronousSourcePaths.has(modulePath)) {
synchronousModuleSources.set(modulePath, source);
} else if (synchronousModuleSources.has(modulePath)) {
// The path lost its require() edges on this walk, but the permanent
// hooks installed while it was synchronous still serve it from this
// map — keep the pre-rewritten bytes fresh instead of serving the
// stale snapshot from the walk that flagged it.
synchronousModuleSources.set(modulePath, await rewriteLegacyExtensionSource(source, modulePath));
}
}
let hookedModules = extensionGraphHookModules.get(entryRealPath);
@@ -1184,6 +1184,55 @@ describe("legacy-pi in-place module loading (issue #1674)", () => {
expect(secondLoad()).toBe("reload-upgrade-ok");
});
it("serves fresh source after a reload drops a module's require edge", async () => {
const dir = await writePackage({
"package.json": JSON.stringify({ name: "reload-downgrade-ext", version: "1.0.0", type: "module" }),
"node_modules/cjs-parent/package.json": JSON.stringify({
name: "cjs-parent",
version: "1.0.0",
main: "index.js",
}),
"node_modules/cjs-parent/index.js": 'module.exports = { load: () => require("../../lib.js").value };',
// v1 reaches `lib.js` through a require() edge, so the first load
// flags it synchronous and snapshots its pre-rewritten source.
"lib.js": 'export const value = "downgrade-v1";',
"index.js": [
'import parent from "cjs-parent";',
"export const loadValue = parent.load;",
"export default function (pi) { void pi; }",
].join("\n"),
});
const entry = path.join(dir, "index.js");
const first = await loadLegacyPiModule(entry);
const firstLoad = Reflect.get(Object(first), "loadValue");
if (typeof firstLoad !== "function") {
throw new Error("reload-downgrade fixture v1 did not export loadValue");
}
expect(firstLoad()).toBe("downgrade-v1");
// v2 drops the require edge and edits lib.js. The permanent hooks still
// serve the path from the synchronous snapshot, so the reload walk must
// refresh it instead of replaying the v1 bytes.
await fs.writeFile(path.join(dir, "lib.js"), 'export const value = "downgrade-v2";', "utf8");
await fs.writeFile(
entry,
[
'import { value } from "./lib.js";',
"export const loadValue = () => value;",
"export default function (pi) { void pi; }",
].join("\n"),
"utf8",
);
const second = await loadLegacyPiModule(entry);
const secondLoad = Reflect.get(Object(second), "loadValue");
if (typeof secondLoad !== "function") {
throw new Error("reload-downgrade fixture v2 did not export loadValue");
}
expect(secondLoad()).toBe("downgrade-v2");
});
it("chooses the ESM branch when dual package graphs converge", async () => {
const dir = await writePackage({
"package.json": JSON.stringify({ name: "dual-convergence-ext", version: "1.0.0", type: "module" }),