@@ -1116,6 +1116,29 @@ fn prefetch_module_imports(
11161116 continue ;
11171117 }
11181118
1119+ // Detect CJS and wrap in ESM shim if needed
1120+ let is_cjs = is_likely_cjs ( source_code) ;
1121+ if resolved_path. contains ( "balanced" ) {
1122+ eprintln ! ( "[v8-runtime] balanced-match check: is_cjs={} source_len={} first_100={}" , is_cjs, source_code. len( ) , & source_code[ ..std:: cmp:: min( source_code. len( ) , 100 ) ] ) ;
1123+ }
1124+ let effective_source = if is_cjs {
1125+ eprintln ! ( "[v8-runtime] CJS→ESM shim (prefetch): {}" , resolved_path) ;
1126+ let exports = extract_cjs_export_names ( source_code) ;
1127+ let mut shim = format ! (
1128+ "const _cjsModule = globalThis._requireFrom(\" {}\" , \" /\" );\n export default _cjsModule;\n " ,
1129+ resolved_path. replace( '\\' , "\\ \\ " ) . replace( '"' , "\\ \" " )
1130+ ) ;
1131+ for name in & exports {
1132+ shim. push_str ( & format ! (
1133+ "export const {} = _cjsModule[\" {}\" ];\n " ,
1134+ name, name
1135+ ) ) ;
1136+ }
1137+ shim
1138+ } else {
1139+ source_code. clone ( )
1140+ } ;
1141+
11191142 // Compile the module
11201143 let resource = match v8:: String :: new ( scope, resolved_path) {
11211144 Some ( s) => s,
@@ -1134,7 +1157,7 @@ fn prefetch_module_imports(
11341157 true , // is_module
11351158 None ,
11361159 ) ;
1137- let v8_source = match v8:: String :: new ( scope, source_code ) {
1160+ let v8_source = match v8:: String :: new ( scope, & effective_source ) {
11381161 Some ( s) => s,
11391162 None => continue ,
11401163 } ;
@@ -1211,7 +1234,36 @@ fn resolve_or_compile_module<'s>(
12111234 }
12121235
12131236 // Phase 5: Load and compile the module source.
1214- let source_code = load_module_via_ipc ( scope, ctx, & resolved_path) ?;
1237+ eprintln ! ( "[v8-runtime] loading module: {} (specifier={} referrer={})" , & resolved_path, specifier_str, referrer_name) ;
1238+ let raw_source = load_module_via_ipc ( scope, ctx, & resolved_path) ?;
1239+ eprintln ! ( "[v8-runtime] loaded {} bytes, is_cjs={}" , raw_source. len( ) , is_likely_cjs( & raw_source) ) ;
1240+
1241+ // Phase 5b: Detect CJS modules and wrap in ESM shim.
1242+ // CJS modules use module.exports/exports which isn't valid ESM syntax.
1243+ // Real Node.js wraps CJS in a default+named export shim. We do the same
1244+ // by generating: `const _m = globalThis._requireFrom(path, "/"); export default _m; export const {named1, named2, ...} = _m;`
1245+ let source_code = if is_likely_cjs ( & raw_source) {
1246+ eprintln ! ( "[v8-runtime] CJS→ESM shim for: {}" , & resolved_path) ;
1247+ // Extract potential named exports by scanning for common patterns
1248+ let exports = extract_cjs_export_names ( & raw_source) ;
1249+ let mut shim = format ! (
1250+ "const _cjsModule = globalThis._requireFrom({}, \" /\" );\n export default _cjsModule;\n " ,
1251+ format!( "\" {}\" " , resolved_path. replace( '\\' , "\\ \\ " ) . replace( '"' , "\\ \" " ) )
1252+ ) ;
1253+ if !exports. is_empty ( ) {
1254+ // Re-export each named export from the CJS module
1255+ for name in & exports {
1256+ shim. push_str ( & format ! (
1257+ "export const {} = _cjsModule[\" {}\" ];\n " ,
1258+ name, name
1259+ ) ) ;
1260+ }
1261+ }
1262+ shim
1263+ } else {
1264+ raw_source
1265+ } ;
1266+
12151267 let resource = v8:: String :: new ( scope, & resolved_path) ?;
12161268 let origin = v8:: ScriptOrigin :: new (
12171269 scope,
@@ -1569,7 +1621,9 @@ fn load_module_via_ipc(
15691621 }
15701622 } ;
15711623
1572- match ctx. sync_call ( "_loadFile" , args) {
1624+ let ipc_result = ctx. sync_call ( "_loadFile" , args) ;
1625+ eprintln ! ( "[v8-runtime] _loadFile result: is_ok={} has_bytes={}" , ipc_result. is_ok( ) , ipc_result. as_ref( ) . ok( ) . map( |r| r. is_some( ) ) . unwrap_or( false ) ) ;
1626+ match ipc_result {
15731627 Ok ( Some ( bytes) ) => match deserialize_v8_value ( scope, & bytes) {
15741628 Ok ( val) => {
15751629 if val. is_string ( ) {
@@ -5068,3 +5122,76 @@ mod tests {
50685122 }
50695123 }
50705124}
5125+
5126+ /// Detect if source code is likely CommonJS (not ESM).
5127+ /// Checks for module.exports, exports.X, or require() patterns without ESM import/export.
5128+ fn is_likely_cjs ( source : & str ) -> bool {
5129+ // If it has ESM syntax, it's not CJS
5130+ if source. contains ( "export " ) || source. contains ( "import " ) {
5131+ // Check for actual ESM: `export default`, `export {`, `export const`, `import {`, `import "`, `import(`
5132+ let has_esm_export = source. contains ( "export default" )
5133+ || source. contains ( "export {" )
5134+ || source. contains ( "export const" )
5135+ || source. contains ( "export function" )
5136+ || source. contains ( "export class" )
5137+ || source. contains ( "export var" )
5138+ || source. contains ( "export let" ) ;
5139+ let has_esm_import = source. contains ( "import {" )
5140+ || source. contains ( "import \" " )
5141+ || source. contains ( "import '" )
5142+ || source. contains ( "import *" ) ;
5143+ if has_esm_export || has_esm_import {
5144+ return false ;
5145+ }
5146+ }
5147+ // CJS indicators
5148+ source. contains ( "module.exports" ) || source. contains ( "exports." ) || source. contains ( "require(" )
5149+ }
5150+
5151+ /// Extract named export names from CJS source by scanning for `exports.X =` and
5152+ /// `module.exports = { X: ... }` patterns. Returns a list of valid JS identifiers.
5153+ fn extract_cjs_export_names ( source : & str ) -> Vec < String > {
5154+ use std:: collections:: HashSet ;
5155+ let mut names = HashSet :: new ( ) ;
5156+
5157+ // Pattern 1: exports.NAME = ...
5158+ for line in source. lines ( ) {
5159+ let trimmed = line. trim ( ) ;
5160+ if let Some ( rest) = trimmed. strip_prefix ( "exports." ) {
5161+ if let Some ( eq_pos) = rest. find ( '=' ) {
5162+ let name = rest[ ..eq_pos] . trim ( ) ;
5163+ if is_valid_js_ident ( name) && name != "default" {
5164+ names. insert ( name. to_string ( ) ) ;
5165+ }
5166+ }
5167+ }
5168+ // Pattern 2: Object.defineProperty(exports, "NAME", ...)
5169+ if trimmed. contains ( "Object.defineProperty(exports" ) {
5170+ if let Some ( start) = trimmed. find ( '"' ) . or_else ( || trimmed. find ( '\'' ) ) {
5171+ let rest = & trimmed[ start + 1 ..] ;
5172+ if let Some ( end) = rest. find ( '"' ) . or_else ( || rest. find ( '\'' ) ) {
5173+ let name = & rest[ ..end] ;
5174+ if is_valid_js_ident ( name) && name != "default" && name != "__esModule" {
5175+ names. insert ( name. to_string ( ) ) ;
5176+ }
5177+ }
5178+ }
5179+ }
5180+ }
5181+
5182+ let mut result: Vec < String > = names. into_iter ( ) . collect ( ) ;
5183+ result. sort ( ) ;
5184+ result
5185+ }
5186+
5187+ fn is_valid_js_ident ( s : & str ) -> bool {
5188+ if s. is_empty ( ) {
5189+ return false ;
5190+ }
5191+ let mut chars = s. chars ( ) ;
5192+ let first = chars. next ( ) . unwrap ( ) ;
5193+ if !first. is_alphabetic ( ) && first != '_' && first != '$' {
5194+ return false ;
5195+ }
5196+ chars. all ( |c| c. is_alphanumeric ( ) || c == '_' || c == '$' )
5197+ }
0 commit comments