NativeScript / NativeScript/android
ESM loader: remaining cross-platform decisions and deferred follow-ups from #1965
还没有人认领这个 Issue。
- 主要语言
- C++
- 星标
- 563
- 派生
- 144
- 平均合并
- 10 小时 46 分钟
- 30 天内合并 PR
- 14
描述
#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 (fixes landed as NativeScript/ios#444).
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 namingns: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: directEvaluate()+ 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.urlshape: Android exposes the full device path; iOS base-strips tofile:///app/.... Portable code matchingfile:///app/breaks on Android today. Couples withfile://specifier candidate handling and iOS'sdirnameinconsistency. - Transport identity: Android sends
User-Agent: NativeScript-HTTP-ESMand inherits the processCookieHandler; 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).
-
HasPendingAsyncModuleGraphWorkis 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-rolledfile:parser disagrees withnode:url(%2Fhonored,file://FILE:rejected vs Node); shared. Rebase it on theURLprimordial (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-factoryhooks:__requireOverrideis Android-only with no producers in-tree;__pauseOnNextRequireis 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.
- Pump mechanism shape on iOS (surfaced by porting android#1965's timer specs to iOS): a pumping require started from inside iOS's shared JS-timer
CFRunLoopTimercallback can never observe another timer fire — CFRunLoopTimer does not re-enter. Android's pump drains the ordered lane directly and is immune; whether iOS should adopt the same drain shape instead of spinning the runloop is a joint pump-contract call. - 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
Reclassified 2026-08-20: the five checked items immediately below turned out to be features present in the merged ios#383 code that the port had missed — implemented in #2021.
-
ESM code cache — landed in #2021:
CompileFileEsModuleconsumes/produces cache blobs (.mcachekeying avoids colliding with the classic.cachefor the same file). -
Fetch concurrency cap — landed in #2021: 16 concurrent fetches process-wide (iOS:
HTTPMaximumConnectionsPerHost = 16), queued jobs drained by finishing threads, spawn failures delivered as transport errors. -
Canonical-key transport signatures — landed in #2021: the transport takes keys from callers (iOS's shape); also fixed collapsed-scheme cache-bust marks never matching.
-
HTTP worker entries — landed in #2021:
new Worker("http://…")works end-to-end (security gate, vocabulary copy, settle gate, inspector URL), spec included. -
ns:modulebinding beside the loader state — landed in #2021 (ios5ca0c43c), including theParseImportMap/InstallParsedImportMapsplit that removes the double parse. -
Lock the process-global interop caches — done in 7c9c3db3, though not as scoped here. The three caches this item names were already covered by #2013:
Consoletimers andArgConverter's type-long cache became per-runtimeRuntimeState::For<T>(isolate)(theIsolate*-keyed process-global maps are gone entirely),MetadataNode's name/tree-node caches gots_nodeCacheMutex,MetadataReadergot its ownStateMutex, andJEnv::s_classCache/s_missingClassesplusMethodCache::s_mthod_ctor_signature_cacheeach got ashared_mutex— all on the lock-the-find-and-the-emplace-but-never-across-the-build discipline this item asks for. Auditing the runtime for what genuinely remained in that class turned up four more, all fixed in 7c9c3db3:File::Buffer— one process-wide 1MB scratch buffer thatFile::ReadTextfilled and returned a pointer into. Main and worker runtimes read modules concurrently from their own threads, so one thread'sfreadoverwrote bytes another was still copying out: silent module-source corruption rather than a crash, which is why this one never appeared in a tombstone. It also never saved the allocation it appears to save — every caller goes through thestd::stringoverload, which copied out of it on the next line — so reads now go straight into the returned string and the borrowing overload (no callers) is deleted.JType::EnsureInstance— published*instancebeforeInithad filledclazz/ctor/valueMethodId, so a second thread could find the pointer non-null and call through uninitialised JNI ids, on the hot boxing path. Nowstd::call_onceperType.CallbackHandlers::Init— runs once per runtime fromPrepareV8Runtime, so every worker start rewrote the process-global class and method-id statics (and re-ranMethodCache::Init) while the isolates already running were reading them. Process-global half now undercall_once;MetadataNode::Init(isolate)stays per-isolate.MetadataNode::IsJavascriptKeyword— filled a function-localstatic set<string>behind anempty()check, racing reads from every runtime's thread.
Audited and deliberately left alone:
JsV8InspectorClient::Domainsis unguarded but main-isolate only (workers useWorkerInspectorClient, which never registers domain dispatchers);SimpleProfiler::s_framesis racy and stores a pointer into a reallocating vector, butSimpleProfiler::Inithas no callers (dead code);PageResource::s_cachedPageResourcesis write-only, nothing reads it. -
Runtime::ReadFileText'sm_fileWriteMutexis broken three ways (surfaced while removingFile::Buffer, which this mutex superficially looked like it guarded — it never did). Its real purpose is LiveSync:NativeScriptSyncServiceSocketImpl.createOrOverrideFile()takesruntime.lock()so the runtime does not compile a half-written hot-synced module. But (1)m_fileWriteMutexis a non-static member (Runtime.h), so LiveSync locking one runtime excludes nothing on a worker's thread — precisely the case that matters; (2) the lock is taken afterprepareFile()deletes the file andnew FileOutputStreamtruncates it, so it covers onlyfos.writeand not the window a reader actually hits; (3) thefinallycallsruntime.unlock()even whenlock()was never reached (a throw frommkdirs()/new FileOutputStream), which is UB and on bionic can release a lock the JS thread holds insideReadFileText. Fix direction: make the write atomic — temp file plus same-directoryATOMIC_MOVE— instead of locking readers.rename(2)within a directory is atomic, so a reader gets the whole old or the whole new file; that also closes the delete/truncate window no reader-side lock can reach, and drops a mutex from every debug module read.m_fileWriteMutexandRuntime::Lock/Unlockthen delete cleanly, keepingcom.tns.Runtime.lock()/unlock()as deprecated no-ops since they arepublicand external tooling may call them. Implemented in #2022 — unmerged: LiveSync has no automated coverage in the runtime test app, so it needs a manual hot-sync check (ns run android, edit a file, confirm reload; also a newly-created file and a deleted one, sinceprepareFile()is gone) before merging. -
NS_DEBUGis unreachable on a real device (env var); the docs call it "the only way to trace boot". Adebug.nativescript.*system property (pattern already used inRuntime::GetAndroidVersion) would make it real. -
Remaining K14 leftovers: three resolver/import empty-return-without-exception windows in teardown/OOM paths;
catch (std::exception e)by value inModuleInternal.cpp;IsolateData::RUNTIMEis never cleared (theTryGetRuntimeguards are defense-in-depth only and one teardown comment overstates the invariant). -
NS_TIMERS_NESTING_CLAMPis 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'ssetTimeoutrides__ns__setTimeout: the pumping-require timer story assumes it (the test app's own polyfill is a JavaHandlerpost 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 forNativeScriptActivity;require.resolve/require.cache/require.mainremain absent (feature-detectable).
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
首先将跨平台契约决策与 Android 后续工作分开,然后阅读 Runtime.h、Runtime::ReadFileText 和 ModuleInternal.cpp,以了解明确列出的原生工作。使用 Android 运行时测试应用和列出的 ns run android LiveSync 检查作为验证入口;只有在其中所述的行为已实现或记录,并且相关平台检查通过后,每个选定项目才算完成。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- android, cpp
- 领域
- mobile-dev
- Issue 类型
- 重构
- 难度
- 5/5
- 预计耗时
- 一周以上
- 活跃度
- 冷清
- 描述清晰度
- 需要澄清
- 新手友好度
- 20/100