Skip to content

Commit 155acd3

Browse files
committed
feat: improved diagnostic logging for debugging
1 parent cb9390e commit 155acd3

File tree

1 file changed

+298
-23
lines changed

1 file changed

+298
-23
lines changed

test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp

Lines changed: 298 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
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
29187
void 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

Comments
 (0)