66#include < string>
77#include " Caches.h"
88#include " Helpers.h"
9+ #include " ModuleInternalCallbacks.h" // for ResolveModuleCallback
910#include " NativeScriptException.h"
1011#include " RuntimeConfig.h"
1112
115116
116117 const char * path1 = [fullPath fileSystemRepresentation ];
117118 const char * path2 =
118- [[fullPath stringByAppendingPathExtension: @" js " ] fileSystemRepresentation ];
119+ [[fullPath stringByAppendingPathExtension: @" mjs " ] fileSystemRepresentation ];
119120
120121 if (!tns::Exists (path1) && !tns::Exists (path2)) {
121122 fullPath = [tnsModulesPath stringByAppendingPathComponent: @" tns-core-modules" ];
183184 return it2->second ->Get (isolate);
184185 }
185186
186- if ([extension isEqualToString: @" js " ]) {
187+ if ([extension isEqualToString: @" mjs " ]) {
187188 moduleObj = this ->LoadModule (isolate, path, cacheKey);
188189 } else if ([extension isEqualToString: @" json" ]) {
189190 moduleObj = this ->LoadData (isolate, path);
217218 std::make_shared<Persistent<Object>>(isolate, moduleObj);
218219 TempModule tempModule (this , modulePath, cacheKey, poModuleObj);
219220
220- Local<Script> script = LoadScript (isolate, modulePath);
221+ Local<Value> scriptValue = LoadScript (isolate, modulePath);
222+
223+ if (!scriptValue->IsFunction ()) {
224+ throw NativeScriptException (isolate,
225+ " Expected module factory to be a function for " + modulePath);
226+ }
227+ v8::Local<v8::Function> moduleFunc = scriptValue.As <v8::Function>();
221228
222- Local<v8::Function> moduleFunc;
223229 {
224230 TryCatch tc (isolate);
225- moduleFunc = script->Run (context).ToLocalChecked ().As <v8::Function>();
231+ // moduleFunc = script->Run(context).ToLocalChecked().As<v8::Function>();
226232 if (tc.HasCaught ()) {
227233 throw NativeScriptException (isolate, tc, " Error running script " + modulePath);
228234 }
282288 return json;
283289}
284290
285- Local<Script> ModuleInternal::LoadScript (Isolate* isolate, const std::string& path) {
286- Local<Context> context = isolate->GetCurrentContext ();
287- std::string baseOrigin = tns::ReplaceAll (path, RuntimeConfig.BaseDir , " " );
288- std::string fullRequiredModulePathWithSchema = " file://" + baseOrigin;
289- ScriptOrigin origin (isolate, tns::ToV8String (isolate, fullRequiredModulePathWithSchema));
290- Local<v8::String> scriptText = WrapModuleContent (isolate, path);
291- ScriptCompiler::CachedData* cacheData = LoadScriptCache (path);
292- ScriptCompiler::Source source (scriptText, origin, cacheData);
293-
294- ScriptCompiler::CompileOptions options = ScriptCompiler::kNoCompileOptions ;
295-
296- if (cacheData != nullptr ) {
297- options = ScriptCompiler::kConsumeCodeCache ;
291+ Local<Value> ModuleInternal::LoadScript (Isolate* isolate, const std::string& path) {
292+ // Simple dispatch on extension:
293+ if (path.size () >= 4 && path.compare (path.size () - 4 , 4 , " .mjs" ) == 0 ) {
294+ return ModuleInternal::LoadESModule (isolate, path);
295+ } else {
296+ Local<Script> script = ModuleInternal::LoadClassicScript (isolate, path);
297+ // run it and return the value
298+ return script->Run (isolate->GetCurrentContext ()).ToLocalChecked ();
298299 }
300+ }
301+
302+ Local<Script> ModuleInternal::LoadClassicScript (Isolate* isolate, const std::string& path) {
303+ auto context = isolate->GetCurrentContext ();
304+ // build URL
305+ std::string base = ReplaceAll (path, RuntimeConfig.BaseDir , " " );
306+ std::string url = " file://" + base;
307+
308+ // wrap & cache lookup
309+ Local<v8::String> sourceText = ModuleInternal::WrapModuleContent (isolate, path);
310+ auto * cacheData = ModuleInternal::LoadScriptCache (path);
311+
312+ // note: is_module=false here
313+ ScriptOrigin origin (
314+ isolate,
315+ v8::String::NewFromUtf8 (isolate, url.c_str (), NewStringType::kNormal ).ToLocalChecked (),
316+ 0 , // line offset
317+ 0 , // column offset
318+ false , // shared_cross_origin
319+ -1 , // script_id
320+ Local<Value>(),
321+ false , // is_opaque
322+ false , // is_wasm
323+ false // is_module
324+ );
325+ ScriptCompiler::Source source (sourceText, origin, cacheData);
326+
327+ auto opts = cacheData ? ScriptCompiler::kConsumeCodeCache : ScriptCompiler::kNoCompileOptions ;
299328
300- Local<Script> script;
301329 TryCatch tc (isolate);
302- bool success = ScriptCompiler::Compile (context, &source, options). ToLocal (& script) ;
303- if (!success || tc.HasCaught ()) {
304- throw NativeScriptException (isolate, tc, " Cannot compile " + path);
330+ Local<Script> script;
331+ if (!ScriptCompiler::Compile (context, &source, opts). ToLocal (&script) || tc.HasCaught ()) {
332+ throw NativeScriptException (isolate, tc, " Cannot compile script " + path);
305333 }
306334
307335 if (cacheData == nullptr ) {
308- SaveScriptCache (script, path);
336+ ModuleInternal:: SaveScriptCache (script, path);
309337 }
310338
311339 return script;
312340}
313341
342+ Local<Value> ModuleInternal::LoadESModule (Isolate* isolate, const std::string& path) {
343+ auto context = isolate->GetCurrentContext ();
344+
345+ // 1) Prepare URL & source
346+ std::string base = ReplaceAll (path, RuntimeConfig.BaseDir , " " );
347+ std::string url = " file://" + base;
348+ v8::Local<v8::String> sourceText = ModuleInternal::WrapModuleContent (isolate, path);
349+ auto * cacheData = ModuleInternal::LoadScriptCache (path);
350+
351+ ScriptOrigin origin (
352+ isolate,
353+ v8::String::NewFromUtf8 (isolate, url.c_str (), NewStringType::kNormal ).ToLocalChecked (), 0 , 0 ,
354+ false , -1 , Local<Value>(), false , false ,
355+ true // ← is_module
356+ );
357+ ScriptCompiler::Source source (sourceText, origin, cacheData);
358+
359+ // 2) Compile with its own TryCatch
360+ Local<Module> module ;
361+ {
362+ TryCatch tcCompile (isolate);
363+ MaybeLocal<Module> maybeMod = ScriptCompiler::CompileModule (
364+ isolate, &source,
365+ cacheData ? ScriptCompiler::kConsumeCodeCache : ScriptCompiler::kNoCompileOptions );
366+
367+ if (!maybeMod.ToLocal (&module )) {
368+ // V8 threw a syntax error or similar
369+ throw NativeScriptException (isolate, tcCompile, " Cannot compile ES module " + path);
370+ }
371+ }
372+
373+ // 3) Register for resolution callback
374+ extern std::unordered_map<std::string, Global<Module>> g_moduleRegistry;
375+ g_moduleRegistry[path].Reset (isolate, module );
376+
377+ // 4) Save cache if first time
378+ if (cacheData == nullptr ) {
379+ Local<UnboundModuleScript> unbound = module ->GetUnboundModuleScript ();
380+ auto * generatedCache = ScriptCompiler::CreateCodeCache (unbound);
381+ ModuleInternal::SaveScriptCache (generatedCache, path);
382+ }
383+
384+ // 5) Instantiate (link) with its own TryCatch
385+ {
386+ TryCatch tcLink (isolate);
387+ bool linked = module ->InstantiateModule (context, &ResolveModuleCallback).FromMaybe (false );
388+
389+ if (!linked) {
390+ if (tcLink.HasCaught ()) {
391+ throw NativeScriptException (isolate, tcLink, " Cannot instantiate module " + path);
392+ } else {
393+ // V8 gave no exception object—throw plain text
394+ throw NativeScriptException (isolate, " Cannot instantiate module " + path);
395+ }
396+ }
397+ }
398+
399+ // 6) Evaluate with its own TryCatch
400+ Local<Value> result;
401+ {
402+ TryCatch tcEval (isolate);
403+ if (!module ->Evaluate (context).ToLocal (&result)) {
404+ throw NativeScriptException (isolate, tcEval, " Cannot evaluate module " + path);
405+ }
406+ }
407+
408+ // 7) Return the namespace
409+ return module ->GetModuleNamespace ();
410+ }
411+
314412MaybeLocal<Value> ModuleInternal::RunScriptString (Isolate* isolate, Local<Context> context,
315413 const std::string scriptString) {
316414 ScriptCompiler::CompileOptions options = ScriptCompiler::kNoCompileOptions ;
332430 this ->RunScriptString (isolate, context, script);
333431}
334432
335- Local<v8::String> ModuleInternal::WrapModuleContent (Isolate* isolate, const std::string& path) {
433+ v8::Local<v8::String> ModuleInternal::WrapModuleContent (v8::Isolate* isolate,
434+ const std::string& path) {
435+ // For classical scripts we wrap the source into the CommonJS factory function
436+ // but for ES modules (".mjs") we must leave the source intact so that the
437+ // V8 parser can recognise the "export"/"import" syntax. Wrapping an ES module
438+ // in a function expression would turn those top-level keywords into syntax
439+ // errors (e.g. `export *` → "Unexpected token '*'").
440+
441+ if (path.size () >= 4 && path.compare (path.size () - 4 , 4 , " .mjs" ) == 0 ) {
442+ // Read raw text without wrapping.
443+ std::string sourceText = tns::ReadText (path);
444+ return tns::ToV8String (isolate, sourceText);
445+ }
446+
336447 return tns::ReadModule (isolate, path);
337448}
338449
348459 BOOL exists = [fileManager fileExistsAtPath: fullPath isDirectory: &isDirectory];
349460
350461 if (exists == YES && isDirectory == YES ) {
351- NSString * jsFile = [fullPath stringByAppendingPathExtension: @" js " ];
462+ NSString * jsFile = [fullPath stringByAppendingPathExtension: @" mjs " ];
352463 BOOL isDir;
353464 if ([fileManager fileExistsAtPath: jsFile isDirectory: &isDir] && isDir == NO ) {
354465 return [jsFile UTF8String ];
355466 }
356467 }
357468
358469 if (exists == NO ) {
359- fullPath = [fullPath stringByAppendingPathExtension: @" js " ];
470+ fullPath = [fullPath stringByAppendingPathExtension: @" mjs " ];
360471 exists = [fileManager fileExistsAtPath: fullPath isDirectory: &isDirectory];
361472 }
362473
@@ -387,9 +498,9 @@ throw NativeScriptException("Unable to locate main entry in " +
387498 }
388499
389500 if (exists == NO ) {
390- fullPath = [fullPath stringByAppendingPathExtension: @" js " ];
501+ fullPath = [fullPath stringByAppendingPathExtension: @" mjs " ];
391502 } else {
392- fullPath = [fullPath stringByAppendingPathComponent: @" index.js " ];
503+ fullPath = [fullPath stringByAppendingPathComponent: @" index.mjs " ];
393504 }
394505
395506 exists = [fileManager fileExistsAtPath: fullPath isDirectory: &isDirectory];
@@ -448,7 +559,7 @@ throw NativeScriptException("Unable to locate main entry in " +
448559 }
449560
450561 long length = 0 ;
451- std::string cachePath = GetCacheFileName (path + " .cache" );
562+ std::string cachePath = ModuleInternal:: GetCacheFileName (path + " .cache" );
452563
453564 struct stat result;
454565 if (stat (cachePath.c_str (), &result) == 0 ) {
@@ -474,6 +585,33 @@ throw NativeScriptException("Unable to locate main entry in " +
474585 isNew ? ScriptCompiler::CachedData::BufferOwned : ScriptCompiler::CachedData::BufferNotOwned);
475586}
476587
588+ void ModuleInternal::SaveScriptCache (const ScriptCompiler::CachedData* cache,
589+ const std::string& path) {
590+ std::string cachePath = ModuleInternal::GetCacheFileName (path + " .cache" );
591+
592+ // std::ofstream ofs(cachePath, std::ios::binary);
593+ // if (!ofs) return; // or throw
594+
595+ // ofs.write(reinterpret_cast<const char*>(cache->data),
596+ // cache->length);
597+ // ofs.close();
598+
599+ int length = cache->length ;
600+ tns::WriteBinary (cachePath, cache->data , length);
601+ delete cache;
602+
603+ // make sure cache and js file have the same modification date
604+ struct stat result;
605+ struct utimbuf new_times;
606+ new_times.actime = time (nullptr );
607+ new_times.modtime = time (nullptr );
608+ if (stat (path.c_str (), &result) == 0 ) {
609+ auto jsLastModifiedTime = result.st_mtime ;
610+ new_times.modtime = jsLastModifiedTime;
611+ }
612+ utime (cachePath.c_str (), &new_times);
613+ }
614+
477615void ModuleInternal::SaveScriptCache (const Local<Script> script, const std::string& path) {
478616 if (RuntimeConfig.IsDebug ) {
479617 return ;
@@ -484,7 +622,7 @@ throw NativeScriptException("Unable to locate main entry in " +
484622 ScriptCompiler::CachedData* cachedData = ScriptCompiler::CreateCodeCache (unboundScript);
485623
486624 int length = cachedData->length ;
487- std::string cachePath = GetCacheFileName (path + " .cache" );
625+ std::string cachePath = ModuleInternal:: GetCacheFileName (path + " .cache" );
488626 tns::WriteBinary (cachePath, cachedData->data , length);
489627 delete cachedData;
490628
0 commit comments