99#include < vector>
1010#include < algorithm>
1111#include < atomic>
12+ #include < cstring>
1213#include " HMRSupport.h"
1314#include " DevFlags.h"
1415#include " JEnv.h"
@@ -25,6 +26,163 @@ std::string GetApplicationPath();
2526
2627// Logging flag now provided via DevFlags for fast cached access
2728
29+ // Diagnostic helper: emit detailed V8 compile error info for HTTP ESM sources.
30+ static void LogHttpCompileDiagnostics (v8::Isolate* isolate,
31+ v8::Local<v8::Context> context,
32+ const std::string& url,
33+ const std::string& code,
34+ v8::TryCatch& tc) {
35+ if (!IsScriptLoadingLogEnabled ()) {
36+ return ;
37+ }
38+ using namespace v8 ;
39+
40+ const char * classification = " unknown" ;
41+ std::string msgStr;
42+ std::string srcLineStr;
43+ int lineNum = 0 ;
44+ int startCol = 0 ;
45+ int endCol = 0 ;
46+
47+ Local<Message> message = tc.Message ();
48+ if (!message.IsEmpty ()) {
49+ String::Utf8Value m8 (isolate, message->Get ());
50+ if (*m8) msgStr = *m8;
51+ lineNum = message->GetLineNumber (context).FromMaybe (0 );
52+ startCol = message->GetStartColumn ();
53+ endCol = message->GetEndColumn ();
54+ MaybeLocal<String> maybeLine = message->GetSourceLine (context);
55+ if (!maybeLine.IsEmpty ()) {
56+ String::Utf8Value l8 (isolate, maybeLine.ToLocalChecked ());
57+ if (*l8) srcLineStr = *l8;
58+ }
59+ // Heuristics similar to iOS for quick triage
60+ if (msgStr.find (" Unexpected identifier" ) != std::string::npos ||
61+ msgStr.find (" Unexpected token" ) != std::string::npos) {
62+ if (msgStr.find (" export" ) != std::string::npos &&
63+ code.find (" export default" ) == std::string::npos &&
64+ code.find (" __sfc__" ) != std::string::npos) {
65+ classification = " missing-export-default" ;
66+ } else {
67+ classification = " syntax" ;
68+ }
69+ } else if (msgStr.find (" Cannot use import statement" ) != std::string::npos) {
70+ classification = " wrap-error" ;
71+ }
72+ }
73+ if (std::string (classification) == " unknown" ) {
74+ if (code.find (" export default" ) == std::string::npos && code.find (" __sfc__" ) != std::string::npos) classification = " missing-export-default" ;
75+ else if (code.find (" __sfc__" ) != std::string::npos && code.find (" export {" ) == std::string::npos && code.find (" export " ) == std::string::npos) classification = " no-exports" ;
76+ else if (code.find (" import " ) == std::string::npos && code.find (" export " ) == std::string::npos) classification = " not-module" ;
77+ else if (code.find (" _openBlock" ) != std::string::npos && code.find (" openBlock" ) == std::string::npos) classification = " underscore-helper-unmapped" ;
78+ }
79+
80+ // FNV-1a 64-bit hash of source for correlation
81+ unsigned long long h = 1469598103934665603ull ; // offset basis
82+ for (unsigned char c : code) { h ^= c; h *= 1099511628211ull ; }
83+
84+ // Trim the snippet for readability
85+ std::string snippet = code.substr (0 , 600 );
86+ for (char & ch : snippet) { if (ch == ' \n ' || ch == ' \r ' ) ch = ' ' ; }
87+ if (srcLineStr.size () > 240 ) srcLineStr = srcLineStr.substr (0 , 240 );
88+
89+ DEBUG_WRITE (" [http-esm][compile][v8-error][%s] %s line=%d col=%d..%d hash=%llx bytes=%lu msg=%s srcLine=%s snippet=%s" ,
90+ classification,
91+ url.c_str (),
92+ lineNum,
93+ startCol,
94+ endCol,
95+ (unsigned long long )h,
96+ (unsigned long )code.size (),
97+ msgStr.c_str (),
98+ srcLineStr.c_str (),
99+ snippet.c_str ());
100+ }
101+
102+ // Helper: resolve relative or root-absolute spec against an HTTP(S) referrer URL.
103+ // Returns empty string if resolution is not possible.
104+ static std::string ResolveHttpRelative (const std::string& referrerUrl, const std::string& spec) {
105+ if (referrerUrl.empty ()) {
106+ return std::string ();
107+ }
108+ auto startsWith = [](const std::string& s, const char * pre ) -> bool {
109+ size_t n = strlen (pre );
110+ return s.size () >= n && s.compare (0 , n, pre ) == 0 ;
111+ };
112+ if (!(startsWith (referrerUrl, " http://" ) || startsWith (referrerUrl, " https://" ))) {
113+ return std::string ();
114+ }
115+ // Normalize referrer: drop fragment and query
116+ std::string base = referrerUrl;
117+ size_t hashPos = base.find (' #' );
118+ if (hashPos != std::string::npos) base = base.substr (0 , hashPos);
119+ size_t qPos = base.find (' ?' );
120+ if (qPos != std::string::npos) base = base.substr (0 , qPos);
121+
122+ // Extract origin and path
123+ size_t schemePos = base.find (" ://" );
124+ if (schemePos == std::string::npos) {
125+ return std::string ();
126+ }
127+ size_t pathStart = base.find (' /' , schemePos + 3 );
128+ std::string origin = (pathStart == std::string::npos) ? base : base.substr (0 , pathStart);
129+ std::string path = (pathStart == std::string::npos) ? std::string (" /" ) : base.substr (pathStart);
130+
131+ // Separate query/fragment from spec
132+ std::string specPath = spec;
133+ std::string specSuffix;
134+ size_t specQ = specPath.find (' ?' );
135+ size_t specH = specPath.find (' #' );
136+ size_t cut = std::string::npos;
137+ if (specQ != std::string::npos && specH != std::string::npos) {
138+ cut = std::min (specQ, specH);
139+ } else if (specQ != std::string::npos) {
140+ cut = specQ;
141+ } else if (specH != std::string::npos) {
142+ cut = specH;
143+ }
144+ if (cut != std::string::npos) {
145+ specSuffix = specPath.substr (cut);
146+ specPath = specPath.substr (0 , cut);
147+ }
148+
149+ // Build new path
150+ std::string newPath;
151+ if (!specPath.empty () && specPath[0 ] == ' /' ) {
152+ // Root-absolute relative to origin
153+ newPath = specPath;
154+ } else {
155+ // Relative to directory of referrer path
156+ size_t lastSlash = path.find_last_of (' /' );
157+ std::string baseDir = (lastSlash == std::string::npos) ? std::string (" /" ) : path.substr (0 , lastSlash + 1 );
158+ newPath = baseDir + specPath;
159+ }
160+
161+ // Normalize "." and ".." segments
162+ std::vector<std::string> stack;
163+ bool absolute = !newPath.empty () && newPath[0 ] == ' /' ;
164+ size_t i = 0 ;
165+ while (i <= newPath.size ()) {
166+ size_t j = newPath.find (' /' , i);
167+ std::string seg = (j == std::string::npos) ? newPath.substr (i) : newPath.substr (i, j - i);
168+ if (seg.empty () || seg == " ." ) {
169+ // skip
170+ } else if (seg == " .." ) {
171+ if (!stack.empty ()) stack.pop_back ();
172+ } else {
173+ stack.push_back (seg);
174+ }
175+ if (j == std::string::npos) break ;
176+ i = j + 1 ;
177+ }
178+ std::string normPath = absolute ? " /" : std::string ();
179+ for (size_t k = 0 ; k < stack.size (); k++) {
180+ if (k > 0 ) normPath += " /" ;
181+ normPath += stack[k];
182+ }
183+ return origin + normPath + specSuffix;
184+ }
185+
28186// Import meta callback to support import.meta.url and import.meta.dirname
29187void InitializeImportMetaObject (Local<Context> context, Local<Module> module , Local<Object> meta) {
30188 Isolate* isolate = context->GetIsolate ();
@@ -159,60 +317,135 @@ v8::MaybeLocal<v8::Module> ResolveModuleCallback(v8::Local<v8::Context> context,
159317 DEBUG_WRITE (" ResolveModuleCallback: Resolving '%s'" , spec.c_str ());
160318 }
161319
320+ // Normalize malformed http:/ and https:/ prefixes
321+ if (spec.rfind (" http:/" , 0 ) == 0 && spec.rfind (" http://" , 0 ) != 0 ) {
322+ spec.insert (5 , " /" );
323+ } else if (spec.rfind (" https:/" , 0 ) == 0 && spec.rfind (" https://" , 0 ) != 0 ) {
324+ spec.insert (6 , " /" );
325+ }
326+
327+ // Attempt to resolve relative or root-absolute specifiers against an HTTP referrer URL
328+ std::string referrerPath;
329+ for (auto & kv : g_moduleRegistry) {
330+ v8::Local<v8::Module> registered = kv.second .Get (isolate);
331+ if (!registered.IsEmpty () && registered == referrer) {
332+ referrerPath = kv.first ;
333+ break ;
334+ }
335+ }
336+ bool specIsRelative = !spec.empty () && spec[0 ] == ' .' ;
337+ bool specIsRootAbs = !spec.empty () && spec[0 ] == ' /' ;
338+ auto startsWithHttp = [](const std::string& s) -> bool {
339+ return s.rfind (" http://" , 0 ) == 0 || s.rfind (" https://" , 0 ) == 0 ;
340+ };
341+ if (!startsWithHttp (spec) && (specIsRelative || specIsRootAbs)) {
342+ if (!referrerPath.empty () && startsWithHttp (referrerPath)) {
343+ std::string resolved = ResolveHttpRelative (referrerPath, spec);
344+ if (!resolved.empty ()) {
345+ if (IsScriptLoadingLogEnabled ()) {
346+ DEBUG_WRITE (" ResolveModuleCallback: HTTP-relative resolved '%s' + '%s' -> '%s'" ,
347+ referrerPath.c_str (), spec.c_str (), resolved.c_str ());
348+ }
349+ spec = resolved;
350+ }
351+ } else if (specIsRootAbs) {
352+ // Fallback: use global __NS_HTTP_ORIGIN__ if present to anchor root-absolute specs
353+ v8::Local<v8::String> key = ArgConverter::ConvertToV8String (isolate, " __NS_HTTP_ORIGIN__" );
354+ v8::Local<v8::Object> global = context->Global ();
355+ v8::MaybeLocal<v8::Value> maybeOriginVal = global->Get (context, key);
356+ v8::Local<v8::Value> originVal;
357+ if (!maybeOriginVal.IsEmpty () && maybeOriginVal.ToLocal (&originVal) && originVal->IsString ()) {
358+ v8::String::Utf8Value o8 (isolate, originVal);
359+ std::string origin = *o8 ? *o8 : " " ;
360+ if (!origin.empty () && (origin.rfind (" http://" , 0 ) == 0 || origin.rfind (" https://" , 0 ) == 0 )) {
361+ std::string refBase = origin;
362+ if (refBase.back () != ' /' ) refBase += ' /' ;
363+ std::string resolved = ResolveHttpRelative (refBase, spec);
364+ if (!resolved.empty ()) {
365+ if (IsScriptLoadingLogEnabled ()) {
366+ DEBUG_WRITE (" [http-esm][http-origin][fallback] origin=%s spec=%s -> %s" , refBase.c_str (), spec.c_str (), resolved.c_str ());
367+ }
368+ spec = resolved;
369+ }
370+ }
371+ }
372+ }
373+ }
374+
162375 // HTTP(S) ESM support: resolve, fetch and compile from dev server
163376 if (spec.rfind (" http://" , 0 ) == 0 || spec.rfind (" https://" , 0 ) == 0 ) {
164377 std::string canonical = tns::CanonicalizeHttpUrlKey (spec);
378+ if (IsScriptLoadingLogEnabled ()) {
379+ DEBUG_WRITE (" [http-esm][resolve] spec=%s canonical=%s" , spec.c_str (), canonical.c_str ());
380+ }
165381 auto it = g_moduleRegistry.find (canonical);
166382 if (it != g_moduleRegistry.end ()) {
167383 if (IsScriptLoadingLogEnabled ()) {
168- DEBUG_WRITE (" ResolveModuleCallback: Using cached HTTP module '%s' " , canonical.c_str ());
384+ DEBUG_WRITE (" [http-esm][cache] hit %s " , canonical.c_str ());
169385 }
170386 return v8::MaybeLocal<v8::Module>(it->second .Get (isolate));
171387 }
172388
173389 std::string body, ct;
174390 int status = 0 ;
175391 if (!tns::HttpFetchText (spec, body, ct, status)) {
392+ if (IsScriptLoadingLogEnabled ()) {
393+ DEBUG_WRITE (" [http-esm][fetch][fail] url=%s status=%d" , spec.c_str (), status);
394+ }
176395 std::string msg = std::string (" Failed to fetch " ) + spec + " , status=" + std::to_string (status);
177396 isolate->ThrowException (v8::Exception::Error (ArgConverter::ConvertToV8String (isolate, msg)));
178397 return v8::MaybeLocal<v8::Module>();
179398 }
399+ if (IsScriptLoadingLogEnabled ()) {
400+ DEBUG_WRITE (" [http-esm][fetch][ok] url=%s status=%d bytes=%lu ct=%s" , spec.c_str (), status, (unsigned long )body.size (), ct.c_str ());
401+ }
180402
181403 v8::Local<v8::String> sourceText = ArgConverter::ConvertToV8String (isolate, body);
182404 v8::Local<v8::String> urlString = ArgConverter::ConvertToV8String (isolate, canonical);
183405 v8::ScriptOrigin origin (isolate, urlString, 0 , 0 , false , -1 , v8::Local<v8::Value>(), false , false , true );
184406 v8::ScriptCompiler::Source src (sourceText, origin);
185407 v8::Local<v8::Module> mod;
186- if (!v8::ScriptCompiler::CompileModule (isolate, &src).ToLocal (&mod)) {
187- isolate->ThrowException (v8::Exception::Error (ArgConverter::ConvertToV8String (isolate, " HTTP module compile failed" )));
188- return v8::MaybeLocal<v8::Module>();
189- }
190- // Register before instantiation to allow cyclic imports to resolve to same instance
191- g_moduleRegistry[canonical].Reset (isolate, mod);
192- if (mod->GetStatus () == v8::Module::kUninstantiated ) {
193- if (!mod->InstantiateModule (context, &ResolveModuleCallback).FromMaybe (false )) {
194- g_moduleRegistry.erase (canonical);
408+ {
409+ v8::TryCatch tc (isolate);
410+ if (!v8::ScriptCompiler::CompileModule (isolate, &src).ToLocal (&mod)) {
411+ LogHttpCompileDiagnostics (isolate, context, canonical, body, tc);
412+ isolate->ThrowException (v8::Exception::Error (ArgConverter::ConvertToV8String (isolate, " HTTP module compile failed" )));
195413 return v8::MaybeLocal<v8::Module>();
196414 }
197415 }
416+ if (IsScriptLoadingLogEnabled ()) {
417+ DEBUG_WRITE (" [http-esm][compile][ok] %s bytes=%lu" , canonical.c_str (), (unsigned long )body.size ());
418+ }
419+ // Register before instantiation to allow cyclic imports to resolve to same instance
420+ g_moduleRegistry[canonical].Reset (isolate, mod);
421+ // Do not evaluate here; allow V8 to handle instantiation/evaluation in importer context.
422+ // Instantiate proactively if desired (safe), but not required.
423+ // if (mod->GetStatus() == v8::Module::kUninstantiated) {
424+ // if (!mod->InstantiateModule(context, &ResolveModuleCallback).FromMaybe(false)) {
425+ // g_moduleRegistry.erase(canonical);
426+ // return v8::MaybeLocal<v8::Module>();
427+ // }
428+ // }
198429 // Let V8 evaluate during importer evaluation. Returning compiled module is fine.
199430 return v8::MaybeLocal<v8::Module>(mod);
200431 }
201432
202- // 2) Find which filepath the referrer was compiled under
203- std::string referrerPath;
204- for (auto & kv : g_moduleRegistry) {
205- v8::Local<v8::Module> registered = kv.second .Get (isolate);
206- if (registered == referrer) {
207- referrerPath = kv.first ;
208- break ;
433+ // 2) Find which filepath the referrer was compiled under (local filesystem case)
434+ // referrerPath may already be set above; leave as-is if found.
435+ if (referrerPath.empty ()) {
436+ for (auto & kv : g_moduleRegistry) {
437+ v8::Local<v8::Module> registered = kv.second .Get (isolate);
438+ if (registered == referrer) {
439+ referrerPath = kv.first ;
440+ break ;
441+ }
209442 }
210443 }
211444
212445 // If we couldn't identify the referrer and the specifier is relative,
213446 // assume the base directory is the application root
214- bool specIsRelative = !spec.empty () && spec[0 ] == ' .' ;
215- if (referrerPath.empty () && specIsRelative ) {
447+ bool specIsRelativeFs = !spec.empty () && spec[0 ] == ' .' ;
448+ if (referrerPath.empty () && specIsRelativeFs ) {
216449 referrerPath = GetApplicationPath () + " /index.mjs" ; // Default referrer
217450 }
218451
@@ -610,26 +843,68 @@ v8::MaybeLocal<v8::Promise> ImportModuleDynamicallyCallback(
610843 return v8::MaybeLocal<v8::Promise>();
611844 }
612845
846+ // Resolve relative or root-absolute dynamic imports against the referrer's URL when provided
847+ auto isHttpLike = [](const std::string& s) -> bool {
848+ return s.rfind (" http://" , 0 ) == 0 || s.rfind (" https://" , 0 ) == 0 ;
849+ };
850+ bool specIsRelative = !spec.empty () && spec[0 ] == ' .' ;
851+ bool specIsRootAbs = !spec.empty () && spec[0 ] == ' /' ;
852+ std::string referrerUrl;
853+ if (!resource_name.IsEmpty () && resource_name->IsString ()) {
854+ v8::String::Utf8Value r8 (isolate, resource_name);
855+ referrerUrl = *r8 ? *r8 : " " ;
856+ }
857+ if ((specIsRelative || specIsRootAbs) && isHttpLike (referrerUrl)) {
858+ std::string resolved = ResolveHttpRelative (referrerUrl, spec);
859+ if (!resolved.empty ()) {
860+ if (IsScriptLoadingLogEnabled ()) {
861+ DEBUG_WRITE (" [http-esm][dyn][http-rel] base=%s spec=%s -> %s" , referrerUrl.c_str (), spec.c_str (), resolved.c_str ());
862+ }
863+ spec = resolved;
864+ } else if (IsScriptLoadingLogEnabled ()) {
865+ DEBUG_WRITE (" [http-esm][dyn][http-rel][skip] base=%s spec=%s" , referrerUrl.c_str (), spec.c_str ());
866+ }
867+ }
868+
613869 // Handle HTTP(S) dynamic import directly
614- if (!spec.empty () && (spec. rfind ( " http:// " , 0 ) == 0 || spec. rfind ( " https:// " , 0 ) == 0 )) {
870+ if (!spec.empty () && isHttpLike (spec)) {
615871 std::string canonical = tns::CanonicalizeHttpUrlKey (spec);
872+ if (IsScriptLoadingLogEnabled ()) {
873+ DEBUG_WRITE (" [http-esm][dyn][resolve] spec=%s canonical=%s" , spec.c_str (), canonical.c_str ());
874+ }
616875 v8::Local<v8::Module> mod;
617876 auto it = g_moduleRegistry.find (canonical);
618877 if (it != g_moduleRegistry.end ()) {
619878 mod = it->second .Get (isolate);
879+ if (IsScriptLoadingLogEnabled ()) {
880+ DEBUG_WRITE (" [http-esm][dyn][cache] hit %s" , canonical.c_str ());
881+ }
620882 } else {
621883 std::string body, ct; int status = 0 ;
622884 if (!tns::HttpFetchText (spec, body, ct, status)) {
885+ if (IsScriptLoadingLogEnabled ()) {
886+ DEBUG_WRITE (" [http-esm][dyn][fetch][fail] url=%s status=%d" , spec.c_str (), status);
887+ }
623888 resolver->Reject (context, v8::Exception::Error (ArgConverter::ConvertToV8String (isolate, std::string (" Failed to fetch " )+spec))).Check ();
624889 return scope.Escape (resolver->GetPromise ());
625890 }
891+ if (IsScriptLoadingLogEnabled ()) {
892+ DEBUG_WRITE (" [http-esm][dyn][fetch][ok] url=%s status=%d bytes=%lu ct=%s" , spec.c_str (), status, (unsigned long )body.size (), ct.c_str ());
893+ }
626894 v8::Local<v8::String> sourceText = ArgConverter::ConvertToV8String (isolate, body);
627895 v8::Local<v8::String> urlString = ArgConverter::ConvertToV8String (isolate, canonical);
628896 v8::ScriptOrigin origin (isolate, urlString, 0 , 0 , false , -1 , v8::Local<v8::Value>(), false , false , true );
629897 v8::ScriptCompiler::Source src (sourceText, origin);
630- if (!v8::ScriptCompiler::CompileModule (isolate, &src).ToLocal (&mod)) {
631- resolver->Reject (context, v8::Exception::Error (ArgConverter::ConvertToV8String (isolate, " HTTP module compile failed" ))).Check ();
632- return scope.Escape (resolver->GetPromise ());
898+ {
899+ v8::TryCatch tc (isolate);
900+ if (!v8::ScriptCompiler::CompileModule (isolate, &src).ToLocal (&mod)) {
901+ LogHttpCompileDiagnostics (isolate, context, canonical, body, tc);
902+ resolver->Reject (context, v8::Exception::Error (ArgConverter::ConvertToV8String (isolate, " HTTP module compile failed" ))).Check ();
903+ return scope.Escape (resolver->GetPromise ());
904+ }
905+ }
906+ if (IsScriptLoadingLogEnabled ()) {
907+ DEBUG_WRITE (" [http-esm][dyn][compile][ok] %s bytes=%lu" , canonical.c_str (), (unsigned long )body.size ());
633908 }
634909 g_moduleRegistry[canonical].Reset (isolate, mod);
635910 }
0 commit comments