@@ -274,7 +274,7 @@ bool IsLikelyOptionalModule(const std::string& moduleName) {
274274 return it2->second ->Get (isolate);
275275 }
276276
277- if ([extension isEqualToString: @" mjs" ]) {
277+ if ([extension isEqualToString: @" mjs" ] || [extension isEqualToString: @" js " ] ) {
278278 moduleObj = this ->LoadModule (isolate, path, cacheKey);
279279 } else if ([extension isEqualToString: @" json" ]) {
280280 moduleObj = this ->LoadData (isolate, path);
@@ -311,15 +311,17 @@ bool IsLikelyOptionalModule(const std::string& moduleName) {
311311 // Compile/load the JavaScript/ESM source
312312 Local<Value> scriptValue = LoadScript (isolate, modulePath);
313313
314+ // Check if this is an ES module
314315 bool isESM = modulePath.size () >= 4 && modulePath.compare (modulePath.size () - 4 , 4 , " .mjs" ) == 0 ;
316+ std::shared_ptr<Caches> cache = Caches::Get (isolate);
315317
316- // Debug: Log ES module detection
318+ // Debug: Log module type detection
317319 printf (" LoadModule: Module path: %s, isESM: %s\n " , modulePath.c_str (), isESM ? " true" : " false" );
318320
319321 if (isESM) {
320- // For ES modules the returned value is the module namespace object, not a
321- // factory function. Wire it as the exports and skip CommonJS invocation.
322+ // For ES modules, the returned value is the namespace object
322323 printf (" LoadModule: Processing as ES module\n " );
324+
323325 if (!scriptValue->IsObject ()) {
324326 printf (" LoadModule: ES module failed - scriptValue is not an object\n " );
325327 throw NativeScriptException (isolate, " Failed to load ES module " + modulePath);
@@ -352,7 +354,15 @@ bool IsLikelyOptionalModule(const std::string& moduleName) {
352354 }
353355 }
354356
355- exportsObj = scriptValue.As <Object>();
357+ // Handle exports differently for ES modules vs worker scripts
358+ if (isESM) {
359+ exportsObj = scriptValue.As <Object>();
360+ } else {
361+ // For worker scripts, create an empty exports object since they don't export anything
362+ // They work through global scope (self.onmessage, etc.)
363+ exportsObj = Object::New (isolate);
364+ }
365+
356366 bool succ =
357367 moduleObj->Set (context, tns::ToV8String (isolate, " exports" ), exportsObj).FromMaybe (false );
358368 tns::Assert (succ, isolate);
@@ -522,6 +532,15 @@ ScriptOrigin origin(
522532
523533 // 3) Register for resolution callback
524534 extern std::unordered_map<std::string, Global<Module>> g_moduleRegistry;
535+
536+ // Safe Global handle management: Clear any existing entry first
537+ auto it = g_moduleRegistry.find (path);
538+ if (it != g_moduleRegistry.end ()) {
539+ // Clear the existing Global handle before replacing it
540+ it->second .Reset ();
541+ }
542+
543+ // Now safely set the new module handle
525544 g_moduleRegistry[path].Reset (isolate, module );
526545
527546 // 4) Save cache if first time
@@ -559,30 +578,49 @@ ScriptOrigin origin(
559578
560579 // Handle the case where evaluation returns a Promise (for top-level await)
561580 if (result->IsPromise ()) {
562- printf (" LoadESModule: Module evaluation returned a Promise, waiting for resolution\n " );
581+ printf (" LoadESModule: Module evaluation returned a Promise, processing...\n " );
582+
583+ // Use TryCatch to safely handle Promise operations
584+ TryCatch promiseTc (isolate);
563585 Local<Promise> promise = result.As <Promise>();
564586
565- // For worker context, we need to wait for the promise to resolve
566- // This is important for modules that use top-level await or have async initialization
567- std::shared_ptr<Caches> cache = Caches::Get (isolate);
568- if (cache->isWorker ) {
569- printf (" LoadESModule: In worker context, processing promise resolution\n " );
570-
571- // Run the microtask queue to allow the promise to resolve
572- while (promise->State () == Promise::kPending ) {
573- isolate->PerformMicrotaskCheckpoint ();
574- // Add a small delay to prevent busy waiting
575- usleep (1000 ); // 1ms
587+ // Process microtasks to allow Promise resolution (for both worker and main contexts)
588+ printf (" LoadESModule: Processing microtasks for Promise resolution\n " );
589+
590+ // Limited attempts to resolve the promise to avoid infinite loops
591+ int maxAttempts = 100 ;
592+ int attempts = 0 ;
593+
594+ while (attempts < maxAttempts && !promiseTc.HasCaught ()) {
595+ isolate->PerformMicrotaskCheckpoint ();
596+
597+ // Check promise state safely
598+ if (promiseTc.HasCaught ()) {
599+ printf (" LoadESModule: Exception during Promise processing, breaking\n " );
600+ break ;
576601 }
577602
578- if (promise->State () == Promise::kRejected ) {
579- printf (" LoadESModule: Promise was rejected\n " );
580- Local<Value> reason = promise->Result ();
581- isolate->ThrowException (reason);
582- throw NativeScriptException (isolate, tcEval, " Module evaluation promise rejected" );
603+ Promise::PromiseState state = promise->State ();
604+
605+ if (state != Promise::kPending ) {
606+ if (state == Promise::kRejected ) {
607+ printf (" LoadESModule: Promise was rejected\n " );
608+ if (!promiseTc.HasCaught ()) {
609+ Local<Value> reason = promise->Result ();
610+ isolate->ThrowException (reason);
611+ }
612+ throw NativeScriptException (isolate, promiseTc, " Module evaluation promise rejected" );
613+ }
614+ printf (" LoadESModule: Promise resolved successfully\n " );
615+ break ;
583616 }
584617
585- printf (" LoadESModule: Promise resolved successfully\n " );
618+ attempts++;
619+ usleep (100 ); // 0.1ms delay
620+ }
621+
622+ if (attempts >= maxAttempts) {
623+ printf (" LoadESModule: Promise resolution timeout, continuing anyway\n " );
586624 }
587625 }
588626 }
@@ -620,14 +658,17 @@ ScriptOrigin origin(
620658 // in a function expression would turn those top-level keywords into syntax
621659 // errors (e.g. `export *` → "Unexpected token '*'").
622660
661+ // Check if we're in a worker context
662+ std::shared_ptr<Caches> cache = Caches::Get (isolate);
663+ bool isWorkerContext = cache && cache->isWorker ;
664+
623665 if (path.size () >= 4 && path.compare (path.size () - 4 , 4 , " .mjs" ) == 0 ) {
624666 // Read raw text without wrapping.
625667 std::string sourceText = tns::ReadText (path);
626668
627669 // For ES modules in worker context, we need to provide access to global objects
628670 // since ES modules run in their own scope
629- std::shared_ptr<Caches> cache = Caches::Get (isolate);
630- if (cache && cache->isWorker ) {
671+ if (isWorkerContext) {
631672 // Prepend global declarations to make worker globals available in ES module scope
632673 std::string globalDeclarations = " const self = globalThis.self || globalThis;\n "
633674 " const postMessage = globalThis.postMessage;\n "
@@ -642,6 +683,9 @@ ScriptOrigin origin(
642683 return tns::ToV8String (isolate, sourceText);
643684 }
644685
686+ // Worker .js files should use CommonJS wrapping like regular .js files
687+ // This ensures proper runtime context and global object setup
688+
645689 return tns::ReadModule (isolate, path);
646690}
647691
0 commit comments