Skip to content

feat: ESM resolver hardening, http loader, dev-mode globals#1965

Open
NathanWalker wants to merge 4 commits into
mainfrom
feat/hmr-dev-sessions
Open

feat: ESM resolver hardening, http loader, dev-mode globals#1965
NathanWalker wants to merge 4 commits into
mainfrom
feat/hmr-dev-sessions

Conversation

@NathanWalker

@NathanWalker NathanWalker commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

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:

Member Why it must be native
configureRuntime(config) Import map / volatile URL patterns consumed inside V8's synchronous ResolveModuleCallback
invalidateModules(urls) Drops host-owned v8::Module records + prewarm cache + arms a CFNetwork cache-bust nonce
kickstartPrefetch(urls) Parallel HTTP prewarm of a server-computed closure, so V8's serial synchronous import walk doesn't trip the launch watchdog
getLoadedModuleUrls() Registry introspection for the JS full-reload path
setDevBootComplete(bool) Disarms cold-boot-only behaviors (runloop pump, kickstart wait)

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

    • Added new dev/runtime support for safer hot reload, module loading, and blob URL handling.
    • Introduced a configurable HTTP fetch logging option and updated the runtime version metadata.
  • Bug Fixes

    • Improved module resolution and cache handling for HTTP, blob, optional, and built-in imports.
    • Fixed worker cleanup and added support for stopping all workers from the dev API.
    • Added better crash diagnostics with richer native stack traces.
  • Documentation

    • Expanded the README with guidance on static vs. runtime generation paths and related troubleshooting notes.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a native __NS_DEV__ dev-loader/HMR namespace with HTTP module fetching, URL canonicalization, cache-busting, and worker termination; hardens the ESM resolver with a per-thread module registry and synthetic-namespace handling; enforces boundary-aware remote URL allowlisting; adds blob URL polyfills, crash backtrace diagnostics, and runtime dex generation fallback for classes; updates documentation and adds extensive tests.

Changes

HMR dev-loader, ESM resolver, and dex fallback

Layer / File(s) Summary
SBG vs runtime dex generation documentation
README.md
Adds a new documentation section contrasting build-time static binding generation with runtime dex generation, plus edge cases and a troubleshooting checklist.
Runtime dex generation fallback
test-app/runtime/src/main/java/com/tns/ClassResolver.java, .../DexFactory.java
ClassResolver falls back to DexFactory-based dex generation on lookup failure when debuggable; DexFactory normalizes $ to _ in generated proxy class names for cache/classloader lookups.
Debuggable flag and allowlist boundary checks
AppConfig.java, Runtime.java, DevFlags.cpp/.h
Adds HttpFetchUrlLog/isDebuggable config accessors and replaces prefix-only allowlist matching with boundary-aware component matching in both native and Java code.
HTTP fetch, canonicalization, cache-busting core
HMRSupport.cpp, HMRSupport.h
Rewrites URL canonicalization, relative specifier resolution, cache-bust marking, allowlist enforcement, prefetch caching, and JNI exception diagnostics for HTTP fetch retries.
__NS_DEV__ globals and worker termination
HMRSupport.cpp, CallbackHandlers.cpp/.h, WorkerWrapper.cpp/.h
Installs the __NS_DEV__ JS namespace and a terminateAllWorkers callback that returns the count of terminated child workers.
Per-thread ESM registry and resolver hardening
ModuleInternal.cpp, ModuleInternalCallbacks.cpp/.h
Switches to a thread-local module registry, adds vendor/blob/optional synthetic-namespace resolution, HTTP module loading, and removes import.meta.hot attachment.
Blob polyfill, crash diagnostics, cleanup
URLImpl.cpp/.h, Runtime.cpp, MetadataNode.cpp, Version.h
Adds a blob URL/searchParams JS polyfill, libunwind-based crash backtraces, HMR global cleanup on teardown, and defensive script-path normalization.
Dev-loader test coverage
test-app/app/.../tests/*, mainpage.js
Adds tests for the __NS_DEV__ boundary, HTTP canonical key rules, node built-in/optional module resolution, and blob URL module reuse.

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
Loading

Possibly related PRs

  • NativeScript/android#1968: Both PRs touch DexFactory.resolveClass/findClass classloader lookup logic for injected proxy dex.
  • NativeScript/android#1972: Both PRs touch the WorkerWrapper::TerminateChildren worker termination codepath invoked from CallbackHandlers.

Poem

A rabbit hops through dex and thread,
New dev-loader paths ahead,
URLs canonicalized just right,
Workers terminate, backtrace bright,
$ turns to _ where proxies dwell —
Hop hop, all tests pass well! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: ESM resolver hardening, HTTP module loading, and dev-mode globals.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@NathanWalker NathanWalker force-pushed the feat/hmr-dev-sessions branch 2 times, most recently from fb386eb to 708fecd Compare June 16, 2026 17:07
@NathanWalker NathanWalker changed the title feat: HMR dev-sessions, ESM resolver hardening, dev-mode runtime globals feat: ESM resolver hardening, http loader, dev-mode globals Jul 3, 2026
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.
@NathanWalker NathanWalker force-pushed the feat/hmr-dev-sessions branch from 08e7b8e to 696d4fc Compare July 3, 2026 23:54
@NathanWalker NathanWalker marked this pull request as ready for review July 4, 2026 00:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 value

Loose 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

📥 Commits

Reviewing files that changed from the base of the PR and between b0a9964 and d49581c.

📒 Files selected for processing (26)
  • README.md
  • test-app/app/src/main/assets/app/mainpage.js
  • test-app/app/src/main/assets/app/tests/esm/meta-no-hot.mjs
  • test-app/app/src/main/assets/app/tests/testHttpCanonicalKey.mjs
  • test-app/app/src/main/assets/app/tests/testNodeBuiltinsAndOptionalModules.mjs
  • test-app/app/src/main/assets/app/tests/testNsDevBoundary.mjs
  • test-app/runtime/src/main/cpp/CallbackHandlers.cpp
  • test-app/runtime/src/main/cpp/CallbackHandlers.h
  • test-app/runtime/src/main/cpp/DevFlags.cpp
  • test-app/runtime/src/main/cpp/DevFlags.h
  • test-app/runtime/src/main/cpp/HMRSupport.cpp
  • test-app/runtime/src/main/cpp/HMRSupport.h
  • test-app/runtime/src/main/cpp/MetadataNode.cpp
  • test-app/runtime/src/main/cpp/ModuleInternal.cpp
  • test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp
  • test-app/runtime/src/main/cpp/ModuleInternalCallbacks.h
  • test-app/runtime/src/main/cpp/Runtime.cpp
  • test-app/runtime/src/main/cpp/URLImpl.cpp
  • test-app/runtime/src/main/cpp/URLImpl.h
  • test-app/runtime/src/main/cpp/Version.h
  • test-app/runtime/src/main/cpp/WorkerWrapper.cpp
  • test-app/runtime/src/main/cpp/WorkerWrapper.h
  • test-app/runtime/src/main/java/com/tns/AppConfig.java
  • test-app/runtime/src/main/java/com/tns/ClassResolver.java
  • test-app/runtime/src/main/java/com/tns/DexFactory.java
  • test-app/runtime/src/main/java/com/tns/Runtime.java

Comment thread README.md
Comment on lines +80 to +92
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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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.

Comment on lines +1348 to +1361
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment on lines +580 to +586
{
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()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +664 to +668
// 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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
// 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.

Comment on lines +1044 to +1063
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment on lines +1798 to 1816
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +292 to +321
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 -B4

Repository: 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 -B3

Repository: 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.

Comment on lines +101 to +113
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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}')
PY

Repository: NativeScript/android

Length of output: 472


🏁 Script executed:

#!/bin/bash
sed -n '90,180p' test-app/runtime/src/main/cpp/Runtime.cpp | cat -n

Repository: 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.

Comment on lines +55 to +86
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;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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/tests

Repository: 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.js

Repository: 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.cpp

Repository: 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.

Comment on lines +1 to +2
#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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.gradle

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant