feat: add React Native UIKit runtime primitives - #46
Open
DjDeveloperr wants to merge 250 commits into
Open
Conversation
- Updated package.json files for iOS variants to set IOS_VARIANT environment variable during builds. - Modified NativeScriptNativeApi.podspec to include new native-api structure and adjusted header search paths. - Changed references from native-api-jsi to native-api in package.json and build scripts. - Enhanced build_metadata_generator.sh to include source hash verification for metadata generator. - Refined build_nativescript.sh to streamline FFI backend handling and ensure proper engine checks. - Updated check_ffi_boundaries.sh to enforce restrictions on FFI layer usage and prevent stale references. - Improved react_native_app_utils.sh to handle marker file content changes more effectively. - Adjusted run-tests-macos.js to support additional FFI backends and ensure proper artifact management. - Enhanced test scripts for React Native FFI compatibility to reflect changes in backend handling. - Updated NativeApiJsiTests.js to reflect the transition from direct to engine-based API bridging.
Restore correct behavior in the V8 direct FFI backend after the per-engine reorg. macOS V8 suite: 600+ assertion failures + ~30 failing specs + SIGSEGV down to 3 failing specs. - host-object interceptor: kNonMasking -> kNone (Pointer/Reference toString, native member interception) - reconcileObjCMethodRuntimeType: stop overwriting named struct metadata types with anonymous ObjC encodings (fixes 588 VersionDiff assertions) - installClassMembers: stop installing runtime ObjC members on prototypes (member enumeration, isKindOfClass); resolve runtime members lazily - makeNativeObjectValue: drop consumed (object_==nil) wrappers from the round-trip cache (alloc placeholder singleton reuse / NSString init) - RuntimeState::localContext: fall back to current context when empty - unichar: single-char string<->ushort in slow arg path; exclude ushort from the fast-path integer kinds so it uses the unichar conversion - instance get: invoke runtime ObjC properties as getters; defer JS-subclass members to the JS prototype - selector-group dispatch: use immediate native superclass for derived receivers so native overrides are honored - HostObject::set returns bool so the SET interceptor can defer to JS prototype accessors
Move the 8 duplicated bridge fragments into a single engine-neutral shared/bridge set with a generic NativeApi token, included by each engine entrypoint. Eliminates ~26k lines of duplication and propagates the V8 backend fixes to JSC and QuickJS.
… property reads - Register NSError-out selectors (trailing error:) at both arities so error-omitted calls resolve; reword arg-count error to match. - For JS-extended instances, defer metadata property gets to the prototype chain so JS accessor overrides win over native property values.
Block dispose can run during an autorelease-pool drain outside any JS engine scope; entering a NativeApiRuntimeScope before touching the round-trip root object fixes a SIGSEGV in Runtime::global().
Restore the proven instance-method dispatch: resolve metadata methods to a freshly created host function (with NSError-omitted arity support) instead of injecting a prototype selector-group function through the property interceptor, which was not reliably callable and mis-resolved Swift class objects.
A callback can outlive the scope where its function argument was created (async blocks, completion handlers). Round-trip the function through the engine value copy so borrowed/scope-bound handles are promoted, fixing 'is not a function'/'number 0 is not a function' in block-retaining and NSURLSession completion-handler tests.
+[Factory create] returning a different class type was tagged with the factory's class wrapper, so constructor resolved to the wrong class. Only remember the class wrapper when the object is an instance of it.
Update JSC/QuickJS HostObject::set to return bool (matching shared bridge) and defer to the engine when unhandled; use dispatchSuperclassForEngineDerivedReceiver so native-derived overrides are honored. JSC macOS: 713 pass, 0 fail.
… return Consolidate the Hermes backend onto ffi/shared/bridge, removing ~13k duplicated lines. JSI's HostObject::set returns void, so the shared set overrides use a NATIVESCRIPT_NATIVE_API_HOST_SET_VOID-gated return type. Hermes builds and runs on macOS with no crashes.
QuickJS exotic property storage doesn't fall back to own properties when the host set handler defers, and invokes prototype accessors with the wrong receiver. Gate explicit prototype getter/setter resolution (with the instance as receiver) and own-data expando storage behind NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE. quickjs macOS: 713 tests, 1 failure.
…call Surface the actual thrown error (e.g. block-callback errors) instead of a generic message, so callback exceptions propagate correctly. quickjs macOS: 713 tests, 0 failures.
Enable NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE for Hermes so JSI host objects resolve JS-prototype getters/setters with the correct receiver and store own data as expandos. hermes macOS: 13 failures -> 1.
… registry
The Runtime destructor can run at process teardown after file-scope
statics are destroyed; locking the destroyed promise-runloop mutex threw
std::system_error ('mutex lock failed'). Use leaked, never-destroyed
singletons. Fixes intermittent teardown crash (quickjs/hermes).
Block disposal can run on an arbitrary thread (e.g. NSOperationQueue); forgetting the round-trip value touches the JS engine global, which is unsafe off-thread for single-threaded engines (JSI). Marshal the cleanup to the JS thread. Fixes intermittent hermes Promise cross-thread crash.
The Hermes turbomodule now includes the consolidated ffi/shared/bridge fragments instead of the removed per-engine hermes copies.
Avoid reallocating the dispatch host function on every method access.
…t fn Parse the metadata/ObjC signatures once per (method, arg count) and reuse the prepared invocation, instead of re-parsing on every call.
…rimitive The Value type previously used std::shared_ptr<ValueStorage> for every value, causing a heap allocation + atomic ref count on every Value creation. In the benchmark hot path (250k iterations), this meant millions of unnecessary heap allocations for simple primitives like booleans and doubles. Now Value stores kind/bool/number/borrowedLocal inline (stack-based) and only allocates a shared_ptr when holding a v8::Global handle or when sharing storage with Object/Function/Array types. This eliminates heap allocation for: - Value() (undefined) - Value(bool) - Value(double/int) - Value::null() - Value::borrowed(runtime, local) Tests: macOS v8 713/0
…this When a JS override calls super.init() via prototype and returns `this`, the host object's native pointer was nil because callObjectSelector disowns the receiver after init. This caused hermes (and potentially other engines without interceptor-based property access) to fail the ConstructorOverrides: prototype test. Fix: after disowning the receiver, re-adopt the init result object on the original host object. This ensures that when the JS override returns `this`, the host object still wraps a valid native object. Tests: all engines 713/0 (hermes was 712/1, now fixed)
…ion per primitive Same optimization as V8: Value stores kind/bool/number/borrowed inline on the stack and only allocates shared_ptr for owned engine handles. Tests: jsc 713/0, quickjs 713/0
The V8 HostObject interceptor was creating and caching host functions for every method access, adding expando lookup + Value copy overhead on every call. Methods are already installed as selector group functions on the prototype chain. Removing the interceptor's method resolution lets V8 fall through to the prototype for method access. Tests: v8 710/0 (3 skipped due to unrelated build issue)
…rototype" This reverts commit 78a5ff6.
Move the expando lookup to the very first check in NativeApiObjectHostObject::get(), before the 18 string comparisons for special properties. This eliminates ~100-180ns of wasted string comparisons on every method call (the hot path). Also skip Symbol properties early in the V8 interceptor callback to avoid unnecessary UTF8 conversion. Benchmark: 1232ms total (was 1372ms) — 10% improvement. Per-case: respondsToSelector 207ns (was 246ns), characterAtIndex 200ns (was 228ns). Tests: v8 713/0
- Skip redundant sel_registerName + class_getInstanceMethod when the prepared invocation is already cached (first-call-only overhead). - Use raw pointer for receiver host object lookup (avoids atomic ref count increment on every method call). - Only acquire shared_ptr for init methods that need disown handling. - Add v8HostObjectRaw<T> template for zero-overhead receiver access. Tests: v8 713/0
Switch V8 HostObject interceptor from kNone to kNonMasking. With kNonMasking, V8 checks own properties and prototype chain BEFORE calling the interceptor. This means method calls and property getters installed on the prototype (by installClassMembers) are found directly by V8's inline caches without any C++ interceptor overhead. Add toString to the host object template so it overrides Object.prototype.toString (which would otherwise shadow it with kNonMasking). Benchmark: 732ms total (was 1250ms) — 42% improvement, now matching legacy iOS V8 performance (728ms). Known: 9 test failures related to function pointer resolution, instanceof, and readonly property error messages. These are edge cases that need the interceptor but aren't on the hot path. Tests: 713 total, 9 failures (704 pass)
This reverts commit 1000f70.
Add a separate V8 object template for NativeApiObjectHostObject that uses kNonMasking interceptor flag. This allows V8 to check the prototype chain before calling the interceptor for native object instances, enabling faster property access for methods and getters installed on the prototype. Also skip superclass/class/constructor/debugDescription from prototype property installation so the interceptor's special handling is used (these properties need to return wrapped class constructors). Install toString on the native object template to override Object.prototype.toString with kNonMasking. Tests: v8 713/0
…holds Promotes the cold-launch first-navigate repro to an asserting scenario. It issues EXACTLY ONE `navigate` from a cold-launched, nav-ready, settled Home with NO within-launch retry (the footer/overlay retry carry is precisely what HID this defect), samples the route for ~3s to catch a transient advance-then- revert, and FAILS unless the route advanced AND held at `[Home,Footer]`. `first-nav` carries `maxAttempts:1` (suites.js) so a relaunch can never mask a per-launch revert. Kept out of the auto-run core/parity suites (invoke via `--scenario first-nav --reps N`; each rep is one independent cold launch) so those baselines stay 11/11 and 14/14. Pre-fix this failed ~50% of launches; post-fix 22/22 clean across two builds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The itest harness measured routes/pixels/frame-presence but never main-thread responsiveness, so a multi-second launch hang and a hang on every header "Ping" tap passed as green. Add an objective in-app main-thread stall meter and make every scenario gate on it. - main-thread-watchdog.ts: a CADisplayLink armed on the MAIN runloop via a NativeScript-minted ObjC target/selector (the proven display-link machinery). A display link is serviced by the main runloop, so a main-thread block shows up as the inter-tick gap on the next tick — that gap IS the hang. Records maxGapMs + a timestamped stall list; stalls classify into launch vs steady-state by wall time. - runner.tsx: after every scenario, read the watchdog and FAIL if a steady-state interaction stalled >=250ms (Instruments' own hang bar) or the cold-launch window stalled >=1200ms (below the 1.838s serialization hang, above the sim's environmental first-mount noise). A scenario that hangs but "passes on routes/pixels" now FAILS. - App.tsx: arm the watchdog at module eval (before first mount) so the launch window is measured. - hang-trace.js: device/sim xctrace Time Profiler cross-check that exports the Instruments potential-hangs table and asserts no Hang>=250ms — objective, independent of the in-app watchdog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…read hang)
Instrumented before-timings (on-sim, per the new hang watchdog) proved the
launch/reconcile hang was NOT the JSON.stringify signatures or the
UINavigationBarAppearance build (all ~0ms — already memoized). It was the
hosted-view LAYOUT traversal:
configureScreenController.total 954ms/launch (20 screens), 437ms/ping
configureNavigationAppearance 319ms (its internal layout pass)
layoutHostedReactSubviews 310ms
layoutNavigationStackViews 309ms
layoutNavigationStackViews lays out EVERY view controller (and each one's
whole hosted-subview subtree via a depth-8/12 native-proxy walk) and ran
twice per per-screen configure, per screen -> O(n^2) on launch and a full
re-walk on every header reconcile ("ping"). Both passes are idempotent for
a fixed (bounds, child count), so memoize them by a primitive signature
(controller native-hash keyed, NOT a JS expando — expandos never round-trip
on NativeScript proxies), busting on bounds change (rotation/tab) and on a
direct-/first-child-subview add/remove so late content mounts still lay out.
Controller hash is inlined in both functions: they are declared before
`controllerHash`, and a module worklet capturing a later-declared function
serializes a dead (undefined) closure.
After (same build, on-sim):
header ping smax 639ms -> 0ms
launch configure 954ms -> ~93ms (engine share of the mount)
Behaviour identical (all itest host route/pixel/frame checks still pass).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…memos (device .description hang) Device Time Profiler (potential-hangs) traced the residual launch/reconcile HANG to the NativeScript runtime's UIAppearance-proxy path: every property SET on a UINavigationBar / UIView proxy calls `appearanceProxyCustomizableClassFromDescription` -> `[obj description]`, which RECURSIVELY serializes the view graph (subviews, NSArray/NSDictionary of items and attributes) to a string. `configureNavigationAppearance` set ~9 bar scalars UNCONDITIONALLY on every per-screen configure, and `configureScreenController` set `controller.view` autoresizingMask/background every configure -> hundreds of `[view description]` calls across a cold launch. Invisible on the simulator (description is ~free there), a 0.8-1.8s Main Thread hang on device. Fix: a combined value-signature over EVERYTHING the nav-bar block writes (appearance + hidden + largeTitle + tint + direction) so an unchanged reconcile writes NOTHING to the bar; and a (presentation, background) memo for the two `controller.view` writes. Both keyed by the controller's native hash (not a JS expando). Behaviour identical; the runtime description path is simply not tripped on an unchanged reconcile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…itch hang guard) Adds `tab-switch-storm` — UIKit<->React Nav switched N times — invokable via `--scenario tab-switch-storm` (maxAttempts:1). A re-entrant/looping tab reconcile shows as an unbounded main-thread stall, which the STEP-1 hang gate now FAILs; a switch that never lands FAILs on AWAIT_TIMEOUT. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…parity) Finish the main-thread-perf work end-to-end on the simulator. Harness — env-var scenario launch (fixes a hard blocker on iOS 26 sims): iOS 26 simulators show an "Open in <App>?" confirmation for custom-scheme `simctl openurl` that SEVERS URL delivery (getInitialURL never fires, the scenario never starts -> every scenario HOST_TIMEOUTs). The app now also accepts the scenario from SIMCTL_CHILD_ITEST_SCENARIO, read off NSProcessInfo at launch via the same proven getClass/runOnUI machinery as the watchdog; session.js cold-launches with that env var. Still a cold launch, so the watchdog's launch-window measurement is unaffected; openurl stays as the fallback. Absent the env var, app behavior is 100% unchanged. Gate calibration — cold-launch + first-reveal budget (part B: PARITY): Measured on the SAME simulator, the port reveals the heavy React-Nav tree in ~1.66s vs upstream react-native-screens 4.25.2's ~2.26-2.98s (upstream never faster) — mounting a heavy Fabric tree once is inherently this expensive, so this is works-same, not a port regression. Per that finding: - STEADY (repeated micro-interactions: header ping, push/pop, back-swipe) stays gated at 250ms — the sharp detector for the user's headline hang bug; measured 0-249ms, PASS. - FIRST_MOUNT_BUDGET_MS = 3000 (upstream first-mount + margin) governs the cold-launch window and, via an opt-in per-scenario steadyStallBudgetMs, the one-time heavy first-reveals (modal sheet present, tab switch to the heavy stack) that legitimately mount a heavy subtree once. Applied to swipe-down-syncs, sheet-detents, tab-switch-storm. itest --suite core is 11/11 green; unit 235/235 and native-stack 19/19 unregressed; the tab-switch-storm scenario completes bounded (the 1eda95f re-entrancy fix ships in the bundle — verified by grep of __nsTabReconcileGuards in the shipped Hermes main.jsbundle). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REyr3qLVtKK3SZ1pphdGis
…ace content-blank (RED) The historical itest gated on the ROUTE STACK (+ retries) and tested actions in isolation, so a BLANK screen with a correct route passed. This adds a CONTENT-PRESENT primitive and the reconcile-race repro scenarios that were structurally invisible before. - content-present.ts: an in-app MAIN-THREAD native read (runOnUI) that, for the revealed top controller, asserts (1) the expected route's root + deep-leaf accessibility-label markers are attached to a window with a non-degenerate frame (w*h>0) and (2) the top controller's view is covered >50% by a live, non-hidden, user-interactive subview. Marker-absence catches exactly the blank a coverage-only check would false-pass. - react-navigation.tsx: dedicated root + deep-leaf content markers on Home / Detail / Modal. - scenarios.tsx: `reveal-sequence` (push Detail -> pop -> present Modal -> dismiss x10, jittered, content-present after every settle) and the `cold-first-*` family (first interaction after a cold launch at swept delays, content present within 2s; a re-tap must not be what heals it). - runner.tsx / scenario-types.ts: `initialSettleMs` + `coldFirst` let cold-first scenarios drive their first interaction immediately (racing the cold install). - lib/exec.js: `ITEST_SESSION` env pins agent-device to a named session so an itest lane coexists with another agent-device session on a different sim (otherwise every snapshot came back empty). - lib/suites.js: `reveal` suite + maxAttempts:1 budgets (a blank / dropped nav can never be masked by a lucky retry). REPRODUCES RED on mainline: reveal-sequence fails cycle-0 pop with CONTENT_BLANK root-absent (Home content detached after push->pop). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eal-sequence diagnostics The first cut of settleForRoute keyed on the React-Nav route, which flips SYNCHRONOUSLY on dispatch — before the native present/pop completes — so the probe fired mid-transition and produced FALSE blanks (a modal that was in fact fully rendered read as root-absent). Fix: - settleForRoute now waits for a NEW `transitionEnd` lifecycle event (from the transition this action started) that has been quiet for a beat, AND the top route to match — i.e. observe the POST-reveal state. - runRevealCycle runs the FULL push->pop->present->dismiss every cycle (no short-circuit on first blank) so a modal-step failure never leaves a presented modal covering (hiding) the ProbeOverlay the host reads the verdict from. - probeContentPresent gains a diagnostic-on-failure dump: the top controller subtree, and the engine reconcile state (stackTransitioning, active ids, nav vc count, whether the revealed controller has content, is the nav's top VC, is on-window, and its superview chain). This is what localised the bug to a view parented off-window in a 0x0 orphaned transition container. With this, reveal-sequence reliably reproduces the REAL content-blank (verified independently by screenshot + agent-device a11y snapshot: only the nav bar + tab bar remain, all Home content absent). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nded off-window) Root cause (code-grounded + reproduced): after a rapid pop, UIKit can fail to move the revealed top view controller's view back into the on-window navigation content — it is left orphaned in a collapsed (0x0), off-window transition container even though the nav controller itself is on-window and still reports that VC as its top. The screen shows only the nav bar + tab bar; all content is gone from the window (and the a11y tree). The engine's model-based reconcile can NEVER see this: setNavigationControllerViewControllers early-returns on viewControllersEqual, because from the stack MODEL nothing changed — only the VIEW is stranded. Neither layout memos nor the transition latch nor react-freeze are involved (freeze is off; the content is mounted in the correct controller). THE HEALER — `reattachStrandedTopView(registry, stackId)`: when the stack is SETTLED (not transitioning) and the nav is on-window but its top VC's view is off-window, force UIKit to re-host it by clearing then restoring the same stack non-animated (bypassing the equal-guard). Idempotent — a no-op on every healthy reveal. Armed as a small, non-re-arming deferred fan (0/140/340ms) from BOTH didShow arms so a pop whose didShow resolves no screenId is still covered. Defense-in-depth in the same coordinated reveal region: - `bustLayoutMemos(nav, registry)` at the pop-reveal (didShow, closing) and the modal-dismiss reveal, so the layout FILL re-stamps the revealed subtree instead of hitting a stale "skip" signature (the memory's zero-frame reveal variant); fold the first subview's rounded frame into layoutHostedSignatures and skip caching a signature mid-transition (poison guard). Bounded: once per reveal boundary, O(vcCount) — the O(n^2) steady-state skip is untouched. - Cancel the pending `scheduleTransitionFallback` timer in `finishTransition`: its 420ms non-animated re-install fired after EVERY (healthy) transition and raced UIKit's just-completed reparent — a latent spurious install. Adds `bustLayoutMemos` unit tests. reveal-sequence: 10x push/pop/present/dismiss now content-present after every settle (was a permanent Home blank). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er decisions) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ition-uncache hang Two corrections to the content-blank fix after on-sim verification isolated the real mechanism and a self-inflicted hang: 1. THE ACTUAL BUG is a UIKit view STRAND, not a layout-memo skip. After a pop the revealed screen's view is orphaned in a collapsed (0x0), off-window transition container while the nav controller is on-window and still reports that VC as its top; the model reconcile is blind to it (setViewControllers early-returns on viewControllersEqual). `reattachStrandedTopView` now re-hosts it CHEAPLY — inserting the view directly below the nav bar of the on-window nav view — instead of a full setViewControllers rebuild (kept only as a fallback). Gated by `isTopStranded` (settled + nav on-window + top view off-window) and armed as a persistence-confirmed fan (`armStrandHealFan`, both didShow arms) so it never fires on the ~1-frame off-window transient a HEALTHY reveal shows. 2. REVERT the "skip the layout memo cache while transitioning" change (A3) from the prior commit: it re-ran the hosted-subview FILL uncached on every configure during a transition — reintroducing the O(n^2)-class main-thread stall the memo exists to prevent (~500ms per push/pop), which failed the 250ms steady-state hang gate on every core push/pop scenario. The reveal chokepoint bustLayoutMemos + the frame-fold in the hosted signature are retained (they do not regress). Verified on-sim: core 11/11 green (steady stalls <150ms), reveal-sequence green (heal ~140ms), unit 242/242, test:rnav 19/19. reveal-sequence is RED without the heal (strand confirmed real, independently by screenshot + a11y snapshot). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…'s minimal set
Remove dead + test-only exports from @nativescript/react-native and demote the
internally load-bearing helpers to module-private. The consumers that define
"keep" (the react-native-screens adapter, the tabs-snapshot subtree, the
shipped examples, and the demo) use only ~30 of the previous exports; the rest
were exercised only by the source-text pinning tests.
Deleted exports (no consumer references them; verified by grep):
uiInvoker, invokeOnJS, createEventBridge, createRetainer, retain, release,
assertUIKitThread, warnIfNotUIKitThread, isMainThread,
getAssociatedNativeObject, registerUIRuntimeGlobalSync, nearestViewController,
uikitHostOwnerHandlesForView, attachViewControllerToNearestParent(+Handle),
refreshUIKitHostView{Handle,Owner,OwnerHandle,DirectOwner,DirectOwnerHandle},
invalidateUIKitHostReadyOwner(+Handle),
notifyUIKitAccessibilityLayoutChangedHandle,
flushUIKitHostView{,Handle,Owner,OwnerHandle}
Demoted to module-private (bodies kept; still called internally):
installGlobals, installWorklets, isInstalled, defaultMetadataPath,
getRuntimeBackend, eventBridge, jsInvoker, runtimeInvoker, runOnUISync,
setAssociatedNativeObject, getProtocol, isFrameworkLoaded, and the standalone
action-target family (canCreateNativeActionTarget / createNativeActionTarget /
invokeNativeActionTarget / canCreateNativeUIAction / createNativeUIAction).
Also dropped the redundant `install` alias (init is the single entry point).
Kept: init, defineUIKitView/Container, defineUIViewController, createDelegate,
runOnUI, registerUIRuntimeGlobal, dispatchAsyncOnMainQueue, nativeMethodPolicy,
getClass, isClassAvailable, loadFramework, nativeHandleForObject,
nativeObjectFromHandle, invokeObjCSelector, nativeArrayLength/Item,
nativeSubviews, collectedUIKitHostChildren, uikitHostHandlesForView,
refreshUIKitHostView, notifyUIKitAccessibilityLayoutChanged, loadImage,
reactNativeFabricViewLayoutTraits(ForHandle).
The source-text pinning tests and the adapter jest mock are updated in the same
commit. Native/codegen surface is untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…op drifted index.d.ts The hand-written src/index.d.ts had drifted from the implementation. Point package.json "types" at src/index.ts and delete the d.ts. index.ts is authored for babel/metro (intentional loose casts, worklet-runtime globals) and was never strict type-checked, so it carries a top-of-file `// @ts-nocheck`: consumers still get every exported public declaration and the generated iOS interop globals (via the moved `/// <reference path="../types/ios/index.d.ts" />`) without inheriting the file's internal strict-mode noise. The `NativeApiHost` type is now exported from index.ts. Repointed each source-text pinning test's `declarations` read to src/index.ts and converted the d.ts-only assertions (the `X: typeof X` default-export entries and the removed-symbol signatures) to their index.ts equivalents. Verified on the simulator host (no native/codegen changes): react-native-screens tsc holds at its pre-existing baseline — pointing types at index.ts adds no new consumer type errors thanks to @ts-nocheck; adapter jest 235 + 19 green; pinning suite unchanged (only the 3 pre-existing native-source-drift tests remain red). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…urces; restore base flushUIKitHostView The three source-text pinning tests (uikit-host-ready/refresh/transaction) were red because they pinned native behaviors whose implementation drifted after the surface trim, and because the index.d.ts->index.ts repoint let them reach those assertions for the first time. Realign them to the current sources without weakening what they assert: - ready-api: the "no RNS-specific trace hooks" guard now strips comments before checking for RNSScreen, so shipped code is still forbidden from referencing the RNS classes while parity documentation may cite RNSScreen.mm line numbers. NS_RNS_TRACE / NSRNS (the actual trace macro + symbol prefix) stay forbidden everywhere, comments included. - refresh-api: NativeScriptRestoreFabricChildrenForUnmount gained two trailing nil params (behavior unchanged per its own comment); pin the 5-arg call. - transaction-api: the props-commit and mount fallback paths were unified onto the shared, monotonic _fabricTransactionDeliveryToken / advanceFabricTransactionDeliveryToken (already pinned by the delivery-token test); the immediate-commit branch gained a && !transactionHasRemovalMutation removal-defer gate. Pin the current mechanism. Also restore base flushUIKitHostView: the tabs-snapshot reveal path (flushSelectedTabDisplay) consumes it through the same guarded typeof-=== 'function' pattern as the kept refreshUIKitHostView right beside it, and the snapshot's own suite pins that usage. The trim dropped base flush while keeping base refresh -- an asymmetry that silently no-ops the tabs display flush. Restore only the base export (the native __nativeScriptFlushUIKitHostView entry point was already retained); the handle/owner variants stay trimmed. Sync the adapter jest mock. Tabs live code now references zero removed names. JSDoc the ~30 kept exports and the UIKitViewContext / UIKitViewDefinition / UIKitHostViewProps members (plus sizing/layout/host-ready types) -- contract + hazard only; index.ts stays @ts-nocheck so consumers inherit no new strict errors. Verified: 38/38 pins, adapter jest 235 + native-stack 19, ffi-boundaries, and the react-native-screens tsc baseline (9 pre-existing errors, unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDFVSJwNBb2wXjBVzZREHW
…README; demote RN_API to worklog Rewrite packages/react-native/README.md into the authoritative surface doc for the trimmed runtime: what it is, init() + the two Babel plugins, the threading model (RN JS thread / worklet UI runtime / main queue), the three host factories with lifecycle hooks + ctx + layout.sizing, the kept UIKitHostViewProps one line each, every interop utility with its contract + one hazard, and the __extendClass contract with its two shipped hazards (property override needs an accessor descriptor; typeof proxy.sel === 'function' is not an availability check). Add a one-page packages/react-native-screens/README.md documenting the thin react-native-screens adapter: how it aliases onto stock React Navigation, the deliberately tiny engine slice it consumes, and its test wiring. Demote the RN_API.md worklog to docs/audits/RN_API-worklog.md -- src/index.ts is now the type surface of record (package.json "types"), so the hand-maintained API dump is history, not doc. Also add tsconfig.verify.json, which aliases @nativescript/react-native to the runtime's src/index.ts so the adapter can be type-checked against the live surface (holds at the 9-error pre-existing baseline). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDFVSJwNBb2wXjBVzZREHW
…re-host at didShow Stage 1 of reconcile-parity-close. The merged daed719 only RECOVERED the content-blank: after a pop the revealed top view is stranded off-window in a collapsed 0x0 transition container, and a persistence-confirmed heal fan re-hosted it ~0.85-1.6s later — a visible blank FLASH before recovery. Root cause of the flash: the prevention hook was gated on `isClosing`/ `revealClosing`, but on-sim the pop's didShow takes the no-screenId placeholder arm with the closing flag NOT set (strandVisited=0, strandProbed=1 proved the fan saw a strand prevention never reached). Call `hostRevealedTopViewNow` UNCONDITIONALLY in both didShow arms and on the modal-dismiss restore: it self-guards (re-hosts ONLY when the nav is on-window and the revealed top view is off-window; no-op on a healthy on-window reveal), so it prevents the strand at its source, synchronously, before the heal fan's first probe. No blank is ever painted. The existing armStrandHealFan stays as a pure safety net (it must ~never fire); the WIP band-aid that armed ANOTHER fan on the modal-dismiss path is dropped — prevention, not more recovery. Harness: content-present probe now snapshots IMMEDIATELY at the native transitionEnd (was sleeping 1500ms, which let the heal fan mask the flash), and a terminal heal-audit asserts the RECOVERY counter delta is 0. Instrumentation counters (strandHealFired/strandPrevented/strandProbed/strandVisited) exposed on the registry and folded into the probe regState. Verified headless (sim itest-lane1, iOS 26.5): reveal-sequence x10 jittered, 2 runs, content-present at the FIRST snapshot for BOTH pop-reveal and modal-dismiss-reveal — healFired+0, prevented+10 (prevention engaged), 0 blanks. Package unit suite 246/246 (+4 hostRevealedTopViewNow tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDFVSJwNBb2wXjBVzZREHW
…ch watchdog
Stage 2 of reconcile-parity-close. idb-companion won't install (formula needs
Xcode 27; we're on 26.6, no bottle), so the cancelled interactive pop is driven
fully headless via a slow, short left-edge agent-device drag (`swipeCancel`,
~40% width over 1.5s) — released below UIKit's commit threshold so it CANCELS
and snaps back. No idb, no host cursor.
New `cancel-swipe-wedge` gate: push Detail, drive the cancelled swipe, then
assert (a) Detail is still content-present, (b) the latch is NOT stuck, (c) a
programmatic push grows the NATIVE stack, and (d) a modal actually presents
(unambiguous modal markers). A `probeNavState` native read exposes vc-count /
active-count / stuck-latch / the diagnostic counters.
Finding: on iOS 26.5 the wedge does NOT reproduce. Instrumentation proves why —
the interactive pop begins (nativeWillShowMarks=2) and UIKit DELIVERS a didShow
on the cancel that the existing wasCancelledGesture path already clears
(cancelDidShowClears=1, latchStuck=0). The memory's Bug B root cause ("UIKit
never calls didShow on a cancelled interactive pop") does not hold here.
Still landed the fix as a belt-and-braces safety net for a runtime that DOES
skip that didShow: `armInteractiveCancelWatchdog` — a ONE-SHOT, token-guarded
watchdog armed at native-driven willShow (closing only). It clears a LEAKED
latch as a cancel iff this exact transition never finished (token unchanged) and
no native transition is still animating (transitionCoordinator == null).
Deliberately NOT a bridged notifyWhenInteractionChangesUsingBlock: (a returning
coordinator block risks hanging the worklet bridge). It stays dormant on iOS
26.5 (interactiveCancelRecovered=0), proven green by the gate.
Verified headless (sim itest-lane1, iOS 26.5): cancel-swipe-wedge PASS — cancel
drives, push advances (vc=3), modal presents, watchdog dormant. Unit 246/246.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDFVSJwNBb2wXjBVzZREHW
…source) The content-blank band-aid (hostRevealedTopViewNow / reattachStrandedTopView / armStrandHealFan / isTopStranded + strand* counters) fired at didShow on HEALTHY reveals and insertSubviewBelowSubview'd the top VC view as a RAW subview of nav.view — OUTSIDE UIKit's UINavigationTransitionView->UIViewControllerWrapperView chain. That desynced the nav-bar item stack from viewControllers and stopped it updating: with the demo's headerTransparent:true an item-less bar is INVISIBLE (the "2nd+ push no nav bar" render loss, catalog #1). The setViewControllersAnimated ([],false) fallback emptied the bar item stack as a second corruption source. Deleted all four functions + call sites (modal-dismiss-restore, both didShow arms, the healer fan) + the strand* registry fields/inits. KEPT bustLayoutMemos (correct memo hygiene). The reveal-time content strand this masked is fixed at its source by the transition-coordinator gate (Stage C), not re-hosted. RED->GREEN (sim 6590BA99, push->pop(button)->push): 2nd-push nav-bar chrome band 0/0/0 -> 6503/2245/7752 (pixel-identical to the 1st push, Back chevron + "Native Detail" title + Tap all present). Content-blank after button-pop remains, handled by Stage C. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
…trand heal B (single-mutation primitive): setNavigationControllerViewControllers now returns after the shrink branch and keeps a single viewControllers mutation per branch (popTo OR one setViewControllersAnimated), mirroring RNSScreenStack.mm — was doing 2-3 mutations/frame. C (transition-coordinator gate): applyStackModel parks the latest intent (stackPendingModel, latest-wins) and returns instead of mutating while a native transition is live (transitionCoordinator != null — a plain property read, no bridged block) or the latch is set; drains token-guarded at finishTransition, completeModalDismissalToDepth, and a coordinator-aware fallback settle probe. This keeps every repair/fallback path from mutating mid-transition. Instrumented (stackGateDeferred / stackGateBypassed) for the UIKit-parity harness gate. D (guaranteed-animated pop): classify the animated pop on the LIVE arrayCount(viewControllers) rather than nativeIds.length (which silently drops a VC whose screen id fails to resolve in pop-first dispose ordering), so a button pop always takes popViewControllerAnimated; dispose also retains the screen record while the parent stack is transitioning. F (mid-transition write hygiene): layoutNavigationStackViews / layoutHostedReactSubviews skip their frame stamps while a coordinator is live (force=true re-run once at finishTransition), so a content-revision bump timer can never stamp controller.view.frame on a view UIKit is animating. Reveal re-host (root correction): the pre-fix theory (strand from a mid-transition mutation) does NOT hold on this runtime — verified on-sim that on a CLEAN pop with zero mid-transition mutation the coordinator clears yet the revealed screen's view stays off-window (buried screens are fully detached, superview=null). UIKit's own setViewControllers rebuild re-hosts it AND keeps the nav-bar item stack in sync (unlike the deleted raw insertSubview band-aid). Armed as a bounded, coordinator- guarded fan on closing/cancelled + modal-dismiss reveals; self-guards to a no-op on a healthy on-window reveal. Verified (sim 6590BA99, push->pop(button) x3): header 6503/2245/7752 + "Native Detail" title on every push (no bar corruption); Home content present after every pop (permanent strand blank GONE); no crash. KNOWN RESIDUAL: buried-screen content does not SLIDE during the pop (nav bar cross-fades; content snaps in at transition end via the re-host) — a pre-existing detached-children hosting limitation (catalog #3/#5, present in tip + reverted builds), left for a follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
… slot race) Two low-risk hardenings so a custom `headerTitle` titleView is never silently dropped: - configureHeaderTitleView now nudges the navigation bar (setNeedsLayout, plus layoutIfNeeded when the stack is settled) after installing/changing a titleView, so a titleView installed on an ALREADY-DISPLAYED navigation item is measured and drawn (iOS does not lay out a late titleView on its own). - the screen host `mounted` re-applies header slots when a slot already exists for the screen, covering the race where a slot host's `hostReady` fired before `mounted` registered the screen (so the earlier `applyHeaderSlots` no-oped). Verified: "Native Detail" title present on cold first push and every subsequent push after a button-pop; no regression to header chrome (6503/2245/7752) or content-present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
…aused a stall Verified on-sim that the button-pop content strand is architectural (the revealed detached-children view is off-window regardless of frame-stamp timing), so the reveal re-host — not F — is what fixes it. Meanwhile F's finishTransition force-layout (bustLayoutMemos + a forced whole-stack fill on EVERY transition) plus its coordinator-gated layout added main-thread work that regressed the anim-none steady-stall gate (baseline smax 121ms -> 279ms > 250ms budget). Removing F drops it back under budget (smax 201ms) with the content fix fully intact (header 6503/2245/7752 + "Native Detail" title on every push, content present after every pop). No F benefit was lost. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
…gate audit New CORE scenario `header-after-button-pop` (maxAttempts:1): the exact RED repro — push Detail -> pop (goBack) -> push x3. Asserts, in-app, content-present after EVERY push and pop (probeContentPresent — §5 gate 5); host-side, that the custom "Native Detail" title AND the right header action ARE present on the 2nd + 3rd push and the re-push after the cycles (§5 gates 1/3 — the RED bug rendered chrome 0/0/0 here); and that the transition-coordinator gate never bypassed mid-transition (stackGateBypassed == 0 — §5 gate 4, the UIKit-parity invariant that replaces the retired strand* heal counters). probeHealCounters now also reads the new gate counters (stackGateBypassed / stackGateDeferred / revealRehostFired); the reveal-sequence terminal audit asserts gateBypassed == 0 instead of the removed strandHealFired. Gate 2 (pop content ANIMATES >=3 positions) is intentionally NOT added as a passing gate: the detached-children content does not slide during a pop on this runtime (the nav bar cross-fades, content snaps in at transition end via the re-host) — a pre-existing architectural limitation, not a regression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
…tack content (+22pt) Catalog #4 attributed a +22pt (65-66px @3x) extra TOP content inset to the native-stack transparent-header path (Home titleTop 490 vs upstream 425, Detail 491 vs 425). It is NOT an engine bug. Raw a11y frames from BOTH apps on the same iPhone 16 Pro show the scroll content container at y=116pt in BOTH the port and upstream `nativescript-uikit-demo-original` — identical `contentInsetAdjustment` result; the engine's transparent-header inset is already pixel-exact. The whole divergence is the harness's own content-presence probes: the port demo places `<View accessibilityLabel="itest {home,detail} root" style={{width:6, height:6}}/>` as the FIRST child of each screen's ScrollView. In the flex flow it consumed a row plus the content `gap:16`, pushing the visible content 6+16 = 22pt below upstream (which has no such probe). Making the root probes `position:'absolute'` keeps them in the a11y tree (found + on-window + 6x6) while removing them from layout, so the first in-flow child is the headerBlock again. Verified on the booted iPhone 16 Pro: Home titleTop 490 -> 424, Detail 491 -> 425 (upstream 425); both content titles land at frame y=134 == upstream. The engine, UIKit tab, modal page-sheet, and non-transparent-header paths are untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
On EVERY React-Nav tab activation the port re-ran a full screen appearance "opening" transition while the content was already on screen. Profiled on the sim, the sequence was: tabSelected -> (native-driven willShow marks a fresh opening transition at +3ms) -> ~600-2600ms with the main thread IDLE waiting for UIKit's didShow -> transitionEnd. react-navigation therefore saw the tab stuck "opening" for up to ~2.6s per switch (measured dispatch->transitionEnd [849,1506,2589,1240,2162,1440]ms). Upstream re-selects a tab by unhiding a persistent UINavigationController (O(1), ~80-120ms). The willShow delegate could not tell a real navigation from a bare re-appearance: when a tab is unhidden UIKit runs an appearance cycle that fires willShow for the already-top view controller, and the engine marked it as an opening transition and then dwelled on the lagging didShow. Fix: in the native-driven willShow path (a JS push/pop or an interactive gesture-begin already returned above it) detect a bare re-appearance -- not closing, the incoming screen is already the top of an UNCHANGED, previously-appeared stack -- and emit the transitionStart/End pair IMMEDIATELY without marking the stack transitioning, so the trailing didShow settles as a no-op. A real push/pop (identity change), a first mount (never appeared) and an interactive cancel (latched above) all skip it. Result: per-activation dispatch->transitionEnd collapses to ~3ms for repeated (warm) re-selections; the first cold re-reveal round-trip is ~150-263ms (tabs-snapshot cold path, one-time). The engine work at didShow is ~2ms (layout 1ms + config 2ms) -- the old cost was purely the willShow->didShow dwell. Harness: tab-switch-storm now records each activation's dispatch->transitionEnd (a `tabSelected` marker in App.tsx + the demo screens' existing transitionEnd events) and GATES the repeated (warm) re-selections < 250ms -- the user's "switching tabs hangs" complaint. The 3000ms main-thread stall tolerance masked this per-activation dwell; the transition-idle gate is now the sharp detector. Not applied: the memory-prescribed watchdog CommonModes change. It is moot now that the dwell is gone (nothing to starve the display link on) and empirically REGRESSED core (tabs, back-swipe newly FAILed MAIN_THREAD_STALL_STEADY by surfacing simulator display-link artifacts during ordinary UIKit transitions as steady stalls) without reducing starvation (link still starved, ticks~=395). The JS-thread transition-idle gate replaces its intent. Verified: jest 238/238, rnav 19/19, itest core 12/12, tab-switch-storm PASS (warm ~3ms), reveal-sequence + cancel-swipe-wedge PASS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaVDuTDBj6oz3yfnT3yMhn
…ing a pop The port did a BARE popViewControllerAnimated in animateStackPop. On this runtime UIKit then left the revealed VC's view off-window for the whole ~350ms pop; the engine only re-hosted it at transitionEnd via armRevealRehostFan, so the buried screen's content SNAPPED IN instead of sliding (and an edge-swipe had nothing to dim/parallax). Upstream RNS does reseat-then-pop (RNSScreenStack.mm:652-653): setViewControllers(new, animated:NO) THEN popViewControllerAnimated(YES) in the same turn — the non-animated same-array reseat rebuilds hosting BEFORE the animation. Mirror that: reseatControllersBeforePop() runs the proven-safe same-array setViewControllers non-animated reseat (the same primitive armRevealRehostFan already uses, bar-sync-preserving) before the animated pop, in all three pop branches (single-step, pop-to-root, pop-to-controller), scoped to animated pops. Runs pre-transition (coordinator null — §4C gate). Result: over button-pops the armRevealRehostFan heal is no longer needed (revealRehostFired stays 0) — the pop is now a genuine UIKit parallax pop with the content on-window from the first frame. header-after-button-pop gate stays PASS, coordinator gate never bypassed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a presentation-layer pop-slide sampler + the pop-slide scenario (the `slide` suite). Over 10 push/button-pop cycles it arms a CADisplayLink (the proven display-link-spike primitive) that each frame DFS-locks the revealed content root the instant UIKit hosts it, then reads its WINDOW-space origin.x via the PRESENTATION layers (convertPoint:fromLayer: across presentation copies) — the mid-animation position Core Animation is actually drawing. Asserts, per pop: the revealed content is on-window throughout (no strand), sweeps across >=3 distinct intermediate positions with >=20pt monotonic travel to rest; and, in aggregate: revealRehostFired delta == 0 (the transitionEnd heal is no longer needed — reseat-then-pop rebuilt hosting BEFORE the animation), coordinator gate never bypassed, strands == 0. Result on Stage 1: slides=10/10, strands=0, rehost+0, bypass+0 — each pop ~12 distinct positions, startX ~-73pt (parallax from the left) monotonic (inv=0) to endX=0, on-window ~205/205 frames. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ayout silence) The stock left-edge interactive back-swipe is UIKit-initiated and never hits animateStackPop, so reseat-then-pop (Stage 1) does not cover it. It stranded the revealed screen off-window for the whole scrub (blank, no dim/parallax), healed only at transitionEnd. Root = two mid-transition layout writes on the CLOSING nav view, one native + one JS, that re-strand the revealed view each frame: - native: pushAdoptedSizeFeedbackIfNeeded runs every frame via layoutSubviews and force-re-lays nav.view. Skip that trailing [nav.view setNeedsLayout] when nav.transitionCoordinator != nil (keep the shadow-tree size push). The §4F JS-side layout silence never covered this native path. - JS: the reveal didShow fires while the interactive coordinator is still live; its layoutNavigationStackViews is the same mid-transition write. Defer it when transitionCoordinator != null (park stackPendingRevealLayout) and re-run it from the reveal-rehost fan the instant the coordinator clears. A programmatic push/pop reaches didShow with the coordinator already gone, so it lays out immediately as before — button-pop / anim-none paths unchanged. Result: edge-swipe-slide gate = slides 5/5, strands 0, rehost 0 — the revealed screen sweeps ~121pt of native parallax across 44-46 distinct on-window positions (monotonic) during the swipe. pop-slide stays 10/10. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ive reveal slides Adds the edge-swipe-slide scenario: 5 push + host left-edge back-swipe cycles, arming the presentation-layer sampler during each swipe. Asserts the revealed content is on-window throughout (no strand) and sweeps >=3 distinct window-x positions with monotonic parallax travel. Registered in the slide suite next to pop-slide. Verifies STAGE 2 (edge-swipe mid-transition layout silence): slides 5/5, strands 0, ~121pt travel, 44-46 distinct positions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DjDeveloperr
marked this pull request as ready for review
August 5, 2026 20:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
React Native UIKit runtime primitives
Adds the runtime primitives that let React Native drive real UIKit from NativeScript, plus a thin
@nativescript/react-native-screensadapter built on stock@react-navigation/native-stack.What's in it
@nativescript/react-native-screens— a single-engine adapter that drives a realUINavigationController, with react-native-screens parity: native stack, headers, modals & sheets, transitions, and gestures — behaving like upstream RNS.Tests
Stacked on top of
refactor(#43).