feat: ESM resolver hardening, http loader, dev-mode globals#1965
feat: ESM resolver hardening, http loader, dev-mode globals#1965NathanWalker wants to merge 4 commits into
Conversation
📝 WalkthroughWalkthroughThis PR adds a native ChangesHMR dev-loader, ESM resolver, and dex fallback
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant JS as JS (__NS_DEV__ client)
participant ModuleInternal
participant HMRSupport
participant DevFlags
participant JavaHttp as Java HttpURLConnection
JS->>ModuleInternal: import("https://...")
ModuleInternal->>HMRSupport: LoadHttpModuleForUrl / HttpFetchText
HMRSupport->>DevFlags: IsRemoteUrlAllowed(url)
DevFlags-->>HMRSupport: allowed/denied
HMRSupport->>JavaHttp: PerformHttpFetchOnceSync
JavaHttp-->>HMRSupport: response / JNI exception
HMRSupport-->>ModuleInternal: module text or error reason
ModuleInternal->>ModuleInternal: compile, register, evaluate
ModuleInternal-->>JS: resolved module / rejection with reason
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
fb386eb to
708fecd
Compare
Adds the Hot Module Replacement runtime layer plus the supporting ESM
resolver hardening and dev-session globals that make hot reload viable on
Android.
* `import.meta.hot`: `data`, `accept`, `dispose`, `prune`, `decline`,
`invalidate`, `on`/`off`/`send` event surface.
* Dev-session globals (`__nsStartDevSession`, `__nsReloadDevApp`,
`__nsInvalidateModules`, `__nsRunHmrDispose`, `__nsRunHmrPrune`,
`__nsHasDeclinedModule`, `__nsKickstartHmrPrefetch`,
`__nsGetLoadedModuleUrls`, `__nsApplyStyleUpdate`,
`__nsConfigureDevRuntime`/`__nsConfigureRuntime`,
`__nsTerminateAllWorkers`).
* Speculative HTTP module prefetch (opt-in) with canonical-key
normalization so `__ns_hmr__/v<N>` and `__ns_boot__/b<N>` tag prefixes
share `hot.data` identity across reload cycles.
* ESM resolver hardening in `ModuleInternalCallbacks.cpp` to:
- Preserve synthetic-namespace identity (`ns-vendor://`, `optional:`,
`node:`, `blob:`) — these are NOT filesystem paths.
- Handle HTTP/HTTPS module URLs end-to-end (resolution, fetch,
canonical-key collapse, dynamic import).
- Compile `.json` imports into synthetic ES modules.
* Android runtime-dex support for `.extend()` classes created during HMR
that the static binding generator can't see at build time: runtime DEX
generation with `$`/`_` inner-class normalization (`DexFactory`),
dev/HMR class-resolution fallback (`ClassResolver`), and dev-flag /
`logScriptLoading` plumbing (`AppConfig`, `DevFlags`).
…ract The runtime explicitly does not implement HMR policy. import.meta.hot, the hot-data/accept/dispose/prune registries, and dev-session state move to the JS HMR clients (eg, @nativescript/vite); native keeps only the sync HTTP module fetch, prewarm cache + list-mode kickstart, eviction plumbing, and the dev-boot-complete signal. Dev helpers are consolidated under __NS_DEV__. Also fixes dynamic import() error propagation: with top-level-await enabled, Module::Evaluate() returns a promise instead of an empty MaybeLocal on throw, so the previous empty-check never fired and import() resolved a half-evaluated namespace, silently swallowing module evaluation errors. The callback now checks Module::GetStatus() for kErrored (HTTP, blob, and generic paths) and rejects with the module's real exception, matching iOS. Removes the httpModulePrefetch app-config flag and speculative prefetching; list-mode kickstart remains.
08e7b8e to
696d4fc
Compare
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (1)
test-app/app/src/main/assets/app/tests/testNodeBuiltinsAndOptionalModules.mjs (1)
20-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueLoose OR assertion weakens the test.
expect(p === "/foo/bar.txt" || p === "foo/bar.txt").toBe(true)accepts two different behaviors, which means a regression that flips the leading-slash handling would go undetected either way. If the exact expected value on Android is known, pin it directly instead of accepting both.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test-app/app/src/main/assets/app/tests/testNodeBuiltinsAndOptionalModules.mjs` around lines 20 - 21, The assertion in testNodeBuiltinsAndOptionalModules.mjs is too permissive because it accepts both leading-slash and no-leading-slash results from mod.fileURLToPath. Update the test around the fileURLToPath check to assert the exact expected Android value directly, using the same mod.fileURLToPath symbol and expect call, so the test fails if the path handling changes unexpectedly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 80-92: The runtime cache path description is incomplete: the dex
filename pattern in the README should match DexFactory.getDexFile. Update the
documentation around ClassResolver and DexFactory to state that the generated
dex is written with the thumb suffix (class name plus dex thumb) rather than
just <name>.dex, so the troubleshooting guidance reflects the actual on-disk
path.
In `@test-app/runtime/src/main/cpp/CallbackHandlers.cpp`:
- Around line 1348-1361: Wrap CallbackHandlers::TerminateAllWorkersCallback in
the same V8 exception handling pattern used by the neighboring worker callbacks
so any exception from WorkerWrapper::TerminateChildren or child->Terminate() is
converted to NativeScriptException instead of escaping across V8. Locate the fix
in TerminateAllWorkersCallback and apply the same try/catch boundary and
rethrow/forwarding behavior already used in the adjacent callback handlers that
call into WorkerWrapper.
In `@test-app/runtime/src/main/cpp/HMRSupport.cpp`:
- Around line 1249-1251: configureRuntime() is leaving stale resolver state
behind because SetImportMapEntries() and SetVolatilePatterns() are only called
when the parsed lists are non-empty. Update the logic in configureRuntime() so
an explicit empty import map or volatile pattern list still invokes the
համապատասխան setter and replaces any previous session values. Keep the existing
parsing helpers like ReadImportMapEntries() and ReadVolatilePatterns(), but
remove the empty-check gate before SetImportMapEntries() and
SetVolatilePatterns() so cleared runtime config truly resets resolver state.
- Around line 1044-1063: The detached prefetch worker in
HMRSupport::KickstartHmrPrefetchUrlsSync can still update g_prefetchCache after
the request has timed out or global HMR state has been cleaned up. Add a
cancellation/liveness check tied to the current prefetch context (for example in
the ctxCopy worker path before writing to the cache) so stale workers exit
without mutating shared state. Apply the same guard to the matching detached
fetch path referenced by the related block, and keep the cache write under
g_prefetchMutex only when the context is still valid.
- Around line 664-668: The per-fetch URL entry trace in HMRSupport’s HTTP-ESM
fetch path is still guarded by the script-loading flag instead of the new
httpFetchUrlLog setting. Update the conditional around the DEBUG_WRITE in the
fetch entry flow to use the httpFetchUrlLog-backed check (for example, the
getter or helper associated with httpFetchUrlLog) so enabling that setting alone
turns on the URL trace. Keep the existing fetch entry logging in the same
location, just swap the gate used by the HTTP fetch diagnostics path.
- Around line 580-586: `g_prefetchCache` is using raw URLs instead of the same
canonical identity used by `MarkUrlsForCacheBust()`, so equivalent URLs can miss
cache hits or leave stale prefetched bodies behind. Update the prefetch cache
read/write/eviction paths in `HMRSupport.cpp` to normalize URLs before using
them as keys, and make the affected prewarm and invalidation flows use the same
canonicalized key consistently. Use the existing `MarkUrlsForCacheBust()` logic
as the reference for canonicalization and apply it wherever `g_prefetchCache` is
accessed.
In `@test-app/runtime/src/main/cpp/MetadataNode.cpp`:
- Around line 1798-1816: The module-name normalization in
MetadataNode::GetModulePath should strip any query string or fragment before
checking for .mjs/.js suffixes, since cache-busted URLs can bypass the current
extension trimming. Update the logic around the normalized/fullPathToFile
handling to remove everything after ? or # first, then keep sanitizing all
non-identifier characters (including ?, =, &, #) before the Util::SplitString
step.
In `@test-app/runtime/src/main/cpp/ModuleInternal.cpp`:
- Around line 292-321: The promise-drain logic in ModuleInternal.cpp currently
exits successfully when evalResult remains kPending after the maxAttempts loop.
Update the HTTP module evaluation path in the promise handling block to detect
the still-pending state after the loop and throw a timeout/pending-evaluation
NativeScriptException instead of falling through. Keep the existing
rejected-path behavior intact and make the new error message clearly identify
the module path and that evaluation never completed.
In `@test-app/runtime/src/main/cpp/Runtime.cpp`:
- Around line 101-113: The fatal signal handler in Runtime.cpp currently
heap-allocates via abi::__cxa_demangle and frees the result, which makes the
crash path depend on the allocator. Update the backtrace formatting logic around
the symbol lookup to avoid any allocation in this handler: keep info.dli_sname
unchanged for logging, remove the demangling/freeing work from this path, and
move demangling to an offline or non-signal-handling context if needed. Use the
existing backtrace loop and __android_log_print call site as the place to
preserve safe, allocator-free logging.
In `@test-app/runtime/src/main/cpp/URLImpl.cpp`:
- Around line 55-86: The URL.searchParams getter caches a URLSearchParams
instance, but the SetSearch path does not refresh that cached object when
url.search is reassigned, so it can become stale. Update the URLImpl URL/search
handling so the existing _searchParams object is synchronized with the new
search string in SetSearch instead of replacing or leaving it unchanged, and
keep the URLSearchParams methods on the cached instance consistent with the
updated URL.
In `@test-app/runtime/src/main/cpp/Version.h`:
- Around line 1-2: The checked-in fallback for the runtime commit SHA in
Version.h is still the placeholder string, so startup logs can show a bogus
value. Update the Version.h literal or make test-app/runtime/build.gradle
replace the exact symbol used by NATIVE_SCRIPT_RUNTIME_COMMIT_SHA so packaged
release builds include the real git SHA instead of the fallback.
---
Nitpick comments:
In
`@test-app/app/src/main/assets/app/tests/testNodeBuiltinsAndOptionalModules.mjs`:
- Around line 20-21: The assertion in testNodeBuiltinsAndOptionalModules.mjs is
too permissive because it accepts both leading-slash and no-leading-slash
results from mod.fileURLToPath. Update the test around the fileURLToPath check
to assert the exact expected Android value directly, using the same
mod.fileURLToPath symbol and expect call, so the test fails if the path handling
changes unexpectedly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: f6782789-325f-4eaa-9610-9964979b18d6
📒 Files selected for processing (26)
README.mdtest-app/app/src/main/assets/app/mainpage.jstest-app/app/src/main/assets/app/tests/esm/meta-no-hot.mjstest-app/app/src/main/assets/app/tests/testHttpCanonicalKey.mjstest-app/app/src/main/assets/app/tests/testNodeBuiltinsAndOptionalModules.mjstest-app/app/src/main/assets/app/tests/testNsDevBoundary.mjstest-app/runtime/src/main/cpp/CallbackHandlers.cpptest-app/runtime/src/main/cpp/CallbackHandlers.htest-app/runtime/src/main/cpp/DevFlags.cpptest-app/runtime/src/main/cpp/DevFlags.htest-app/runtime/src/main/cpp/HMRSupport.cpptest-app/runtime/src/main/cpp/HMRSupport.htest-app/runtime/src/main/cpp/MetadataNode.cpptest-app/runtime/src/main/cpp/ModuleInternal.cpptest-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpptest-app/runtime/src/main/cpp/ModuleInternalCallbacks.htest-app/runtime/src/main/cpp/Runtime.cpptest-app/runtime/src/main/cpp/URLImpl.cpptest-app/runtime/src/main/cpp/URLImpl.htest-app/runtime/src/main/cpp/Version.htest-app/runtime/src/main/cpp/WorkerWrapper.cpptest-app/runtime/src/main/cpp/WorkerWrapper.htest-app/runtime/src/main/java/com/tns/AppConfig.javatest-app/runtime/src/main/java/com/tns/ClassResolver.javatest-app/runtime/src/main/java/com/tns/DexFactory.javatest-app/runtime/src/main/java/com/tns/Runtime.java
| The runtime path is wired through | ||
| [`com.tns.ClassResolver`](test-app/runtime/src/main/java/com/tns/ClassResolver.java) | ||
| → [`com.tns.DexFactory`](test-app/runtime/src/main/java/com/tns/DexFactory.java): | ||
|
|
||
| 1. `ClassResolver.resolveClass` first tries `classStorageService.retrieveClass(name)`. | ||
| In production this hits the SBG-generated dex and we're done. | ||
| 2. On `LookedUpClassNotFound` (typical for HMR), if a `baseClassName` is | ||
| present, `ClassResolver` falls back to `DexFactory.resolveClass(...)` | ||
| which runs the same `ProxyGenerator`/`Dump` pipeline SBG uses — only | ||
| it does it at runtime, writes the dex into the app's per-thumb cache | ||
| under `<dexDir>/<name>.dex`, wraps it in a `.jar`, and loads it via | ||
| `DexClassLoader` (or `BaseDexClassLoader` injection when the | ||
| `injectIntoParentClassLoader` flag is on). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Dex cache file path in doc omits thumb suffix.
Doc says the dex is written to <dexDir>/<name>.dex, but DexFactory.getDexFile actually appends the thumb: dexDir + "/" + classToProxyFile + "-" + dexThumb + ".dex". Since this section doubles as a troubleshooting reference (see "What to look at when this breaks"), the actual on-disk filename pattern is worth stating precisely.
✏️ Suggested wording fix
- which runs the same `ProxyGenerator`/`Dump` pipeline SBG uses — only
- it does it at runtime, writes the dex into the app's per-thumb cache
- under `<dexDir>/<name>.dex`, wraps it in a `.jar`, and loads it via
+ which runs the same `ProxyGenerator`/`Dump` pipeline SBG uses — only
+ it does it at runtime, writes the dex into the app's per-thumb cache
+ under `<dexDir>/<name>-<dexThumb>.dex`, wraps it in a `.jar`, and loads it via
`DexClassLoader` (or `BaseDexClassLoader` injection when the📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| The runtime path is wired through | |
| [`com.tns.ClassResolver`](test-app/runtime/src/main/java/com/tns/ClassResolver.java) | |
| → [`com.tns.DexFactory`](test-app/runtime/src/main/java/com/tns/DexFactory.java): | |
| 1. `ClassResolver.resolveClass` first tries `classStorageService.retrieveClass(name)`. | |
| In production this hits the SBG-generated dex and we're done. | |
| 2. On `LookedUpClassNotFound` (typical for HMR), if a `baseClassName` is | |
| present, `ClassResolver` falls back to `DexFactory.resolveClass(...)` | |
| which runs the same `ProxyGenerator`/`Dump` pipeline SBG uses — only | |
| it does it at runtime, writes the dex into the app's per-thumb cache | |
| under `<dexDir>/<name>.dex`, wraps it in a `.jar`, and loads it via | |
| `DexClassLoader` (or `BaseDexClassLoader` injection when the | |
| `injectIntoParentClassLoader` flag is on). | |
| The runtime path is wired through | |
| [`com.tns.ClassResolver`](test-app/runtime/src/main/java/com/tns/ClassResolver.java) | |
| → [`com.tns.DexFactory`](test-app/runtime/src/main/java/com/tns/DexFactory.java): | |
| 1. `ClassResolver.resolveClass` first tries `classStorageService.retrieveClass(name)`. | |
| In production this hits the SBG-generated dex and we're done. | |
| 2. On `LookedUpClassNotFound` (typical for HMR), if a `baseClassName` is | |
| present, `ClassResolver` falls back to `DexFactory.resolveClass(...)` | |
| which runs the same `ProxyGenerator`/`Dump` pipeline SBG uses — only | |
| it does it at runtime, writes the dex into the app's per-thumb cache | |
| under `<dexDir>/<name>-<dexThumb>.dex`, wraps it in a `.jar`, and loads it via | |
| `DexClassLoader` (or `BaseDexClassLoader` injection when the | |
| `injectIntoParentClassLoader` flag is on). |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 80 - 92, The runtime cache path description is
incomplete: the dex filename pattern in the README should match
DexFactory.getDexFile. Update the documentation around ClassResolver and
DexFactory to state that the generated dex is written with the thumb suffix
(class name plus dex thumb) rather than just <name>.dex, so the troubleshooting
guidance reflects the actual on-disk path.
| void | ||
| CallbackHandlers::TerminateAllWorkersCallback(const v8::FunctionCallbackInfo<v8::Value> &args) { | ||
| // `__NS_DEV__.terminateAllWorkers()` — main-isolate-only dev helper. | ||
| // Tears down every worker parented by this isolate through the WorkerWrapper | ||
| // registry. TerminateChildren snapshots the registry under its lock, | ||
| // terminates and clears each worker, and lets each one cascade into its own | ||
| // nested workers, so a worker self-terminating in parallel can't invalidate | ||
| // the walk. Returns the number of direct (top-level) workers torn down so | ||
| // the HMR client can log it. | ||
| auto isolate = args.GetIsolate(); | ||
| HandleScope scope(isolate); | ||
|
|
||
| int terminated = WorkerWrapper::TerminateChildren(isolate); | ||
| args.GetReturnValue().Set(terminated); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Wrap worker termination in the same V8 callback exception boundary.
TerminateChildren() can reach JNI through child->Terminate(). If that throws, this callback currently lets a C++ exception escape across V8; the neighboring worker callbacks convert it back to NativeScriptException.
Proposed fix
void
CallbackHandlers::TerminateAllWorkersCallback(const v8::FunctionCallbackInfo<v8::Value> &args) {
- // `__NS_DEV__.terminateAllWorkers()` — main-isolate-only dev helper.
- // Tears down every worker parented by this isolate through the WorkerWrapper
- // registry. TerminateChildren snapshots the registry under its lock,
- // terminates and clears each worker, and lets each one cascade into its own
- // nested workers, so a worker self-terminating in parallel can't invalidate
- // the walk. Returns the number of direct (top-level) workers torn down so
- // the HMR client can log it.
auto isolate = args.GetIsolate();
- HandleScope scope(isolate);
+ try {
+ HandleScope scope(isolate);
- int terminated = WorkerWrapper::TerminateChildren(isolate);
- args.GetReturnValue().Set(terminated);
+ int terminated = WorkerWrapper::TerminateChildren(isolate);
+ args.GetReturnValue().Set(terminated);
+ } catch (NativeScriptException &ex) {
+ ex.ReThrowToV8();
+ } catch (std::exception &e) {
+ std::stringstream ss;
+ ss << "Error: c++ exception: " << e.what() << std::endl;
+ NativeScriptException nsEx(ss.str());
+ nsEx.ReThrowToV8();
+ } catch (...) {
+ NativeScriptException nsEx(std::string("Error: c++ exception!"));
+ nsEx.ReThrowToV8();
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| void | |
| CallbackHandlers::TerminateAllWorkersCallback(const v8::FunctionCallbackInfo<v8::Value> &args) { | |
| // `__NS_DEV__.terminateAllWorkers()` — main-isolate-only dev helper. | |
| // Tears down every worker parented by this isolate through the WorkerWrapper | |
| // registry. TerminateChildren snapshots the registry under its lock, | |
| // terminates and clears each worker, and lets each one cascade into its own | |
| // nested workers, so a worker self-terminating in parallel can't invalidate | |
| // the walk. Returns the number of direct (top-level) workers torn down so | |
| // the HMR client can log it. | |
| auto isolate = args.GetIsolate(); | |
| HandleScope scope(isolate); | |
| int terminated = WorkerWrapper::TerminateChildren(isolate); | |
| args.GetReturnValue().Set(terminated); | |
| void | |
| CallbackHandlers::TerminateAllWorkersCallback(const v8::FunctionCallbackInfo<v8::Value> &args) { | |
| auto isolate = args.GetIsolate(); | |
| try { | |
| HandleScope scope(isolate); | |
| int terminated = WorkerWrapper::TerminateChildren(isolate); | |
| args.GetReturnValue().Set(terminated); | |
| } catch (NativeScriptException &ex) { | |
| ex.ReThrowToV8(); | |
| } catch (std::exception &e) { | |
| std::stringstream ss; | |
| ss << "Error: c++ exception: " << e.what() << std::endl; | |
| NativeScriptException nsEx(ss.str()); | |
| nsEx.ReThrowToV8(); | |
| } catch (...) { | |
| NativeScriptException nsEx(std::string("Error: c++ exception!")); | |
| nsEx.ReThrowToV8(); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test-app/runtime/src/main/cpp/CallbackHandlers.cpp` around lines 1348 - 1361,
Wrap CallbackHandlers::TerminateAllWorkersCallback in the same V8 exception
handling pattern used by the neighboring worker callbacks so any exception from
WorkerWrapper::TerminateChildren or child->Terminate() is converted to
NativeScriptException instead of escaping across V8. Locate the fix in
TerminateAllWorkersCallback and apply the same try/catch boundary and
rethrow/forwarding behavior already used in the adjacent callback handlers that
call into WorkerWrapper.
| { | ||
| std::string cached; | ||
| bool cacheHit = false; | ||
| { | ||
| std::lock_guard<std::mutex> lock(g_prefetchMutex); | ||
| auto it = g_prefetchCache.find(url); | ||
| if (it != g_prefetchCache.end()) { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use canonical keys for the prefetch cache.
MarkUrlsForCacheBust() canonicalizes URL identity, but g_prefetchCache reads, writes, and evicts raw URLs. Canonical-equivalent URLs like reordered dev query params or dropped t/v/import markers can miss the prewarm hit or leave stale prefetched bodies behind after invalidation.
Proposed fix
void EvictHttpModulePrefetchCacheUrls(const std::vector<std::string>& urls) {
if (urls.empty()) return;
std::lock_guard<std::mutex> lock(g_prefetchMutex);
size_t hits = 0;
for (const std::string& u : urls) {
- auto it = g_prefetchCache.find(u);
+ auto it = g_prefetchCache.find(CanonicalizeHttpUrlKey(u));
if (it != g_prefetchCache.end()) { g_prefetchCache.erase(it); ++hits; }
} {
std::lock_guard<std::mutex> lock(g_prefetchMutex);
- auto it = g_prefetchCache.find(url);
+ auto it = g_prefetchCache.find(CanonicalizeHttpUrlKey(url));
if (it != g_prefetchCache.end()) { {
std::lock_guard<std::mutex> lock(g_prefetchMutex);
- g_prefetchCache[urlCopy] = std::move(body);
+ g_prefetchCache[CanonicalizeHttpUrlKey(urlCopy)] = std::move(body);
}Also applies to: 1048-1057, 419-425
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test-app/runtime/src/main/cpp/HMRSupport.cpp` around lines 580 - 586,
`g_prefetchCache` is using raw URLs instead of the same canonical identity used
by `MarkUrlsForCacheBust()`, so equivalent URLs can miss cache hits or leave
stale prefetched bodies behind. Update the prefetch cache read/write/eviction
paths in `HMRSupport.cpp` to normalize URLs before using them as keys, and make
the affected prewarm and invalidation flows use the same canonicalized key
consistently. Use the existing `MarkUrlsForCacheBust()` logic as the reference
for canonicalization and apply it wherever `g_prefetchCache` is accessed.
| // Entry trace gated behind `logScriptLoading`. Cold boot fires this dozens of | ||
| // times per session, so it stays off by default; enable `logScriptLoading` | ||
| // when triaging the HTTP-ESM path. | ||
| if (IsScriptLoadingLogEnabled()) { | ||
| DEBUG_WRITE("[http-esm][fetch][enter] url=%s", url.c_str()); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Gate per-fetch URL traces with httpFetchUrlLog.
The new httpFetchUrlLog setting is parsed and exposed, but the high-volume fetch URL entry trace still checks IsScriptLoadingLogEnabled(). Enabling "httpFetchUrlLog": true alone will not produce the advertised per-fetch diagnostics.
Proposed fix
- if (IsScriptLoadingLogEnabled()) {
+ if (IsHttpFetchUrlLogEnabled()) {
DEBUG_WRITE("[http-esm][fetch][enter] url=%s", url.c_str());
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Entry trace gated behind `logScriptLoading`. Cold boot fires this dozens of | |
| // times per session, so it stays off by default; enable `logScriptLoading` | |
| // when triaging the HTTP-ESM path. | |
| if (IsScriptLoadingLogEnabled()) { | |
| DEBUG_WRITE("[http-esm][fetch][enter] url=%s", url.c_str()); | |
| // Entry trace gated behind `logScriptLoading`. Cold boot fires this dozens of | |
| // times per session, so it stays off by default; enable `logScriptLoading` | |
| // when triaging the HTTP-ESM path. | |
| if (IsHttpFetchUrlLogEnabled()) { | |
| DEBUG_WRITE("[http-esm][fetch][enter] url=%s", url.c_str()); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test-app/runtime/src/main/cpp/HMRSupport.cpp` around lines 664 - 668, The
per-fetch URL entry trace in HMRSupport’s HTTP-ESM fetch path is still guarded
by the script-loading flag instead of the new httpFetchUrlLog setting. Update
the conditional around the DEBUG_WRITE in the fetch entry flow to use the
httpFetchUrlLog-backed check (for example, the getter or helper associated with
httpFetchUrlLog) so enabling that setting alone turns on the URL trace. Keep the
existing fetch entry logging in the same location, just swap the gate used by
the HTTP fetch diagnostics path.
| std::thread([ctxCopy, urlCopy]() { | ||
| ctxCopy->concurrency->Acquire(); | ||
| std::string body, contentType; | ||
| int status = 0; | ||
| bool ok = PerformHttpFetchOnceSync(urlCopy, body, contentType, status); | ||
| if (ok && status >= 200 && status < 300 && !body.empty()) { | ||
| const size_t bodySize = body.size(); | ||
| // Overwrite unconditionally — the fresh body we just fetched is | ||
| // by definition the authoritative copy; any older cache entry is | ||
| // stale by construction (the caller has just told us so). | ||
| { | ||
| std::lock_guard<std::mutex> lock(g_prefetchMutex); | ||
| g_prefetchCache[urlCopy] = std::move(body); | ||
| } | ||
| ctxCopy->fetchedCount.fetch_add(1, std::memory_order_relaxed); | ||
| ctxCopy->bytes.fetch_add(bodySize, std::memory_order_relaxed); | ||
| } | ||
| ctxCopy->concurrency->Release(); | ||
| ctxCopy->LeavePending(); | ||
| }).detach(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Prevent timed-out prefetch workers from writing stale cache entries.
Detached fetch threads can outlive KickstartHmrPrefetchUrlsSync() after timeout and still write into the process-wide prefetch cache. That can repopulate stale bodies after the caller has fallen back, after invalidation, or even after CleanupHMRGlobals() clears state.
Proposed direction
struct KickstartContext {
std::mutex mutex;
std::unordered_set<std::string> visited;
+ std::atomic_bool acceptingWrites{true};
std::atomic<size_t> fetchedCount{0};- if (ok && status >= 200 && status < 300 && !body.empty()) {
+ if (ok && status >= 200 && status < 300 && !body.empty() &&
+ ctxCopy->acceptingWrites.load(std::memory_order_acquire)) {
const size_t bodySize = body.size();
{
std::lock_guard<std::mutex> lock(g_prefetchMutex);
- g_prefetchCache[urlCopy] = std::move(body);
+ if (ctxCopy->acceptingWrites.load(std::memory_order_relaxed)) {
+ g_prefetchCache[CanonicalizeHttpUrlKey(urlCopy)] = std::move(body);
+ }
} while (true) {
drained = ctx->WaitDrainSlice(sliceMs);
if (drained) break;
if (std::chrono::steady_clock::now() >= deadline) break;
if (!IsDevSessionBootComplete()) {
InvokeHttpFetchYield();
}
}
+ if (!drained) {
+ ctx->acceptingWrites.store(false, std::memory_order_release);
+ }Also applies to: 1111-1140
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test-app/runtime/src/main/cpp/HMRSupport.cpp` around lines 1044 - 1063, The
detached prefetch worker in HMRSupport::KickstartHmrPrefetchUrlsSync can still
update g_prefetchCache after the request has timed out or global HMR state has
been cleaned up. Add a cancellation/liveness check tied to the current prefetch
context (for example in the ctxCopy worker path before writing to the cache) so
stale workers exit without mutating shared state. Apply the same guard to the
matching detached fetch path referenced by the related block, and keep the cache
write under g_prefetchMutex only when the context is still valid.
| auto endsWith = [](const string& s, const string& suffix) { | ||
| return s.size() >= suffix.size() && | ||
| s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; | ||
| }; | ||
| if (endsWith(normalized, ".mjs")) { | ||
| normalized.resize(normalized.size() - 4); | ||
| } else if (endsWith(normalized, ".js")) { | ||
| normalized.resize(normalized.size() - 3); | ||
| } | ||
|
|
||
| fullPathToFile = normalized; | ||
|
|
||
| std::replace(fullPathToFile.begin(), fullPathToFile.end(), '/', '_'); | ||
| std::replace(fullPathToFile.begin(), fullPathToFile.end(), '.', '_'); | ||
| std::replace(fullPathToFile.begin(), fullPathToFile.end(), '-', '_'); | ||
| std::replace(fullPathToFile.begin(), fullPathToFile.end(), ' ', '_'); | ||
|
|
||
| std::vector<std::string> pathParts; | ||
|
|
||
| Util::SplitString(fullPathToFile, "_", pathParts); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Strip query/fragment and sanitize all non-identifier characters.
Cache-busted HTTP module URLs like foo.mjs?t=... won’t match the .mjs suffix check and can leave ?, =, &, or # in the generated token.
Proposed fix
+ size_t queryOrFragment = normalized.find_first_of("?#");
+ if (queryOrFragment != string::npos) {
+ normalized.resize(queryOrFragment);
+ }
+
auto endsWith = [](const string& s, const string& suffix) {
return s.size() >= suffix.size() &&
s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
};
@@
- std::replace(fullPathToFile.begin(), fullPathToFile.end(), '/', '_');
- std::replace(fullPathToFile.begin(), fullPathToFile.end(), '.', '_');
- std::replace(fullPathToFile.begin(), fullPathToFile.end(), '-', '_');
- std::replace(fullPathToFile.begin(), fullPathToFile.end(), ' ', '_');
+ for (char& ch : fullPathToFile) {
+ const unsigned char c = static_cast<unsigned char>(ch);
+ const bool isIdentifierChar =
+ (c >= 'A' && c <= 'Z') ||
+ (c >= 'a' && c <= 'z') ||
+ (c >= '0' && c <= '9') ||
+ ch == '_';
+ if (!isIdentifierChar) {
+ ch = '_';
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| auto endsWith = [](const string& s, const string& suffix) { | |
| return s.size() >= suffix.size() && | |
| s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; | |
| }; | |
| if (endsWith(normalized, ".mjs")) { | |
| normalized.resize(normalized.size() - 4); | |
| } else if (endsWith(normalized, ".js")) { | |
| normalized.resize(normalized.size() - 3); | |
| } | |
| fullPathToFile = normalized; | |
| std::replace(fullPathToFile.begin(), fullPathToFile.end(), '/', '_'); | |
| std::replace(fullPathToFile.begin(), fullPathToFile.end(), '.', '_'); | |
| std::replace(fullPathToFile.begin(), fullPathToFile.end(), '-', '_'); | |
| std::replace(fullPathToFile.begin(), fullPathToFile.end(), ' ', '_'); | |
| std::vector<std::string> pathParts; | |
| Util::SplitString(fullPathToFile, "_", pathParts); | |
| size_t queryOrFragment = normalized.find_first_of("?#"); | |
| if (queryOrFragment != string::npos) { | |
| normalized.resize(queryOrFragment); | |
| } | |
| auto endsWith = [](const string& s, const string& suffix) { | |
| return s.size() >= suffix.size() && | |
| s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; | |
| }; | |
| if (endsWith(normalized, ".mjs")) { | |
| normalized.resize(normalized.size() - 4); | |
| } else if (endsWith(normalized, ".js")) { | |
| normalized.resize(normalized.size() - 3); | |
| } | |
| fullPathToFile = normalized; | |
| for (char& ch : fullPathToFile) { | |
| const unsigned char c = static_cast<unsigned char>(ch); | |
| const bool isIdentifierChar = | |
| (c >= 'A' && c <= 'Z') || | |
| (c >= 'a' && c <= 'z') || | |
| (c >= '0' && c <= '9') || | |
| ch == '_'; | |
| if (!isIdentifierChar) { | |
| ch = '_'; | |
| } | |
| } | |
| std::vector<std::string> pathParts; | |
| Util::SplitString(fullPathToFile, "_", pathParts); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test-app/runtime/src/main/cpp/MetadataNode.cpp` around lines 1798 - 1816, The
module-name normalization in MetadataNode::GetModulePath should strip any query
string or fragment before checking for .mjs/.js suffixes, since cache-busted
URLs can bypass the current extension trimming. Update the logic around the
normalized/fullPathToFile handling to remove everything after ? or # first, then
keep sanitizing all non-identifier characters (including ?, =, &, #) before the
Util::SplitString step.
| if (!evalResult.IsEmpty() && evalResult->IsPromise()) { | ||
| Local<Promise> promise = evalResult.As<Promise>(); | ||
| const int maxAttempts = 100; | ||
| int attempts = 0; | ||
| while (attempts < maxAttempts) { | ||
| isolate->PerformMicrotaskCheckpoint(); | ||
| Promise::PromiseState state = promise->State(); | ||
| if (state != Promise::kPending) { | ||
| if (state == Promise::kRejected) { | ||
| Local<Value> reason = promise->Result(); | ||
| std::string reasonStr; | ||
| if (!reason.IsEmpty()) { | ||
| v8::Local<v8::String> reasonV8; | ||
| if (reason->ToString(context).ToLocal(&reasonV8)) { | ||
| reasonStr = ArgConverter::ConvertToString(reasonV8); | ||
| } | ||
| } | ||
| DEBUG_WRITE("[run-module][http-esm][evaluate-rejected] %s reason=%s", | ||
| path.c_str(), | ||
| reasonStr.empty() ? "<unknown>" : reasonStr.c_str()); | ||
| isolate->ThrowException(reason); | ||
| throw NativeScriptException( | ||
| string("HTTP module evaluation promise rejected: ") + path + | ||
| (reasonStr.empty() ? "" : (" — " + reasonStr))); | ||
| } | ||
| break; | ||
| } | ||
| attempts++; | ||
| usleep(100); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the relevant file and surrounding context.
git ls-files 'test-app/runtime/src/main/cpp/ModuleInternal.cpp'
wc -l test-app/runtime/src/main/cpp/ModuleInternal.cpp
sed -n '240,360p' test-app/runtime/src/main/cpp/ModuleInternal.cpp
# Find related HTTP/module evaluation code paths.
rg -n "PerformMicrotaskCheckpoint|Promise::kPending|evaluate-rejected|HTTP module evaluation|Top-level-await|TLA|IsPromise\\(" test-app/runtime/src/main/cpp/ModuleInternal.cpp test-app/runtime/src/main/cpp -A4 -B4Repository: NativeScript/android
Length of output: 25227
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the ES module evaluation path around the analogous promise-drain loop.
sed -n '680,760p' test-app/runtime/src/main/cpp/ModuleInternal.cpp
# Check whether any code path explicitly handles "still pending" after the loop.
rg -n "still pending after microtask drain|Promise::kPending|maxAttempts = 100|PerformMicrotaskCheckpoint\\(\\)" test-app/runtime/src/main/cpp/ModuleInternal.cpp -A3 -B3Repository: NativeScript/android
Length of output: 4752
Fail when HTTP module evaluation is still pending after the drain loop.
If the promise is still kPending after maxAttempts, this returns as success while evaluation is still in flight. Throw a timeout/pending-evaluation error instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test-app/runtime/src/main/cpp/ModuleInternal.cpp` around lines 292 - 321, The
promise-drain logic in ModuleInternal.cpp currently exits successfully when
evalResult remains kPending after the maxAttempts loop. Update the HTTP module
evaluation path in the promise handling block to detect the still-pending state
after the loop and throw a timeout/pending-evaluation NativeScriptException
instead of falling through. Keep the existing rejected-path behavior intact and
make the new error message clearly identify the module path and that evaluation
never completed.
| int status = 0; | ||
| demangled = abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, &status); | ||
| if (status == 0 && demangled) { | ||
| symName = demangled; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| __android_log_print(ANDROID_LOG_FATAL, "TNS.Native", | ||
| " #%02d pc 0x%016lx %s (%s+%lu)", idx, | ||
| static_cast<unsigned long>(relPc), libName, symName, | ||
| static_cast<unsigned long>(symOff)); | ||
| if (demangled) free(demangled); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect allocating operations still present in the fatal signal path.
python - <<'PY'
from pathlib import Path
p = Path('test-app/runtime/src/main/cpp/Runtime.cpp')
for i, line in enumerate(p.read_text().splitlines(), 1):
if 85 <= i <= 176 and any(token in line for token in ['__cxa_demangle', 'free(', 'stringstream', 'malloc', 'new ']):
print(f'{i}: {line}')
PYRepository: NativeScript/android
Length of output: 472
🏁 Script executed:
#!/bin/bash
sed -n '90,180p' test-app/runtime/src/main/cpp/Runtime.cpp | cat -nRepository: NativeScript/android
Length of output: 4653
Avoid heap allocation in the fatal signal handler. abi::__cxa_demangle and the matching free() make this path depend on the allocator; if the crash already involves heap corruption, the backtrace can hang or fail before logging completes. Keep info.dli_sname as-is and demangle offline.
🧰 Tools
🪛 Cppcheck (2.21.0)
[error] 108-108: #error Unsupported bitness
(preprocessorErrorDirective)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test-app/runtime/src/main/cpp/Runtime.cpp` around lines 101 - 113, The fatal
signal handler in Runtime.cpp currently heap-allocates via abi::__cxa_demangle
and frees the result, which makes the crash path depend on the allocator. Update
the backtrace formatting logic around the symbol lookup to avoid any allocation
in this handler: keep info.dli_sname unchanged for logging, remove the
demangling/freeing work from this path, and move demangling to an offline or
non-signal-handling context if needed. Use the existing backtrace loop and
__android_log_print call site as the place to preserve safe, allocator-free
logging.
| Object.defineProperty(URL.prototype, 'searchParams', { | ||
| get() { | ||
| if (this._searchParams == null) { | ||
| this._searchParams = new URLSearchParams(this.search); | ||
| Object.defineProperty(this._searchParams, '_url', { | ||
| enumerable: false, | ||
| writable: false, | ||
| value: this, | ||
| }); | ||
| this._searchParams._append = this._searchParams.append; | ||
| this._searchParams.append = function (name, value) { | ||
| this._append(name, value); | ||
| this._url.search = this.toString(); | ||
| }; | ||
| this._searchParams._delete = this._searchParams.delete; | ||
| this._searchParams.delete = function (name) { | ||
| this._delete(name); | ||
| this._url.search = this.toString(); | ||
| }; | ||
| this._searchParams._set = this._searchParams.set; | ||
| this._searchParams.set = function (name, value) { | ||
| this._set(name, value); | ||
| this._url.search = this.toString(); | ||
| }; | ||
| this._searchParams._sort = this._searchParams.sort; | ||
| this._searchParams.sort = function () { | ||
| this._sort(); | ||
| this._url.search = this.toString(); | ||
| }; | ||
| } | ||
| return this._searchParams; | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify coverage for URL.search/searchParams cache synchronization.
rg -n -C3 'searchParams|URLSearchParams|URL\\.createObjectURL' test-app/runtime/src/main/cpp test-app/app/src/main/assets/app/testsRepository: NativeScript/android
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the URL polyfill and any existing cache-invalidation hooks.
sed -n '1,140p' test-app/runtime/src/main/cpp/URLImpl.cpp
printf '\n--- search-related definitions ---\n'
rg -n -C2 'searchParams|\.search\b|_searchParamsSearch|defineProperty\(URL\.prototype' test-app/runtime/src/main/cpp/URLImpl.cpp
printf '\n--- tests covering URL.search / searchParams interactions ---\n'
rg -n -C3 'searchParams|\.search\s*=|URL\(' test-app/app/src/main/assets/app/tests/testURLImpl.js test-app/app/src/main/assets/app/tests/testURLSearchParamsImpl.jsRepository: NativeScript/android
Length of output: 14841
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the URL.search accessor implementation and any cache-reset logic.
rg -n -C4 'SetSearch|GetSearch|search\b|_searchParams|searchParams' test-app/runtime/src/main/cpp/URLImpl.cpp
printf '\n--- accessor block around search ---\n'
sed -n '140,260p' test-app/runtime/src/main/cpp/URLImpl.cppRepository: NativeScript/android
Length of output: 8160
Keep URL.searchParams in sync when search is reassigned. SetSearch updates the backing URL but never resyncs the cached params object, so url.searchParams can expose stale data after url.search = ...; update the existing params object instead of recreating it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test-app/runtime/src/main/cpp/URLImpl.cpp` around lines 55 - 86, The
URL.searchParams getter caches a URLSearchParams instance, but the SetSearch
path does not refresh that cached object when url.search is reassigned, so it
can become stale. Update the URLImpl URL/search handling so the existing
_searchParams object is synchronized with the new search string in SetSearch
instead of replacing or leaving it unchanged, and keep the URLSearchParams
methods on the cached instance consistent with the updated URL.
| #define NATIVE_SCRIPT_RUNTIME_VERSION "9.1.0-alpha.7" | ||
| #define NATIVE_SCRIPT_RUNTIME_COMMIT_SHA "no commit sha was provided by build.gradle build" No newline at end of file |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether build tooling rewrites NATIVE_SCRIPT_RUNTIME_COMMIT_SHA.
rg -n -C3 'NATIVE_SCRIPT_RUNTIME_COMMIT_SHA|Version\\.h|commit sha' .Repository: NativeScript/android
Length of output: 1470
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the runtime build wiring around Version.h and the commit SHA placeholder.
printf '\n== build.gradle relevant lines ==\n'
sed -n '1,220p' build.gradle | nl -ba | sed -n '1,140p'
printf '\n== files that mention Version.h generation ==\n'
rg -n -C 2 'Version\.h|arVersion|pVersion|commit sha was provided by build\.gradle build|native.*version' build.gradle test-app/runtime -g '!**/dist/**'Repository: NativeScript/android
Length of output: 233
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== build.gradle excerpt ==\n'
awk 'NR>=1 && NR<=220 {printf "%4d %s\n", NR, $0}' build.gradle
printf '\n== Version.h generation refs ==\n'
rg -n -C 3 'Version\.h|arVersion|pVersion|commit sha was provided by build\.gradle build|configure.*Version|generate.*Version' build.gradle test-app/runtime -g '!**/dist/**'Repository: NativeScript/android
Length of output: 14555
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== exact runtime build replacements ==\n'
sed -n '260,305p' test-app/runtime/build.gradle | awk '{printf "%4d %s\n", NR+259, $0}'
printf '\n== all placeholder symbols in repo ==\n'
rg -n 'RUNTIME_COMMIT_SHA_PLACEHOLDER|0\.0\.0\.0|no commit sha was provided by build\.gradle build' test-app/runtime build.gradleRepository: NativeScript/android
Length of output: 2400
Replace the commit SHA fallback before packaging
test-app/runtime/build.gradle only rewrites RUNTIME_COMMIT_SHA_PLACEHOLDER, so the checked-in value in test-app/runtime/src/main/cpp/Version.h stays as "no commit sha was provided by build.gradle build" and gets logged at startup. Update the literal or the replacement token so release builds carry the actual git SHA.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test-app/runtime/src/main/cpp/Version.h` around lines 1 - 2, The checked-in
fallback for the runtime commit SHA in Version.h is still the placeholder
string, so startup logs can show a bogus value. Update the Version.h literal or
make test-app/runtime/build.gradle replace the exact symbol used by
NATIVE_SCRIPT_RUNTIME_COMMIT_SHA so packaged release builds include the real git
SHA instead of the fallback.
Framework-agnostic hot module replacement on Android with native ES modules: the device fetches modules over HTTP from the Vite dev server and applies hot updates without restarting the process.
The runtime's entire dev surface is one namespace object,
globalThis.__NS_DEV__, carrying primitives, each traceable to a V8-embedder or OS constraint:configureRuntime(config)ResolveModuleCallbackinvalidateModules(urls)v8::Modulerecords + prewarm cache + arms a CFNetwork cache-bust noncekickstartPrefetch(urls)getLoadedModuleUrls()setDevBootComplete(bool)Every HMR policy decision, boot orchestration,
import.meta.hot, full reload, CSS apply, the WebSocket protocol is JS in@nativescript/vite. No native dev-session state machine, no reload orchestration, no hot-callback registries exist in the runtime, and on-device tests pin both the present members and the absent globals.Summary by CodeRabbit
New Features
Bug Fixes
Documentation