diff --git a/packages/compiler/src/node-islands/probe-recorder.test.ts b/packages/compiler/src/node-islands/probe-recorder.test.ts index 2e82be3e9..f67522a38 100644 --- a/packages/compiler/src/node-islands/probe-recorder.test.ts +++ b/packages/compiler/src/node-islands/probe-recorder.test.ts @@ -72,3 +72,25 @@ test("the probe never evaluates the program's own source", () => { expect(existsSync(join(root, "src/evaluated-program"))).toBe(false); expect(existsSync(join(root, "src/evaluated-shared"))).toBe(false); }); + +test("a package that catches the refusal does not defer, nor does one that loads it later", () => { + const root = project({ + "src/config.js": marker("config"), + "node_modules/catches/package.json": '{ "name": "catches" }', + "node_modules/catches/index.js": + 'let config;\ntry { config = require("../../src/config.js"); } catch { config = { fallback: true }; }\nmodule.exports = config;\n', + "node_modules/later/package.json": '{ "name": "later" }', + "node_modules/later/index.js": 'module.exports = { config: require("catches") };\n', + "node_modules/clean/package.json": '{ "name": "clean" }', + "node_modules/clean/index.js": "module.exports = { ok: true };\n", + }); + const results = probe(root, [ + join(root, "node_modules/catches/index.js"), + join(root, "node_modules/later/index.js"), + join(root, "node_modules/clean/index.js"), + ]); + const refusal = `the build does not evaluate ${join(root, "src/config.js")}, a file of the program`; + expect(results.map((r) => r.throws)).toEqual([refusal, refusal, undefined]); + expect(results.map((r) => r.root === undefined)).toEqual([true, true, false]); + expect(existsSync(join(root, "src/evaluated-config"))).toBe(false); +}); diff --git a/packages/compiler/src/node-islands/probe-recorder.ts b/packages/compiler/src/node-islands/probe-recorder.ts index af29be084..33a357b43 100644 --- a/packages/compiler/src/node-islands/probe-recorder.ts +++ b/packages/compiler/src/node-islands/probe-recorder.ts @@ -21,7 +21,8 @@ * observable by other island modules evaluated out of order); * - every file the load required (the module's closure), and a throw; * a load that resolves a file outside every node_modules directory (the - * program's own source) throws before that file evaluates; + * program's own source) throws before that file evaluates, and its + * record is that throw even when the load catches it; * * and takes the interface snapshot of the module's value (snapshot.ts). * Calls made by Node's own internals (the module loader reading files, @@ -190,6 +191,11 @@ exports.probe = function probe(input) { const inputs = [], volatile = [], effects = [], tty = new Map(), envKeys = new Map(); const files = new Set(); let recording = false; + // The first program file the current load asked for (the resolve hook), + // and, for every file a load with a refusal evaluated, that program file: + // the module cache keeps what such a file exported without it. + let refused = null; + const refusedBy = new Map(); const note = (list, what) => { if (recording && !internalCaller()) list.push(list === volatile ? what : what + " (" + lastCaller + ")"); }; @@ -440,8 +446,10 @@ exports.probe = function probe(input) { const result = nextResolve(specifier, context); if (recording && typeof result.url === "string" && result.url.startsWith("file:")) { const file = require("node:url").fileURLToPath(result.url); - if (!/[\\/]node_modules[\\/]/.test(file)) + if (!/[\\/]node_modules[\\/]/.test(file)) { + if (refused === null) refused = file; throw new Error("the build does not evaluate " + file + ", a file of the program"); + } files.add(file); } return result; @@ -511,6 +519,7 @@ exports.probe = function probe(input) { pendingTimers.clear(); loadPromises.clear(); tty.clear(); envKeys.clear(); files.clear(); + refused = null; files.add(path); currentFiles = files; currentPackageDir = null; @@ -522,6 +531,15 @@ exports.probe = function probe(input) { recording = true; try { exportsValue = createRequire(path)(path); } catch (e) { threw = String((e && e.message) || e); } recording = false; + // A load that catches the refusal evaluates a fallback the executable + // never runs: its exports are not the module's, and neither are those of + // a later load that reaches one of its files through the module cache. + if (refused === null) + for (const f of files) if (refusedBy.has(f)) { refused = refusedBy.get(f); break; } + if (refused !== null) { + threw = "the build does not evaluate " + refused + ", a file of the program"; + for (const f of files) if (!refusedBy.has(f)) refusedBy.set(f, refused); + } const after = shot(); const root = threw === undefined ? value(exportsValue, 0) : undefined; const mutations = [];