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:
@@ -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" }),
|
||||
|
||||
Reference in New Issue
Block a user