@@ -129,6 +129,7 @@ v8::MaybeLocal<v8::Module> ResolveModuleCallback(v8::Local<v8::Context> context,
129129
130130 // Debug logging
131131 DEBUG_WRITE (" ResolveModuleCallback: Resolving '%s'" , spec.c_str ());
132+ __android_log_print (ANDROID_LOG_DEBUG, " TNS.ResolveCallback" , " Resolving module: '%s'" , spec.c_str ());
132133
133134 // 2) Find which filepath the referrer was compiled under
134135 std::string referrerPath;
@@ -165,10 +166,33 @@ v8::MaybeLocal<v8::Module> ResolveModuleCallback(v8::Local<v8::Context> context,
165166 } else if (spec.size () > 7 && spec.substr (0 , 7 ) == " file://" ) {
166167 // Absolute file URL
167168 std::string tail = spec.substr (7 ); // strip file://
168- if (tail[0 ] != ' /' ) {
169+ if (tail. empty () || tail [0 ] != ' /' ) {
169170 tail = " /" + tail;
170171 }
171- std::string candidate = appPath + tail;
172+
173+ // Map common virtual roots to the real appPath
174+ const std::string appVirtualRoot = " /app/" ; // e.g. file:///app/foo.mjs
175+ const std::string androidAssetAppRoot = " /android_asset/app/" ; // e.g. file:///android_asset/app/foo.mjs
176+
177+ std::string candidate;
178+ if (tail.rfind (appVirtualRoot, 0 ) == 0 ) {
179+ // Drop the leading "/app/" and prepend real appPath
180+ candidate = appPath + " /" + tail.substr (appVirtualRoot.size ());
181+ DEBUG_WRITE (" ResolveModuleCallback: file:// to appPath mapping: '%s' -> '%s'" , tail.c_str (), candidate.c_str ());
182+ } else if (tail.rfind (androidAssetAppRoot, 0 ) == 0 ) {
183+ // Replace "/android_asset/app/" with the real appPath
184+ candidate = appPath + " /" + tail.substr (androidAssetAppRoot.size ());
185+ DEBUG_WRITE (" ResolveModuleCallback: file:// android_asset mapping: '%s' -> '%s'" , tail.c_str (), candidate.c_str ());
186+ } else if (tail.rfind (appPath, 0 ) == 0 ) {
187+ // Already an absolute on-disk path to the app folder
188+ candidate = tail;
189+ DEBUG_WRITE (" ResolveModuleCallback: file:// absolute path preserved: '%s'" , candidate.c_str ());
190+ } else {
191+ // Fallback: treat as absolute on-disk path
192+ candidate = tail;
193+ DEBUG_WRITE (" ResolveModuleCallback: file:// generic absolute: '%s'" , candidate.c_str ());
194+ }
195+
172196 candidateBases.push_back (candidate);
173197 } else if (!spec.empty () && spec[0 ] == ' ~' ) {
174198 // Alias to application root using ~/path
@@ -258,6 +282,85 @@ v8::MaybeLocal<v8::Module> ResolveModuleCallback(v8::Local<v8::Context> context,
258282 " export function pathToFileURL(path) {\n "
259283 " return new URL('file://' + encodeURIComponent(path));\n "
260284 " }\n " ;
285+ } else if (builtinName == " module" ) {
286+ // Create a polyfill for node:module with createRequire
287+ polyfillContent = " // Polyfill for node:module\n "
288+ " export function createRequire(filename) {\n "
289+ " // Return the global require function\n "
290+ " // In NativeScript, require is globally available\n "
291+ " if (typeof require === 'function') {\n "
292+ " return require;\n "
293+ " }\n "
294+ " \n "
295+ " // Fallback: create a basic require function\n "
296+ " return function(id) {\n "
297+ " throw new Error('Module ' + id + ' not found. NativeScript require() not available.');\n "
298+ " };\n "
299+ " }\n "
300+ " \n "
301+ " // Export as default as well for compatibility\n "
302+ " export default { createRequire };\n " ;
303+ } else if (builtinName == " path" ) {
304+ // Create a polyfill for node:path
305+ polyfillContent = " // Polyfill for node:path\n "
306+ " export const sep = '/';\n "
307+ " export const delimiter = ':';\n "
308+ " \n "
309+ " export function basename(path, ext) {\n "
310+ " const name = path.split('/').pop() || '';\n "
311+ " return ext && name.endsWith(ext) ? name.slice(0, -ext.length) : name;\n "
312+ " }\n "
313+ " \n "
314+ " export function dirname(path) {\n "
315+ " const parts = path.split('/');\n "
316+ " return parts.slice(0, -1).join('/') || '/';\n "
317+ " }\n "
318+ " \n "
319+ " export function extname(path) {\n "
320+ " const name = basename(path);\n "
321+ " const dot = name.lastIndexOf('.');\n "
322+ " return dot > 0 ? name.slice(dot) : '';\n "
323+ " }\n "
324+ " \n "
325+ " export function join(...paths) {\n "
326+ " return paths.filter(Boolean).join('/').replace(/\\ /+/g, '/');\n "
327+ " }\n "
328+ " \n "
329+ " export function resolve(...paths) {\n "
330+ " let resolved = '';\n "
331+ " for (let path of paths) {\n "
332+ " if (path.startsWith('/')) {\n "
333+ " resolved = path;\n "
334+ " } else {\n "
335+ " resolved = join(resolved, path);\n "
336+ " }\n "
337+ " }\n "
338+ " return resolved || '/';\n "
339+ " }\n "
340+ " \n "
341+ " export function isAbsolute(path) {\n "
342+ " return path.startsWith('/');\n "
343+ " }\n "
344+ " \n "
345+ " export default { basename, dirname, extname, join, resolve, isAbsolute, sep, delimiter };\n " ;
346+ } else if (builtinName == " fs" ) {
347+ // Create a basic polyfill for node:fs
348+ polyfillContent = " // Polyfill for node:fs\n "
349+ " console.warn('Node.js fs module is not supported in NativeScript. Use @nativescript/core File APIs instead.');\n "
350+ " \n "
351+ " export function readFileSync() {\n "
352+ " throw new Error('fs.readFileSync is not supported in NativeScript. Use @nativescript/core File APIs.');\n "
353+ " }\n "
354+ " \n "
355+ " export function writeFileSync() {\n "
356+ " throw new Error('fs.writeFileSync is not supported in NativeScript. Use @nativescript/core File APIs.');\n "
357+ " }\n "
358+ " \n "
359+ " export function existsSync() {\n "
360+ " throw new Error('fs.existsSync is not supported in NativeScript. Use @nativescript/core File APIs.');\n "
361+ " }\n "
362+ " \n "
363+ " export default { readFileSync, writeFileSync, existsSync };\n " ;
261364 } else {
262365 // Generic polyfill for other Node.js built-in modules
263366 polyfillContent = " // Polyfill for node:" + builtinName + " \n "
@@ -398,3 +501,81 @@ v8::MaybeLocal<v8::Module> ResolveModuleCallback(v8::Local<v8::Context> context,
398501
399502 return v8::MaybeLocal<v8::Module>(it2->second .Get (isolate));
400503}
504+
505+ // Dynamic import() host callback
506+ v8::MaybeLocal<v8::Promise> ImportModuleDynamicallyCallback (
507+ v8::Local<v8::Context> context, v8::Local<v8::Data> host_defined_options,
508+ v8::Local<v8::Value> resource_name, v8::Local<v8::String> specifier,
509+ v8::Local<v8::FixedArray> import_assertions) {
510+ v8::Isolate* isolate = context->GetIsolate ();
511+
512+ // Convert specifier to std::string for logging
513+ v8::String::Utf8Value specUtf8 (isolate, specifier);
514+ std::string spec = *specUtf8 ? *specUtf8 : " " ;
515+
516+ DEBUG_WRITE (" ImportModuleDynamicallyCallback: Dynamic import for '%s'" , spec.c_str ());
517+ __android_log_print (ANDROID_LOG_DEBUG, " TNS.ImportCallback" , " Dynamic import: '%s'" , spec.c_str ());
518+
519+ v8::EscapableHandleScope scope (isolate);
520+
521+ // Create a Promise resolver we'll resolve/reject synchronously for now.
522+ v8::Local<v8::Promise::Resolver> resolver;
523+ if (!v8::Promise::Resolver::New (context).ToLocal (&resolver)) {
524+ // Failed to create resolver, return empty promise
525+ return v8::MaybeLocal<v8::Promise>();
526+ }
527+
528+ // Re-use the static resolver to locate / compile the module.
529+ try {
530+ // Pass empty referrer since this V8 version doesn't expose GetModule() on
531+ // ScriptOrModule. The resolver will fall back to absolute-path heuristics.
532+ v8::Local<v8::Module> refMod;
533+
534+ v8::MaybeLocal<v8::Module> maybeModule =
535+ ResolveModuleCallback (context, specifier, import_assertions, refMod);
536+
537+ v8::Local<v8::Module> module ;
538+ if (!maybeModule.ToLocal (&module )) {
539+ // Resolution failed; reject to avoid leaving a pending Promise (white screen)
540+ DEBUG_WRITE (" ImportModuleDynamicallyCallback: Resolution failed for '%s'" , spec.c_str ());
541+ v8::Local<v8::Value> ex = v8::Exception::Error (
542+ ArgConverter::ConvertToV8String (isolate, std::string (" Failed to resolve module: " ) + spec));
543+ resolver->Reject (context, ex).Check ();
544+ return scope.Escape (resolver->GetPromise ());
545+ }
546+
547+ // If not yet instantiated/evaluated, do it now
548+ if (module ->GetStatus () == v8::Module::kUninstantiated ) {
549+ if (!module ->InstantiateModule (context, &ResolveModuleCallback).FromMaybe (false )) {
550+ DEBUG_WRITE (" ImportModuleDynamicallyCallback: Instantiate failed for '%s'" , spec.c_str ());
551+ resolver
552+ ->Reject (context,
553+ v8::Exception::Error (ArgConverter::ConvertToV8String (isolate, " Failed to instantiate module" )))
554+ .Check ();
555+ return scope.Escape (resolver->GetPromise ());
556+ }
557+ }
558+
559+ if (module ->GetStatus () != v8::Module::kEvaluated ) {
560+ if (module ->Evaluate (context).IsEmpty ()) {
561+ DEBUG_WRITE (" ImportModuleDynamicallyCallback: Evaluation failed for '%s'" , spec.c_str ());
562+ v8::Local<v8::Value> ex =
563+ v8::Exception::Error (ArgConverter::ConvertToV8String (isolate, " Evaluation failed" ));
564+ resolver->Reject (context, ex).Check ();
565+ return scope.Escape (resolver->GetPromise ());
566+ }
567+ }
568+
569+ resolver->Resolve (context, module ->GetModuleNamespace ()).Check ();
570+ DEBUG_WRITE (" ImportModuleDynamicallyCallback: Successfully resolved '%s'" , spec.c_str ());
571+ } catch (NativeScriptException& ex) {
572+ ex.ReThrowToV8 ();
573+ DEBUG_WRITE (" ImportModuleDynamicallyCallback: Native exception for '%s'" , spec.c_str ());
574+ resolver
575+ ->Reject (context, v8::Exception::Error (
576+ ArgConverter::ConvertToV8String (isolate, " Native error during dynamic import" )))
577+ .Check ();
578+ }
579+
580+ return scope.Escape (resolver->GetPromise ());
581+ }
0 commit comments