diff --git a/packages/runtime/src/scr_node_island_hooks.cjs b/packages/runtime/src/scr_node_island_hooks.cjs index b587132cf..568672b2f 100644 --- a/packages/runtime/src/scr_node_island_hooks.cjs +++ b/packages/runtime/src/scr_node_island_hooks.cjs @@ -30,8 +30,19 @@ function scriptcSharedModules(nativeHost) { const table = JSON.parse(nativeHost.sharedTable()); const none = { entry: (path, load) => load(null), push() {} }; if (table.length === 0) return none; + const { realpathSync } = require("node:fs"); + // Node resolves an import to the module's real path, so a module of a + // tree reached through a symbolic link (a cache directory under macOS's + // /var, which is /private/var) is keyed by its real path as well. const byUrl = new Map(); - table.forEach((m, id) => byUrl.set(pathToFileURL(m.path).href, id)); + table.forEach((m, id) => { + byUrl.set(pathToFileURL(m.path).href, id); + try { + byUrl.set(pathToFileURL(realpathSync(m.path)).href, id); + } catch { + // A module that is not on disk keeps only its own path. + } + }); const TDZ = Symbol("scriptc.tdz"); const setters = new Map(); // `${mod}:${slot}` -> [(value) => void] const entries = new Map(); // entry URL -> { target, late: [[mod, slot]] } diff --git a/packages/runtime/src/scr_node_island_host.h b/packages/runtime/src/scr_node_island_host.h index ad73b644b..849262a1e 100644 --- a/packages/runtime/src/scr_node_island_host.h +++ b/packages/runtime/src/scr_node_island_host.h @@ -769,8 +769,19 @@ static const char scr_node_island_host_js[] = " const table = JSON.parse(nativeHost.sharedTable());\012" " const none = { entry: (path, load) => load(null), push() {} };\012" " if (table.length === 0) return none;\012" + " const { realpathSync } = require(\"node:fs\");\012" + " // Node resolves an import to the module's real path, so a module of a\012" + " // tree reached through a symbolic link (a cache directory under macOS's\012" + " // /var, which is /private/var) is keyed by its real path as well.\012" " const byUrl = new Map();\012" - " table.forEach((m, id) => byUrl.set(pathToFileURL(m.path).href, id));\012" + " table.forEach((m, id) => {\012" + " byUrl.set(pathToFileURL(m.path).href, id);\012" + " try {\012" + " byUrl.set(pathToFileURL(realpathSync(m.path)).href, id);\012" + " } catch {\012" + " // A module that is not on disk keeps only its own path.\012" + " }\012" + " });\012" " const TDZ = Symbol(\"scriptc.tdz\");\012" " const setters = new Map(); // `${mod}:${slot}` -> [(value) => void]\012" " const entries = new Map(); // entry URL -> { target, late: [[mod, slot]] }\012" diff --git a/tests/harness/node-island.test.ts b/tests/harness/node-island.test.ts index 18f765354..432246491 100644 --- a/tests/harness/node-island.test.ts +++ b/tests/harness/node-island.test.ts @@ -18,6 +18,7 @@ import { realpathSync, rmSync, statSync, + symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -544,13 +545,18 @@ export function patchAndBump(): number { // Node strips types only outside node_modules, and the shared island // cache of these tests lives below the repository's node_modules. const islandCache = mkdtempSync(join(tmpdir(), "scriptc-island-cache-")); + // A cache reached through a symbolic link (macOS's temporary directory + // is under /var, which is /private/var): Node resolves the island's + // import to the shared module's real path. + const linkedCache = `${islandCache}-link`; + symlinkSync(islandCache, linkedCache); try { - const out = await run(join(dir, "main"), [], { - ...process.env, - XDG_CACHE_HOME: islandCache, - }); - expect(out).toEqual({ stdout: "2 2\n", stderr: "", code: 0 }); + for (const cache of [islandCache, linkedCache]) { + const out = await run(join(dir, "main"), [], { ...process.env, XDG_CACHE_HOME: cache }); + expect(out).toEqual({ stdout: "2 2\n", stderr: "", code: 0 }); + } } finally { + rmSync(linkedCache, { force: true }); rmSync(islandCache, { recursive: true, force: true }); } }, 300_000);