11import * as nodeModule from "node:module" ;
2- import type { LoadFnOutput , ModuleHooks , ModuleSource } from "node:module" ;
2+ import type { LoadFnOutput , LoadHookContext , ModuleHooks , ModuleSource } from "node:module" ;
33import { Command } from "@gemini-testing/commander" ;
44import { readFileSync } from "node:fs" ;
55import path from "node:path" ;
@@ -39,13 +39,21 @@ export const registerReplModuleHooks = (): void => {
3939function registerNodeModuleHooks ( ) : { revert : ( ) => void } | null {
4040 const registerHooks = nodeModule . registerHooks ;
4141
42- if ( typeof registerHooks !== "function" || hasBrokenMixedModuleHookValidation ( ) ) {
42+ if ( typeof registerHooks !== "function" ) {
4343 return null ;
4444 }
4545
4646 const hooks : ModuleHooks = registerHooks ( {
4747 load ( url , context , nextLoad ) {
48- const result = nextLoad ( url , context ) ;
48+ let result : LoadFnOutput ;
49+
50+ try {
51+ result = nextLoad ( url , context ) ;
52+ } catch ( err ) {
53+ // https://github.com/nodejs/node/issues/57327
54+ // Fixed for REPL instrumentation in Node 24.13.0; older versions mishandle CJS sources with mixed sync/async hooks.
55+ return instrumentLoadAfterFailedLoad ( url , context , err ) ;
56+ }
4957
5058 return instrumentLoadResult ( url , result ) ;
5159 } ,
@@ -54,14 +62,6 @@ function registerNodeModuleHooks(): { revert: () => void } | null {
5462 return { revert : ( ) => hooks . deregister ( ) } ;
5563}
5664
57- function hasBrokenMixedModuleHookValidation ( ) : boolean {
58- const [ major , minor ] = process . versions . node . split ( "." ) . map ( Number ) ;
59-
60- // https://github.com/nodejs/node/issues/57327
61- // Fixed for REPL instrumentation in Node 24.13.0; older versions mishandle CJS sources with mixed sync/async hooks.
62- return major < 24 || ( major === 24 && minor < 13 ) ;
63- }
64-
6565function registerPiratesHook ( ) : { revert : ( ) => void } {
6666 const revert = addHook ( ( code , sourceFile ) => safeInstrumentRepl ( code , sourceFile ) , {
6767 exts : TRANSFORM_CODE_EXTENSIONS ,
@@ -71,6 +71,26 @@ function registerPiratesHook(): { revert: () => void } {
7171 return { revert } ;
7272}
7373
74+ function instrumentLoadAfterFailedLoad ( url : string , context : LoadHookContext , err : unknown ) : LoadFnOutput {
75+ const sourceFile = getSourceFileFromUrl ( url ) ;
76+
77+ if ( ! isInvalidLoadSourceError ( err ) || ! sourceFile || ! shouldReadSourceFile ( sourceFile ) ) {
78+ throw err ;
79+ }
80+
81+ const source = readSourceFile ( sourceFile ) ;
82+
83+ if ( source === null ) {
84+ throw err ;
85+ }
86+
87+ return {
88+ format : getFallbackFormat ( context , sourceFile ) ,
89+ shortCircuit : true ,
90+ source : shouldHandleSourceFile ( sourceFile ) ? safeInstrumentRepl ( source , sourceFile ) : source ,
91+ } ;
92+ }
93+
7494function instrumentLoadResult ( url : string , result : LoadFnOutput ) : LoadFnOutput {
7595 const sourceFile = getSourceFileFromUrl ( url ) ;
7696
@@ -108,6 +128,43 @@ function readSourceFile(sourceFile: string): string | null {
108128 }
109129}
110130
131+ function isInvalidLoadSourceError ( err : unknown ) : boolean {
132+ if ( ! ( err instanceof Error ) ) {
133+ return false ;
134+ }
135+
136+ const error = err as Error & { code ?: string } ;
137+
138+ return (
139+ error . code === "ERR_INVALID_RETURN_PROPERTY_VALUE" &&
140+ error . message . includes ( '"source"' ) &&
141+ error . message . includes ( '"load" hook' )
142+ ) ;
143+ }
144+
145+ function shouldReadSourceFile ( sourceFile : string ) : boolean {
146+ return TRANSFORM_CODE_EXTENSIONS . includes ( path . extname ( sourceFile ) ) ;
147+ }
148+
149+ function getFallbackFormat ( context : LoadHookContext , sourceFile : string ) : string {
150+ if ( context . format ) {
151+ return context . format ;
152+ }
153+
154+ switch ( path . extname ( sourceFile ) ) {
155+ case ".mjs" :
156+ return "module" ;
157+ case ".mts" :
158+ return "module-typescript" ;
159+ case ".ts" :
160+ case ".tsx" :
161+ case ".cts" :
162+ return "commonjs-typescript" ;
163+ default :
164+ return "commonjs" ;
165+ }
166+ }
167+
111168function getSourceFileFromUrl ( url : string ) : string | null {
112169 try {
113170 return url . startsWith ( "file:" ) ? fileURLToPath ( url ) : null ;
@@ -118,8 +175,7 @@ function getSourceFileFromUrl(url: string): string | null {
118175
119176function shouldHandleSourceFile ( sourceFile : string ) : boolean {
120177 return (
121- TRANSFORM_CODE_EXTENSIONS . includes ( path . extname ( sourceFile ) ) &&
122- ! sourceFile . includes ( `${ path . sep } node_modules${ path . sep } ` )
178+ shouldReadSourceFile ( sourceFile ) && ! sourceFile . includes ( `${ path . sep } node_modules${ path . sep } ` )
123179 ) ;
124180}
125181
0 commit comments