From 4d5eb4d2d7f47dcbeb8314168d5efbb31e6b1f67 Mon Sep 17 00:00:00 2001 From: oxmc7769 Date: Tue, 19 May 2026 03:08:27 -0700 Subject: [PATCH] fix: patch ESM resolve.js during asset build to restore ENOENT catch Node 22.22+ / 24 removed the try/catch around realpathSync in finalizeResolution. realpathSync calls binding.lstat which throws ENOENT for VFS paths (they don't exist on real FS). In Node 24 resolve.js is loaded eagerly during bootstrap before user code runs, so runtime patches to fs.realpathSync arrive too late. Patch the Node source directly during asset builds so the ENOENT guard is compiled into the binary. This silently no-ops on older Node versions where the pattern doesn't match (they already catch ENOENT internally). --- src/patches/third-party-main.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/patches/third-party-main.ts b/src/patches/third-party-main.ts index 9788045..2e35a1c 100644 --- a/src/patches/third-party-main.ts +++ b/src/patches/third-party-main.ts @@ -95,6 +95,14 @@ export default async function main( "assert(!CJSLoader.hasLoadedAnyUserCJSModule)", "/*assert(!CJSLoader.hasLoadedAnyUserCJSModule)*/" ); + // Node 22.22+ / 24 removed the ENOENT catch from finalizeResolution in the + // ESM resolver. realpathSync calls binding.lstat which throws ENOENT for VFS + // paths (they don't exist on real FS). Restore the catch so VFS imports work. + await compiler.replaceInFileAsync( + "lib/internal/modules/esm/resolve.js", + /const real = realpathSync\(path, \{ encoding: ['"]utf8['"] \}\);/, + "let real; try { real = realpathSync(path, { encoding: 'utf8' }); } catch (e) { if (e.code !== 'ENOENT') throw e; real = path; }" + ); const { contents: nodeccContents } = await compiler.readFileAsync( "src/node.cc" );