Skip to content

ESM loader: remaining cross-platform decisions and deferred follow-ups from #1965 #2020

Description

@edusperoni

#1965 landed the full loader-overhaul parity with ios#383 plus several hardening rounds. Everything that could be fixed unilaterally was; this issue tracks what deliberately remains: contract questions that need one answer on both platforms, and Android-side follow-ups that were out of the PR's scope. The iOS-side defects the same reviews surfaced are tracked in NativeScript/ios#443.

Cross-platform decisions (one answer, both runtimes)

  • require(esm) facade evaluates under sync-strict (shared bug): createPumpingRequire() of an already-settled TLA module with a default export throws the async-graph refusal naming ns:require-facade:<path>IsGraphAsync() is transitive and the facade requests the TLA target. Violates the contract twice (pumping lifts the TLA refusal; exports-cascade step 3) and the error recommends the API that just failed. Fix shape to agree on: direct Evaluate() + assert-fulfilled for the facade, or a synthetic-module bypass on the evaluation options.
  • Import-map keys carrying queries: Android strips query/fragment before the map consult (query-bearing keys can never match); iOS matches them for static imports. The HTML import-maps spec matches the specifier as written. Pick a side and align.
  • import.meta.url shape: Android exposes the full device path; iOS base-strips to file:///app/.... Portable code matching file:///app/ breaks on Android today. Couples with file:// specifier candidate handling and iOS's dirname inconsistency.
  • Transport identity: Android sends User-Agent: NativeScript-HTTP-ESM and inherits the process CookieHandler; iOS sends no UA and disables cookies. Cookies especially need a joint call.
  • Transport timeouts: Android 15s connect/15s read vs iOS 5s/10s. The docs now state the per-attempt/per-read semantics honestly, but the values should converge (a stalled edge can outlive the graph deadline).
  • HasPendingAsyncModuleGraphWork is process-wide on both platforms: a worker's graph load holds the main boot backstop open. Make it per-isolate.
  • Instantiate-failure registry semantics: iOS evicts the root, Android leaves-and-self-heals; HTML/Node module-map semantics say keep-and-rethrow. Neither does that — pick one.
  • Import attributes are silently ignored (import x from './d.json' with { type: 'json' } loads as JS on both); Node throws. Document or implement, jointly.
  • Import-map validation error-text shape: parse-detail suffix conditionality and multi-offense selection differ slightly between the platforms' JSON parsers. Normalize or document.
  • createRequire's hand-rolled file: parser disagrees with node:url (%2F honored, file://FILE: rejected vs Node); shared. Rebase it on the URL primordial (the comment claiming that's unavailable is wrong on both sides).
  • JSON modules are built textually ("export default " + jsonText), so a "__proto__" key becomes a prototype setter instead of an own property. Shared deviation from JSON-module semantics.
  • require-factory hooks: __requireOverride is Android-only with no producers in-tree; __pauseOnNextRequire is iOS-only (inspector-produced). Converge or delete.
  • The 1s local-entry yield: normative in both docs, but its original purpose was iOS's pre-main() runway. Decide whether to shrink/remove (doc change on both).
  • Waiter-invariant race fixture: the "already-evaluating with no continuation attached" dynamic-import window is documented but unenforced on both platforms — needs a spec that pins it.
  • Frozen-at-mint pumping options: neither suite pins that the options object is validated once and immune to later mutation. Add the spec on both.

Android follow-ups

  • Lock the process-global interop caches (Console timers, ArgConverter, MetadataNode/MetadataReader statics) reached from main + worker isolates — the Android analogue of ios 3a6332a7, and the long-suspected worker-crash class. Same discipline: lock the find and the emplace, never across a build. Deserves its own PR.
  • Cap/pool the per-HTTP-edge fetch threads: FetchModuleBodyAsync spawns a detached std::thread + JVM attach per edge with no bound; a spawn failure after pendingFetches++ wedges the graph pump to its deadline (iOS caps at 16/host).
  • ESM code cache: classic scripts consume/produce the V8 code cache; ES modules compile from source every time (iOS caches).
  • NS_DEBUG is unreachable on a real device (env var); the docs call it "the only way to trace boot". A debug.nativescript.* system property (pattern already used in Runtime::GetAndroidVersion) would make it real.
  • HTTP worker entries: BootEntryEvaluationOptions carries a full HTTP-worker branch, but the Worker constructor's Java resolution rejects HTTP paths — dead code or missing feature; decide.
  • Move the ns:module binding out of HttpLoader.cpp (beside the loader state, or a dedicated TU): kills the transport→loader header dependency, the ValidateImportMapJson forwarder shim, and the import-map double parse.
  • Make the transport's thread invariant structural: adopt iOS's HttpFetchModule(url, canonicalKey, ...) parameter shape so off-thread canonicalization is impossible by construction (currently convention + comments).
  • Remaining K14 leftovers: three resolver/import empty-return-without-exception windows in teardown/OOM paths; catch (std::exception e) by value in ModuleInternal.cpp; IsolateData::RUNTIME is never cleared (the TryGetRuntime guards are defense-in-depth only and one teardown comment overstates the invariant).
  • NS_TIMERS_NESTING_CLAMP is documented opt-in and defined by no build file — confirm that's intended (the HTML 4ms nesting clamp is currently off everywhere, including the test suite).
  • Verify @nativescript/core's setTimeout rides __ns__setTimeout: the pumping-require timer story assumes it (the test app's own polyfill is a Java Handler post that no pump can dispatch, by construction). Confirm against core before advertising the behavior.
  • Dev-server smoke test against @nativescript/vite (the same gate as iOS): validates the canonicalization change, the vocabulary purge, the MIME gate, and the ESM-entry routing — and, if the sync-fetch anomaly guard stays cold, authorizes deleting it.
  • Accepted-and-documented, revisit if it bites: the boot backstop blocks the launching thread (up to 120s, dev-time HTTP entries only) by explicit decision — runtime.run() returning entry-settled is load-bearing for NativeScriptActivity; require.resolve/require.cache/require.main remain absent (feature-detectable).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions