diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b0a5aa548..6d62dbebf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -53,5 +53,6 @@ jobs:
IOS_TEST_TIMEOUT_MS: "600000"
IOS_TEST_INACTIVITY_TIMEOUT_MS: "180000"
IOS_LOG_JUNIT: "1"
+ IOS_TEST_VERBOSE_SPECS: "1"
IOS_SIMCTL_QUERY_TIMEOUT_MS: "10000"
run: npm run test:ios
diff --git a/.oracle-context/ns-rns-first-paint/demo-react-navigation.tsx b/.oracle-context/ns-rns-first-paint/demo-react-navigation.tsx
new file mode 100644
index 000000000..fa5fd5671
--- /dev/null
+++ b/.oracle-context/ns-rns-first-paint/demo-react-navigation.tsx
@@ -0,0 +1,388 @@
+ const [transition, setTransition] = useState("idle");
+
+ useEffect(() => {
+ const start = navigation.addListener("transitionStart", (event) => {
+ logReactNavTiming(
+ `home-transition-start-${event.data.closing ? "closing" : "opening"}`,
+ );
+ setTransition(event.data.closing ? "closing" : "opening");
+ });
+ const end = navigation.addListener("transitionEnd", (event) => {
+ logReactNavTiming(
+ `home-transition-end-${event.data.closing ? "closing" : "opening"}`,
+ );
+ dumpNativeScriptRNSGeometry(
+ `home-transition-end-${event.data.closing ? "closing" : "opening"}`,
+ );
+ setTransition(event.data.closing ? "closed" : "open");
+ });
+
+ return () => {
+ start();
+ end();
+ };
+ }, [navigation]);
+
+ const pushDetail = useCallback(() => {
+ logReactNavTiming("push-onPress");
+ navigation.push("Detail", { serial: Date.now() % 10000 });
+ scheduleTraceExport("push", 1200);
+ }, [navigation]);
+
+ const presentModal = useCallback(() => {
+ logReactNavTiming("modal-present-onPress");
+ navigation.navigate("Modal");
+ scheduleTraceExport("modal-present", 1200);
+ }, [navigation]);
+
+ return (
+
+
+
+ RNS native-stack parity
+
+
+ {buildDescription}
+
+
+
+
+
+ NativeScript port path
+
+
+ {rows.map(([label, value]) => (
+
+
+ {label}
+
+
+ {value}
+
+
+ ))}
+
+
+
+
+ Push Detail
+
+
+
+ Present Modal Route
+
+
+
+
+
+ {transition}
+
+
+ transition event
+
+
+
+
+ {headerPing.count}
+
+
+ header pings
+
+
+
+
+ 26
+
+
+ iOS target
+
+
+
+
+ {headerPing.menuCount}
+
+
+ menu actions
+
+
+
+
+
+
+ Scroll coverage
+
+
+ This lower card is here to verify the route uses the full tab height.
+ The scroll view should move under the native navigation header and tab
+ bar instead of stopping halfway down the screen.
+
+
+
+ );
+}
+
+function DetailScreen({ navigation, route }: DetailProps) {
+ const headerPing = useContext(HeaderPingContext);
+
+ useEffect(() => {
+ const start = navigation.addListener("transitionStart", (event) => {
+ logReactNavTiming(
+ `detail-transition-start-${event.data.closing ? "closing" : "opening"}`,
+ );
+ });
+ const end = navigation.addListener("transitionEnd", (event) => {
+ logReactNavTiming(
+ `detail-transition-end-${event.data.closing ? "closing" : "opening"}`,
+ );
+ dumpNativeScriptRNSGeometry(
+ `detail-transition-end-${event.data.closing ? "closing" : "opening"}`,
+ );
+ });
+
+ return () => {
+ start();
+ end();
+ };
+ }, [navigation]);
+
+ return (
+
+
+
+ Detail route
+
+
+ Pushed with navigation.push. The visible back button is the system
+ UIKit button in minimal display mode, and the edge swipe should pop
+ this route.
+
+
+
+
+
+ Route params
+
+
+ serial = {route.params.serial}
+
+
+
+
+
+
+
+
+
+
+ custom header taps = {headerPing.customCount}
+
+
+ This count is driven by a React Navigation headerRight React element
+ hosted in a UIKit UIBarButtonItem customView.
+
+
+
+ {
+ logReactNavTiming("pop-onPress");
+ navigation.goBack();
+ scheduleTraceExport("pop", 1200);
+ }}
+ style={styles.primaryButton}
+ >
+ Pop Detail
+
+
+
+
+ Repeated tap guard
+
+
+ UIKit owns the transition. React Navigation receives the completed
+ native stack after the push or pop animation settles.
+
+
+
+ );
+}
+
+function DetailHeaderAction() {
+ const headerPing = useContext(HeaderPingContext);
+
+ return (
+ {
+ logReactNavTiming("custom-header-onPress");
+ headerPing.incrementCustomHeader();
+ }}
+ style={styles.customHeaderButton}
+ >
+
+ {headerPing.customCount > 0 ? headerPing.customCount : "Tap"}
+
+
+ );
+}
+
+function DetailHeaderTitle() {
+ return (
+
+ Native Detail
+
+ );
+}
+
+function ModalScreen({ navigation }: ModalProps) {
+ useEffect(() => {
+ const start = navigation.addListener("transitionStart", (event) => {
+ logReactNavTiming(
+ `modal-transition-start-${event.data.closing ? "closing" : "opening"}`,
+ );
+ });
+ const end = navigation.addListener("transitionEnd", (event) => {
+ logReactNavTiming(
+ `modal-transition-end-${event.data.closing ? "closing" : "opening"}`,
+ );
+ });
+
+ return () => {
+ start();
+ end();
+ };
+ }, [navigation]);
+
+ const dismissModal = useCallback(() => {
+ logReactNavTiming("modal-dismiss-onPress");
+ navigation.goBack();
+ scheduleTraceExport("modal-dismiss", 1200);
+ }, [navigation]);
+
+ return (
+
+
+
+ Modal route
+
+
+ This route uses React Navigation presentation options. It is included
+ to keep checking modal stack semantics in the NativeScript RNS port.
+
+
+
+
+ UIKit presentation
+
+
+ This screen is presented by UIKit with React Navigation state still
+ driving the route lifecycle. Dismissal should animate downward instead
+ of popping sideways.
+
+
+
+ Dismiss Modal
+
+
+ );
+}
+
+export default function ReactNavigationTab() {
+ const [headerPingCount, setHeaderPingCount] = useState(0);
+ const [customHeaderCount, setCustomHeaderCount] = useState(0);
+ const [headerMenuCount, setHeaderMenuCount] = useState(0);
+
+ const incrementHeaderPing = useCallback(() => {
+ logReactNavTiming("header-ping-onPress");
+ setHeaderPingCount((count) => count + 1);
+ }, []);
+
+ const incrementCustomHeader = useCallback(() => {
+ setCustomHeaderCount((count) => count + 1);
+ }, []);
+
+ const incrementHeaderMenu = useCallback(() => {
+ logReactNavTiming("header-menu-onPress");
+ setHeaderMenuCount((count) => count + 1);
+ }, []);
+
+ const headerPingContext = useMemo(
+ () => ({
+ count: headerPingCount,
+ customCount: customHeaderCount,
+ menuCount: headerMenuCount,
+ incrementCustomHeader,
+ }),
+ [customHeaderCount, headerMenuCount, headerPingCount, incrementCustomHeader],
+ );
+
+ return (
+
+
+
+
+ [
+ {
+ type: "button",
+ label:
diff --git a/.oracle-context/ns-rns-first-paint/host-ready-layout-transition.ts b/.oracle-context/ns-rns-first-paint/host-ready-layout-transition.ts
new file mode 100644
index 000000000..95b3d05ec
--- /dev/null
+++ b/.oracle-context/ns-rns-first-paint/host-ready-layout-transition.ts
@@ -0,0 +1,1509 @@
+ : [];
+ const activeStackKey = stackId ? idsKey(activeStackIds) : undefined;
+ const hasPendingNativeStackModelBeforeReady =
+ !!stackId &&
+ (registry.stackPendingReconcileKeys[stackId] != null ||
+ registry.stackNativeKeys[stackId] !== activeStackKey);
+ const ctx = stackId ? registry.stackContexts?.[stackId] : undefined;
+ const props = registry.screenProps?.[screenId];
+ const isModalScreen = isModalPresentation(props?.stackPresentation);
+ if (
+ previousHandle === contentWrapperViewHandle &&
+ previousReadyKey === readyKey &&
+ wasContentReady &&
+ !hasPendingNativeStackModelBeforeReady
+ ) {
+ return false;
+ }
+ let contentWrapperView = screenContentWrapperViewFromHostReadyHandle(
+ screenId,
+ contentWrapperViewHandle,
+ registry,
+ );
+ if (!contentWrapperView) {
+ const registeredContentWrapperView =
+ registry.screenContentWrapperViews?.[screenId];
+ const screenView = screenControllerView(registry.screens?.[screenId]);
+ if (
+ registeredContentWrapperView &&
+ (!screenView ||
+ contentWrapperIsMountedInScreenView(
+ registeredContentWrapperView,
+ screenView,
+ ) ||
+ screenContentWrapperCanMountInScreenView(
+ screenId,
+ registry,
+ screenView,
+ registeredContentWrapperView,
+ ))
+ ) {
+ contentWrapperView = registeredContentWrapperView;
+ }
+ }
+ traceWorkletEvent(
+ 'content-wrapper-host-ready-resolved',
+ `screen=${screenId} handle=${contentWrapperViewHandle} resolved=${
+ nativeObjectStableHandle(contentWrapperView) ?? ''
+ } registered=${
+ nativeObjectStableHandle(
+ registry.screenContentWrapperViews?.[screenId],
+ ) ?? ''
+ } window=${windowAttached === true ? 1 : 0} descendants=${
+ visibleDescendantCount ?? 0
+ }`,
+ );
+ if (
+ contentWrapperView &&
+ shouldIgnoreStaleOffWindowContentWrapperHostReady(
+ screenId,
+ registry,
+ contentWrapperView,
+ windowAttached,
+ )
+ ) {
+ traceWorkletEvent(
+ 'content-wrapper-host-ready-ignore-offwindow-stale',
+ `screen=${screenId} stack=${stackId ?? ''} descendants=${
+ visibleDescendantCount ?? 0
+ }`,
+ );
+ return false;
+ }
+ if (contentWrapperView) {
+ // NATIVESCRIPT_PORT_DEVIATION: upstream receives the
+ // RNSScreenContentWrapper component view directly from Fabric mount
+ // callbacks. NativeScript host-ready crosses the React boundary as a
+ // generic native handle that can also represent the RNSScreen controller
+ // host, so only the dedicated ScreenContentWrapper UIView may certify
+ // content-ready before a push.
+ const contentWrapperViews =
+ registry.screenContentWrapperViews ??
+ (registry.screenContentWrapperViews = {});
+ const hostHandles =
+ registry.screenContentWrapperHostHandles ??
+ (registry.screenContentWrapperHostHandles = {});
+ const hostReadyKeys =
+ registry.screenContentWrapperHostReadyKeys ??
+ (registry.screenContentWrapperHostReadyKeys = {});
+ const visibleDescendantCounts =
+ registry.screenContentWrapperVisibleDescendantCounts ??
+ (registry.screenContentWrapperVisibleDescendantCounts = {});
+ contentWrapperViews[screenId] = contentWrapperView;
+ hostHandles[screenId] =
+ nativeObjectStableHandle(contentWrapperView) ?? contentWrapperViewHandle;
+ hostReadyKeys[screenId] = readyKey;
+ visibleDescendantCounts[screenId] = visibleDescendantCount ?? 0;
+ const controllerView = screenControllerView(registry.screens?.[screenId]);
+ if (
+ (visibleDescendantCount ?? 0) > 0 &&
+ screenContentWrapperMatchesMountedFabricChild(
+ screenId,
+ registry,
+ contentWrapperView,
+ )
+ ) {
+ rememberCommittedScreenContentWrapper(
+ screenId,
+ registry,
+ contentWrapperView,
+ );
+ }
+ if (controllerView) {
+ rememberScreenHostHandleForView(screenId, registry, controllerView);
+ }
+ }
+ traceWorkletEvent(
+ 'content-wrapper-host-ready',
+ `screen=${screenId} stack=${stackId ?? ''} modal=${
+ isModalScreen ? 1 : 0
+ } ready=${contentWrapperView ? 1 : 0} previousReady=${
+ wasContentReady ? 1 : 0
+ } descendants=${visibleDescendantCount ?? 0} window=${
+ windowAttached === true ? 1 : 0
+ }`,
+ );
+ const isContentReady = screenCanCertifyContentReady(screenId, registry);
+ const stableCommittedContentRemainsReady = !!(
+ !isContentReady &&
+ wasContentReady &&
+ previousHandle === contentWrapperViewHandle &&
+ contentWrapperView &&
+ screenHasCurrentStableReadyExposure(
+ screenId,
+ registry,
+ screenControllerView(registry.screens?.[screenId]),
+ )
+ );
+ if (isContentReady || stableCommittedContentRemainsReady) {
+ registry.screenContentReady[screenId] = true;
+ if (windowAttached === true) {
+ registry.screenContentWrapperEverWindowAttached[screenId] = true;
+ }
+ } else {
+ registry.screenContentReady[screenId] = undefined;
+ }
+ const isActiveNativeStackTopScreen =
+ !!stackId &&
+ !isModalScreen &&
+ activeStackIds[activeStackIds.length - 1] === screenId;
+ const canStartNativeStackUpdateFromWrapper =
+ isActiveNativeStackTopScreen &&
+ stackTopScreenCanStartNativeUpdate(activeStackIds, registry, stackId);
+ const canUseScreenContent =
+ isContentReady || stableCommittedContentRemainsReady;
+ if (
+ canUseScreenContent &&
+ stackId &&
+ !isModalScreen &&
+ registry.stackNativeKeys[stackId] === activeStackKey &&
+ registry.stackPendingReconcileKeys[stackId] === activeStackKey
+ ) {
+ registry.stackPendingReconcileKeys[stackId] = undefined;
+ traceWorkletEvent(
+ 'content-wrapper-host-ready-clear-satisfied-pending',
+ `screen=${screenId} stack=${stackId} active=${activeStackKey ?? ''}`,
+ );
+ }
+ const hasPendingNativeStackModel =
+ !!stackId &&
+ (registry.stackPendingReconcileKeys[stackId] != null ||
+ registry.stackNativeKeys[stackId] !== activeStackKey);
+ const hasPendingNativeStackTopModel =
+ hasPendingNativeStackModel && !isModalScreen;
+ const shouldResumePendingNativeStackFromWindowedWrapper =
+ windowAttached === true &&
+ canUseScreenContent &&
+ isActiveNativeStackTopScreen &&
+ hasPendingNativeStackTopModel &&
+ !!ctx &&
+ !!stackId &&
+ registry.stackTransitioning[stackId] !== true;
+ const isFirstWindowAttachAfterOffWindowReady =
+ windowAttached === true &&
+ typeof previousReadyKey === 'string' &&
+ previousReadyKey.endsWith('|offwindow');
+ const currentContentWrapperRefreshKey = contentWrapperView
+ ? screenContentWrapperRefreshKey(
+ screenId,
+ registry,
+ contentWrapperView,
+ false,
+ )
+ : undefined;
+ const previousReadyContentWrapperRefreshKey =
+ contentWrapperView && typeof previousReadyKey === 'string'
+ ? screenContentWrapperRefreshKeyWithHostReadyKey(
+ screenId,
+ registry,
+ contentWrapperView,
+ previousReadyKey,
+ )
+ : undefined;
+ const previousContentWrapperRefreshKey =
+ registry.screenContentWrapperRefreshKeys?.[screenId];
+ const windowedCommittedWrapperHasStaleRefreshKey =
+ windowAttached === true &&
+ wasContentReady &&
+ previousReadyKey != null &&
+ previousReadyKey !== readyKey &&
+ currentContentWrapperRefreshKey != null &&
+ previousContentWrapperRefreshKey != null &&
+ previousContentWrapperRefreshKey !== currentContentWrapperRefreshKey &&
+ previousContentWrapperRefreshKey !== previousReadyContentWrapperRefreshKey;
+ const shouldRefreshWindowedCommittedWrapper =
+ windowAttached === true &&
+ wasContentReady &&
+ previousReadyKey != null &&
+ previousReadyKey !== readyKey &&
+ typeof previousReadyKey === 'string' &&
+ (!previousReadyKey.endsWith('|window') ||
+ windowedCommittedWrapperHasStaleRefreshKey);
+ const shouldSkipWindowedCommittedWrapperRefresh =
+ canUseScreenContent &&
+ wasContentReady &&
+ previousHandle === contentWrapperViewHandle &&
+ previousReadyKey !== readyKey &&
+ windowAttached === true &&
+ !shouldRefreshWindowedCommittedWrapper &&
+ isActiveNativeStackTopScreen &&
+ !isModalScreen &&
+ !!stackId &&
+ registry.stackTransitioning[stackId] !== true;
+ const shouldSkipWindowedCommittedWrapperRefreshDuringTransition =
+ canUseScreenContent &&
+ wasContentReady &&
+ previousHandle === contentWrapperViewHandle &&
+ previousReadyKey !== readyKey &&
+ windowAttached === true &&
+ !shouldRefreshWindowedCommittedWrapper &&
+ !isModalScreen &&
+ !!stackId &&
+ registry.stackTransitioning[stackId] === true &&
+ registry.stackNativeKeys[stackId] === activeStackKey &&
+ registry.stackPendingReconcileKeys[stackId] == null;
+ const shouldSkipReadySameWrapperRefresh =
+ canUseScreenContent &&
+ wasContentReady &&
+ previousHandle === contentWrapperViewHandle &&
+ previousReadyKey !== readyKey &&
+ windowAttached !== true &&
+ isActiveNativeStackTopScreen &&
+ !isModalScreen &&
+ !!stackId &&
+ registry.stackTransitioning[stackId] !== true;
+ const parentPresentedModalId = !isModalScreen
+ ? parentPresentedModalIdForContentScreen(screenId, registry)
+ : undefined;
+ const resolvedContentWrapperViewHandle =
+ contentWrapperView != null
+ ? nativeObjectStableHandle(contentWrapperView) ?? contentWrapperViewHandle
+ : contentWrapperViewHandle;
+ const shouldSkipNoOpNativeStackContainerMutation =
+ contentWrapperHostReadyIsNoOpNativeStackContainerMutation(
+ screenId,
+ stackId,
+ registry,
+ previousHandle,
+ resolvedContentWrapperViewHandle,
+ previousReadyKey,
+ readyKey,
+ windowAttached,
+ );
+ if (shouldSkipNoOpNativeStackContainerMutation) {
+ traceWorkletEvent(
+ 'content-wrapper-host-ready-skip-noop-container-mutation',
+ `screen=${screenId} stack=${stackId ?? ''}`,
+ );
+ return false;
+ }
+ if (
+ shouldSkipReadySameWrapperRefresh ||
+ shouldSkipWindowedCommittedWrapperRefresh ||
+ shouldSkipWindowedCommittedWrapperRefreshDuringTransition
+ ) {
+ if (contentWrapperView) {
+ const refreshKeys =
+ registry.screenContentWrapperRefreshKeys ??
+ (registry.screenContentWrapperRefreshKeys = {});
+ refreshKeys[screenId] = screenContentWrapperRefreshKey(
+ screenId,
+ registry,
+ contentWrapperView,
+ false,
+ );
+ }
+ traceWorkletEvent(
+ 'content-wrapper-host-ready-skip-committed',
+ `screen=${screenId} stack=${stackId ?? ''} resume=${
+ shouldResumePendingNativeStackFromWindowedWrapper ? 1 : 0
+ } pending=${hasPendingNativeStackModel ? 1 : 0} ctx=${
+ ctx ? 1 : 0
+ } activeTop=${isActiveNativeStackTopScreen ? 1 : 0} canUse=${
+ canUseScreenContent ? 1 : 0
+ } native=${
+ stackId ? registry.stackNativeKeys[stackId] ?? '' : ''
+ } active=${activeStackKey ?? ''} pendingKey=${
+ stackId ? registry.stackPendingReconcileKeys[stackId] ?? '' : ''
+ } windowed=${
+ shouldSkipWindowedCommittedWrapperRefresh ? 1 : 0
+ } transitioningWindowed=${
+ shouldSkipWindowedCommittedWrapperRefreshDuringTransition ? 1 : 0
+ } transitioning=${
+ stackId && registry.stackTransitioning[stackId] === true ? 1 : 0
+ }`,
+ );
+ const didReconcileContainingTab =
+ canUseScreenContent && stackId
+ ? reconcileContainingTabControllerFromStackContent(
+ stackId,
+ registry,
+ contentWrapperView,
+ )
+ : false;
+ if (shouldResumePendingNativeStackFromWindowedWrapper) {
+ traceWorkletEvent(
+ 'content-wrapper-host-ready-reconcile-pending',
+ `screen=${screenId} stack=${stackId}`,
+ );
+ refreshStackContainmentFromHostReady(
+ stackId,
+ registry,
+ ctx,
+ registry.stacks[stackId],
+ false,
+ );
+ scheduledReconcileStack(
+ stackId,
+ registry,
+ ctx,
+ registry.stackNativeKeys[stackId] != null,
+ );
+ return true;
+ }
+ return didReconcileContainingTab;
+ }
+ const shouldDeferHostRefresh =
+ contentWrapperView &&
+ canUseScreenContent &&
+ !isFirstWindowAttachAfterOffWindowReady &&
+ shouldDeferReadyContentWrapperRefreshDuringTransition(screenId, registry);
+ const canPrepareOffWindowHostedContentForTabStack =
+ windowAttached !== true &&
+ !!stackId &&
+ !!containingTabBarControllerForController(registry.stacks[stackId]);
+ const shouldSkipHostedRefreshForNativeStackStart =
+ canStartNativeStackUpdateFromWrapper &&
+ !canPrepareOffWindowHostedContentForTabStack &&
+ !shouldRefreshWindowedCommittedWrapper &&
+ !isModalScreen &&
+ !!stackId &&
+ registry.stackTransitioning[stackId] !== true;
+ const modalHeaderSuffix = ':modal-header';
+ const isModalHeaderScreen =
+ screenId.indexOf(modalHeaderSuffix) ===
+ screenId.length - modalHeaderSuffix.length;
+ const shouldSkipOffWindowCommittedWrapperRefresh =
+ canUseScreenContent &&
+ wasContentReady &&
+ previousHandle === contentWrapperViewHandle &&
+ windowAttached !== true &&
+ (!isModalScreen || isModalHeaderScreen) &&
+ !!stackId &&
+ registry.stackTransitioning[stackId] !== true &&
+ !canStartNativeStackUpdateFromWrapper;
+ traceWorkletEvent(
+ 'content-wrapper-host-ready-state',
+ `screen=${screenId} stack=${stackId ?? ''} contentReady=${
+ isContentReady ? 1 : 0
+ } wrapperCanStart=${canStartNativeStackUpdateFromWrapper ? 1 : 0} canUse=${
+ canUseScreenContent ? 1 : 0
+ } ctx=${ctx ? 1 : 0} defer=${shouldDeferHostRefresh ? 1 : 0} skipHosted=${
+ shouldSkipHostedRefreshForNativeStackStart ? 1 : 0
+ } prepareTab=${
+ canPrepareOffWindowHostedContentForTabStack ? 1 : 0
+ } transitioning=${
+ stackId && registry.stackTransitioning[stackId] === true ? 1 : 0
+ }`,
+ );
+ const didCompleteDetachedHeaderDismissal =
+ completePresentedModalDismissalForDetachedHeaderIfNeeded(
+ screenId,
+ stackId,
+ registry,
+ wasContentReady,
+ windowAttached,
+ );
+ if (shouldSkipOffWindowCommittedWrapperRefresh) {
+ if (contentWrapperView) {
+ const refreshKeys =
+ registry.screenContentWrapperRefreshKeys ??
+ (registry.screenContentWrapperRefreshKeys = {});
+ refreshKeys[screenId] = screenContentWrapperRefreshKey(
+ screenId,
+ registry,
+ contentWrapperView,
+ false,
+ );
+ }
+ traceWorkletEvent(
+ 'content-wrapper-host-ready-skip-offwindow-committed',
+ `screen=${screenId} stack=${stackId} modalHeader=${
+ isModalHeaderScreen ? 1 : 0
+ } completedDismiss=${didCompleteDetachedHeaderDismissal ? 1 : 0}`,
+ );
+ return didCompleteDetachedHeaderDismissal;
+ }
+ let didReconcileModalContentStack = false;
+ if (
+ canUseScreenContent &&
+ !wasContentReady &&
+ parentPresentedModalId != null &&
+ windowAttached !== true &&
+ !!stackId &&
+ !!ctx &&
+ registry.stackTransitioning[stackId] !== true &&
+ registry.stackNativeKeys[stackId] !== activeStackKey
+ ) {
+ registry.stackPendingReconcileKeys[stackId] = activeStackKey;
+ traceWorkletEvent(
+ 'content-wrapper-host-ready-modal-content-stack-reconcile',
+ `screen=${screenId} stack=${stackId} modal=${parentPresentedModalId}`,
+ );
+ scheduledReconcileStack(stackId, registry, ctx, true);
+ didReconcileModalContentStack = true;
+ }
+ let didScheduleParentModalStack = false;
+ if (canUseScreenContent && !wasContentReady && stackId) {
+ didScheduleParentModalStack =
+ scheduleParentModalStackReconcileForContentScreen(
+ screenId,
+ registry,
+ windowAttached,
+ );
+ if (didScheduleParentModalStack) {
+ traceWorkletEvent(
+ 'content-wrapper-host-ready-parent-reconcile',
+ );
+ const contentWrapperView = liveScreenContentWrapperViewFromRegistry(
+ screenId,
+ registry,
+ );
+ let didMutate = didNormalizeWrapper;
+ if (controllerView) {
+ controllerView.hidden = false;
+ controllerView.alpha = 1;
+ controllerView.userInteractionEnabled = true;
+ didMutate =
+ normalizeScreenControllerViewFrame(screenId, registry, controllerView) ||
+ didMutate;
+ didMutate = restoreLiveViewInteractionChain(controllerView) || didMutate;
+ didMutate =
+ restoreHostedReactSubviewInteractivity(controllerView) || didMutate;
+ didMutate =
+ disableStaleEmptyScreenHitTargetDescendants(controllerView) || didMutate;
+ }
+ if (contentWrapperView) {
+ contentWrapperView.userInteractionEnabled = true;
+ didMutate =
+ normalizeScreenContentWrapperChildHost(contentWrapperView) || didMutate;
+ didMutate =
+ restoreLiveViewInteractionChain(
+ contentWrapperView,
+ controllerView ?? undefined,
+ ) || didMutate;
+ didMutate =
+ restoreHostedReactSubviewInteractivity(contentWrapperView) || didMutate;
+ didMutate =
+ disableStaleEmptyScreenHitTargetDescendants(contentWrapperView) ||
+ didMutate;
+ }
+ didMutate =
+ refreshScreenContentWrapperTouchHandler(
+ screenId,
+ registry,
+ forceTouchRefresh,
+ ) || didMutate;
+ if (controllerView) {
+ didMutate =
+ refreshScreenSurfaceTouchHandlerIfNeeded(
+ screenId,
+ registry,
+ controllerView,
+ contentWrapperView,
+ forceTouchRefresh,
+ ) || didMutate;
+ }
+ traceWorkletEvent(
+ 'prepare-visible-screen-interaction',
+ `screen=${screenId} did=${didMutate ? 1 : 0} force=${
+ forceTouchRefresh ? 1 : 0
+ }`,
+ );
+ return didMutate;
+}
+export function screenHostedViewNeedsRefresh(
+ controller: any,
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ rootView: any,
+ forceRefresh = false,
+) {
+ 'worklet';
+
+ if (!controller || !rootView) {
+ return false;
+ }
+ if (!screenContentIsReady(screenId, registry)) {
+ return true;
+ }
+ if (
+ screenId &&
+ !screenHasVisibleHostedContent(screenId, registry, rootView)
+ ) {
+ return true;
+ }
+ const refreshKey = screenHostedViewRefreshKey(
+ controller,
+ screenId,
+ registry,
+ rootView,
+ );
+ const previousRefreshKey = screenId
+ ? registry.screenHostRefreshKeys[screenId]
+ : controller.__nativeScriptHostedViewRefreshKey;
+ if (!forceRefresh && previousRefreshKey === refreshKey) {
+ return false;
+ }
+ rememberScreenHostedViewRefreshKey(controller, screenId, registry, rootView);
+ return true;
+}
+export function shouldSkipHostedSubviewRepairDuringNativeTransaction(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry | undefined,
+ rootView: any,
+) {
+ 'worklet';
+
+ if (!screenId || !registry || !rootView) {
+ return false;
+ }
+ const stackId = registry.screenParents?.[screenId];
+ const props = registry.screenProps?.[screenId];
+ if (!stackId || isModalPresentation(props?.stackPresentation)) {
+ return false;
+ }
+ const hasNativeTransaction =
+ registry.stackUpdatingModals?.[stackId] === true ||
+ registry.stackTransitioning?.[stackId] === true;
+ if (!hasNativeTransaction) {
+ return false;
+ }
+ const contentWrapperView = liveScreenContentWrapperViewFromRegistry(
+ screenId,
+ registry,
+ );
+ const hasCommittedWrapperContent =
+ contentWrapperView &&
+ screenContentWrapperHasLiveMountedHostContent(
+ screenId,
+ registry,
+ contentWrapperView,
+ rootView,
+ );
+ const wrapperMountedInRoot =
+ !contentWrapperView ||
+ contentWrapperIsMountedInScreenView(contentWrapperView, rootView);
+ return !!(
+ screenContentIsReady(screenId, registry) &&
+ wrapperMountedInRoot &&
+ (hasCommittedWrapperContent ||
+ screenHasVisibleHostedContent(screenId, registry, rootView))
+ );
+}
+export function layoutHostedReactSubviews(
+ controller: any,
+ rootViewOverride?: any,
+ refreshHostedView = true,
+ repairHostedSubviews = refreshHostedView,
+ registry?: NativeScriptStackRegistry,
+ screenId?: string,
+ forceTouchRefresh = false,
+ forceRootHostWrapperLayout = false,
+) {
+ 'worklet';
+
+ const startedAt = traceTimestamp();
+ const rootView = rootViewOverride ?? screenControllerView(controller);
+ if (!rootView) {
+ return false;
+ }
+ let stepStartedAt = traceTimestamp();
+ if (registry && screenId) {
+ refreshScreenSurfaceTouchHandlerIfNeeded(
+ screenId,
+ registry,
+ rootView,
+ registry.screenContentWrapperViews[screenId],
+ forceTouchRefresh,
+ );
+ } else {
+ refreshScreenViewSurfaceTouchHandler(rootView);
+ }
+ traceSlowWorklet('layoutHosted.refreshTouch', stepStartedAt);
+ rootView.userInteractionEnabled = true;
+ if (
+ shouldSkipHostedSubviewRepairDuringNativeTransaction(
+ screenId,
+ registry,
+ rootView,
+ )
+ ) {
+ traceSlowWorklet(
+ 'layoutHostedReactSubviews.skipNativeTransaction',
+ startedAt,
+ `screen=${screenId ?? ''}`,
+ );
+ return false;
+ }
+ stepStartedAt = traceTimestamp();
+ const shouldRefreshHostedView =
+ refreshHostedView &&
+ (!registry ||
+ screenHostedViewNeedsRefresh(
+ controller,
+ screenId,
+ registry,
+ rootView,
+ forceTouchRefresh,
+ ));
+ const didRefreshHostedViews =
+ shouldRefreshHostedView && refreshKnownUIKitHostView(rootView) === true;
+ if (refreshHostedView) {
+ traceSlowWorklet(
+ 'layoutHosted.refreshHost',
+ stepStartedAt,
+ `attempted=${shouldRefreshHostedView ? 1 : 0}`,
+ );
+ }
+ const subviews = rootView.subviews;
+ const count = arrayCount(subviews);
+ const rootHeight =
+ rootView.bounds?.size?.height ?? rootView.frame?.size?.height ?? 0;
+ disableStaleEmptyScreenHitTargetSiblings(rootView);
+ for (let index = 0; index < count; index += 1) {
+ const subview = arrayItem(subviews, index);
+ if (!subview) {
+ continue;
+ }
+ let didNormalizeSubview = false;
+ const shouldForceRootHostWrapperLayout =
+ forceRootHostWrapperLayout && viewHasVisibleHostedReactContent(subview);
+ if (
+ hostedSubviewMayNeedFill(
+ rootView,
+ subview,
+ shouldForceRootHostWrapperLayout,
+ ) &&
+ shouldFillHostedSubview(
+ rootView,
+ subview,
+ 0,
+ undefined,
+ shouldForceRootHostWrapperLayout,
+ )
+ ) {
+ const targetFrame = rectWithZeroOrigin(rootView.bounds);
+ didNormalizeSubview =
+ setViewFrameIfNeeded(subview, targetFrame) || didNormalizeSubview;
+ didNormalizeSubview =
+ setViewBoundsIfNeeded(
+ subview,
+ boundsRectForFilledHostedSubview(subview, targetFrame),
+ ) || didNormalizeSubview;
+ subview.autoresizingMask = flexibleSizeMask();
+ }
+ if (repairHostedSubviews || didNormalizeSubview) {
+ layoutHostedSubviewChain(
+ subview,
+ 0,
+ rootHeight,
+ [],
+ undefined,
+ 0,
+ didNormalizeSubview ||
+ didRefreshHostedViews ||
+ forceRootHostWrapperLayout,
+ forceRootHostWrapperLayout,
+ );
+ }
+ }
+ traceSlowWorklet(
+ 'layoutHostedReactSubviews',
+ startedAt,
+ `children=${count} refreshed=${didRefreshHostedViews ? 1 : 0}`,
+ );
+ return didRefreshHostedViews && count > 0;
+}
+export const __nativeScriptLayoutHostedReactSubviewsForTests =
+ layoutHostedReactSubviews;
+export function layoutScreenHostedReactSubviews(
+ controller: any,
+ rootViewOverride: any,
+ registry: NativeScriptStackRegistry,
+ screenId: string | undefined,
+ forceRefresh = false,
+ repairHostedSubviews = true,
+) {
+ 'worklet';
+
+ const rootView = rootViewOverride ?? screenControllerView(controller);
+ const hasStableMountedContent =
+ !forceRefresh &&
+ screenHasStableReadyMountedContent(screenId, registry, rootView);
+ if (hasStableMountedContent) {
+ if (rootView) {
+ rootView.userInteractionEnabled = true;
+ restoreHostedReactSubviewInteractivity(rootView);
+ }
+ rememberScreenHostedViewRefreshKey(
+ controller,
+ screenId,
+ registry,
+ rootView,
+ );
+ refreshScreenContentWrapperHost(
+ screenId,
+ registry,
+ false,
+ false,
+ false,
+ true,
+ 'layout-screen-hosted-stable-skip',
+ );
+ refreshScreenContentWrapperTouchHandler(screenId, registry, true);
+ traceWorkletEvent(
+ 'layout-screen-hosted-skip-stable',
+ `screen=${screenId ?? ''}`,
+ );
+ return;
+ }
+ const shouldRefreshHost = screenHostedViewNeedsRefresh(
+ controller,
+ screenId,
+ registry,
+ rootView,
+ forceRefresh,
+ );
+ if (!shouldRefreshHost && !repairHostedSubviews) {
+ if (rootView) {
+ rootView.userInteractionEnabled = true;
+ }
+ return;
+ }
+ const didRefreshHostedContent = layoutHostedReactSubviews(
+ controller,
+ rootView,
+ shouldRefreshHost,
+ repairHostedSubviews || shouldRefreshHost,
+ registry,
+ screenId,
+ forceRefresh,
+ );
+ if (
+ shouldRefreshHost &&
+ didRefreshHostedContent &&
+ screenId &&
+ screenCanCertifyContentReady(screenId, registry)
+ ) {
+ registry.screenContentReady[screenId] = true;
+ rememberScreenHostedViewRefreshKey(
+ controller,
+ screenId,
+ registry,
+ rootView,
+ );
+ }
+ if (screenId) {
+ rememberScreenStableReadyExposure(screenId, registry, rootView);
+ }
+}
+export const __nativeScriptScreenHostedViewNeedsRefreshForTests =
+ screenHostedViewNeedsRefresh;
+export const __nativeScriptLayoutScreenHostedReactSubviewsForTests =
+ layoutScreenHostedReactSubviews;
+export function refreshScreenContentReady(
+ screenId: string | undefined,
+ controller: any,
+ registry: NativeScriptStackRegistry,
+ forceRefresh = false,
+) {
+ 'worklet';
+
+ const startedAt = traceTimestamp();
+ if (!screenId) {
+ return false;
+ }
+ if (!forceRefresh && screenContentIsReady(screenId, registry)) {
+ return true;
+ }
+ const contentAlreadyReady = screenContentIsReady(screenId, registry);
+ const controllerView = screenControllerView(controller);
+ const contentWrapperView = registry.screenContentWrapperViews[screenId];
+ const hasContentWrapper = !!contentWrapperView;
+ const shouldRefreshControllerView =
+ !!controllerView &&
+ (forceRefresh ||
+ !contentAlreadyReady ||
+ screenHostedViewNeedsRefresh(
+ controller,
+ screenId,
+ registry,
+ controllerView,
+ forceRefresh,
+ ));
+ const didRefreshControllerView =
+ shouldRefreshControllerView && refreshKnownUIKitHostView(controllerView);
+ if (controllerView && didRefreshControllerView) {
+ layoutHostedReactSubviews(
+ controller,
+ controllerView,
+ false,
+ true,
+ registry,
+ screenId,
+ forceRefresh,
+ );
+ const didRefreshWrapper = refreshScreenContentWrapperHost(
+ screenId,
+ registry,
+ true,
+ forceRefresh,
+ false,
+ false,
+ 'screen-content-ready-controller',
+ );
+ if (screenCanCertifyContentReady(screenId, registry)) {
+ registry.screenContentReady[screenId] = true;
+ rememberScreenHostedViewRefreshKey(
+ controller,
+ screenId,
+ registry,
+ controllerView,
+ );
+ }
+ traceSlowWorklet(
+ 'refreshScreenContentReady-controller',
+ startedAt,
+ `screen=${screenId} force=${forceRefresh}`,
+ );
+ return didRefreshWrapper || didRefreshControllerView;
+ }
+ if (hasContentWrapper) {
+ const didRefreshWrapper = refreshScreenContentWrapperHost(
+ screenId,
+ registry,
+ true,
+ forceRefresh,
+ false,
+ false,
+ 'screen-content-ready-wrapper',
+ );
+ if (screenCanCertifyContentReady(screenId, registry)) {
+ registry.screenContentReady[screenId] = true;
+ if (controllerView) {
+ rememberScreenHostedViewRefreshKey(
+ controller,
+ screenId,
+ registry,
+ controllerView,
+ );
+ }
+ }
+ traceSlowWorklet(
+ 'refreshScreenContentReady-wrapper',
+ startedAt,
+ `screen=${screenId} force=${forceRefresh}`,
+ );
+ return didRefreshWrapper || screenContentIsReady(screenId, registry);
+ }
+ if (controllerView && screenCanCertifyContentReady(screenId, registry)) {
+ registry.screenContentReady[screenId] = true;
+ rememberScreenHostedViewRefreshKey(
+ controller,
+ screenId,
+ registry,
+ controllerView,
+ );
+ traceSlowWorklet(
+ 'refreshScreenContentReady-controller-visible',
+ startedAt,
+ `screen=${screenId} force=${forceRefresh}`,
+ );
+ return true;
+ }
+ traceSlowWorklet(
+ 'refreshScreenContentReady-empty',
+ startedAt,
+ `screen=${screenId} force=${forceRefresh}`,
+ );
+ return false;
+}
+export const __nativeScriptRefreshScreenContentReadyForTests =
+ refreshScreenContentReady;
+export function prepareScreenHostedContentForNativeTransition(
+ controller: any,
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+) {
+ 'worklet';
+
+ const startedAt = traceTimestamp();
+ const controllerView = screenControllerView(controller);
+ if (!screenId || !controllerView) {
+ return false;
+ }
+ const contentWrapperView = registry.screenContentWrapperViews[screenId];
+ const wrapperAlreadyMounted = contentWrapperIsMountedInScreenView(
+ contentWrapperView,
+ controllerView,
+ );
+ const contentIsReady = screenContentIsReady(screenId, registry);
+ const hasCommittedNativeStackContent =
+ screenHasCommittedContentForNativeStack(screenId, registry, controllerView);
+ const alreadyPreparedCommittedContent =
+ contentIsReady &&
+ (!contentWrapperView || wrapperAlreadyMounted) &&
+ hasCommittedNativeStackContent;
+ if (alreadyPreparedCommittedContent) {
+ restoreHostedReactSubviewInteractivity(controllerView);
+ refreshScreenSurfaceTouchHandlerIfNeeded(
+ screenId,
+ registry,
+ controllerView,
+ contentWrapperView,
+ true,
+ false,
+ true,
+ );
+ traceSlowWorklet(
+ 'prepareScreenHostedContentForNativeTransition.fast',
+ startedAt,
+ `screen=${screenId}`,
+ );
+ return true;
+ }
+ traceWorkletEvent(
+ 'prepareScreenHostedContentForNativeTransition-miss',
+ `screen=${screenId} ready=${contentIsReady ? 1 : 0} wrapper=${
+ contentWrapperView ? 1 : 0
+ } mounted=${wrapperAlreadyMounted ? 1 : 0} committed=${
+ hasCommittedNativeStackContent ? 1 : 0
+ }`,
+ );
+ refreshScreenContentReady(screenId, controller, registry, true);
+ const didNormalizeWrapper = ensureScreenContentWrapperMounted(
+ screenId,
+ registry,
+ );
+ layoutHostedReactSubviews(
+ controller,
+ controllerView,
+ false,
+ true,
+ registry,
+ screenId,
+ didNormalizeWrapper,
+ );
+ const availableHeight =
+ controllerView.bounds?.size?.height ??
+ controllerView.frame?.size?.height ??
+ contentWrapperView?.bounds?.size?.height ??
+ contentWrapperView?.frame?.size?.height ??
+ 0;
+ if (contentWrapperView) {
+ layoutHostedSubviewChain(
+ contentWrapperView,
+ 0,
+ availableHeight,
+ [],
+ undefined,
+ 0,
+ true,
+ );
+ contentWrapperView.setNeedsLayout?.();
+ contentWrapperView.layoutIfNeeded?.();
+ }
+ controllerView.setNeedsLayout?.();
+ controllerView.layoutIfNeeded?.();
+ restoreHostedReactSubviewInteractivity(controllerView);
+ refreshScreenSurfaceTouchHandlerIfNeeded(
+ screenId,
+ registry,
+ controllerView,
+ registry.screenContentWrapperViews[screenId],
+ true,
+ false,
+ true,
+ );
+ const hasVisibleHostedContent = screenHasVisibleHostedContent(
+ screenId,
+ registry,
+ controllerView,
+ );
+ traceSlowWorklet(
+ 'prepareScreenHostedContentForNativeTransition',
+ startedAt,
+ `screen=${screenId} visible=${hasVisibleHostedContent ? 1 : 0}`,
+ );
+ return hasVisibleHostedContent;
+}
+export const __nativeScriptPrepareScreenHostedContentForNativeTransitionForTests =
+ prepareScreenHostedContentForNativeTransition;
+export function prepareRevealedScreenForNativePop(
+ screenId: string | undefined,
+ controller: any,
+ registry: NativeScriptStackRegistry,
+) {
+ 'worklet';
+
+ const startedAt = traceTimestamp();
+ if (!screenId || !controller) {
+ return false;
+ }
+ const didRestoreHostView = restoreScreenControllerViewFromHostHandle(
+ controller,
+ screenId,
+ registry,
+ );
+ const controllerView = screenControllerView(controller);
+ if (controllerView) {
+ controllerView.hidden = false;
+ controllerView.alpha = 1;
+ controllerView.userInteractionEnabled = true;
+ controllerView.autoresizingMask = flexibleSizeMask();
+ normalizeScreenControllerViewFrame(screenId, registry, controllerView);
+ }
+
+ // Upstream RNS enters pop with the revealed RNSScreen.view already hosting
+ // its React content. NativeScript can keep that view behind a stable handle
+ // while UIKit temporarily owns a wrapper/snapshot, so reassert the same
+ // invariant before handing the controller array to UINavigationController.
+ const didPrepareContent = prepareScreenHostedContentForNativeTransition(
+ controller,
+ screenId,
+ registry,
+ );
+ const restoredControllerView = screenControllerView(controller);
+ const hasVisibleHostedContent = screenHasVisibleHostedContent(
+ screenId,
+ registry,
+ restoredControllerView,
+ );
+ const didCertifyContentReady =
+ hasVisibleHostedContent && screenCanCertifyContentReady(screenId, registry);
+ if (didCertifyContentReady) {
+ registry.screenContentReady[screenId] = true;
+ rememberScreenStableReadyExposure(
+ screenId,
+ registry,
+ restoredControllerView,
+ );
+ }
+ const scrollView = findScrollViewInFirstDescendantChainFrom(controllerView);
+ if (!scrollView) {
+ return;
+ }
+ const registry = getRegistry(globalThis as Record);
+ const screenId = directScreenIdForController(controller);
+ const injectedTop = screenId
+ ? registry.screenScrollInsetTops[screenId] ?? 0
+ : controller.__rnsNativeScriptNavigationBarContentInsetTop ?? 0;
+ const existingContentInset = scrollView.contentInset;
+ const baseTop = Math.max(
+ 0,
+ edgeInsetsValue(existingContentInset, 'top') - injectedTop,
+ );
+ const controllerSafeAreaTop = edgeInsetsValue(
+ controllerView?.safeAreaInsets,
+ 'top',
+ );
+ if (
+ targetTop < 0.5 &&
+ injectedTop < 0.5 &&
+ controllerSafeAreaTop >= 0.5 &&
+ Math.abs(
+ edgeInsetsValue(existingContentInset, 'top') - controllerSafeAreaTop,
+ ) < 0.5
+ ) {
+ // NATIVESCRIPT_PORT_DEVIATION: upstream never writes a manual
+ // UIScrollView.contentInset before the screen is parented. The TS port can
+ // briefly lay out a stack before UIKit containment is repaired; once UIKit
+ // supplies the safe area, clear that pre-parent manual top inset so
+ // contentInsetAdjustmentBehavior does not double it.
+ const existingOffset = scrollView.contentOffset;
+ if (screenId) {
+ registry.screenScrollInsetTops[screenId] = 0;
+ } else {
+ controller.__rnsNativeScriptNavigationBarContentInsetTop = 0;
+ }
+ scrollView.contentInset = edgeInsetsReplacingTop(existingContentInset, 0);
+ if (scrollView.scrollIndicatorInsets) {
+ scrollView.scrollIndicatorInsets = edgeInsetsReplacingTop(
+ scrollView.scrollIndicatorInsets,
+ 0,
+ );
+ }
+ scrollView.contentOffset = pointReplacingY(
+ existingOffset,
+ (existingOffset?.y ?? 0) + controllerSafeAreaTop,
+ );
+ return;
+ }
+ const nextTop = baseTop + targetTop;
+ if (Math.abs(nextTop - edgeInsetsValue(existingContentInset, 'top')) < 0.5) {
+ return;
+ }
+
+ // NATIVESCRIPT_PORT_DEVIATION: upstream does not need a cleanup path here
+ // because RNSScreen is parented before UIKit computes ScrollView safe-area
+ // insets. The TS port may briefly have a pre-parent manual inset from an
+ // earlier host refresh, so keep this helper generic but pass targetTop=0 for
+ // header safe-area propagation; UIKit's adjustedContentInset remains the
+ // source of truth once containment is repaired.
+ const shouldCompensateContentOffset =
+ Math.abs(targetTop - injectedTop) >= 0.5;
+ const offsetDelta = shouldCompensateContentOffset
+ ? nextTop - edgeInsetsValue(existingContentInset, 'top')
+ : 0;
+ const existingOffset = scrollView.contentOffset;
+ if (screenId) {
+ registry.screenScrollInsetTops[screenId] = targetTop;
+ } else {
+ controller.__rnsNativeScriptNavigationBarContentInsetTop = targetTop;
+ }
+ scrollView.contentInset = edgeInsetsReplacingTop(
+ existingContentInset,
+ nextTop,
+ );
+ if (scrollView.scrollIndicatorInsets) {
+ scrollView.scrollIndicatorInsets = edgeInsetsReplacingTop(
+ scrollView.scrollIndicatorInsets,
+ nextTop,
+ );
+ }
+ scrollView.contentOffset = pointReplacingY(
+ existingOffset,
+ (existingOffset?.y ?? 0) - offsetDelta,
+ );
+}
+export function navigationControllerIsPresentedModally(
+ navigationController: any,
+) {
+ 'worklet';
+
+ return (
+ navigationController?.presentingViewController != null ||
+ navigationController?.presentationController != null
+ );
+}
+export function syncNavigationBarSafeAreaInset(
+ navigationController: any,
+ controller: any,
+) {
+ 'worklet';
+
+ if (!navigationController || !controller) {
+ return;
+ }
+ const navigationBar = navigationController.navigationBar;
+ const controllerView = screenControllerView(controller);
+ const existingInsets = controller.additionalSafeAreaInsets;
+ const registry = getRegistry(globalThis as Record);
+ const screenId = directScreenIdForController(controller);
+ const injectedTop = screenId
+ ? registry.screenSafeAreaInsetTops[screenId] ??
+ controller.__rnsNativeScriptNavigationBarSafeAreaTop ??
+ 0
+ : controller.__rnsNativeScriptNavigationBarSafeAreaTop ?? 0;
+ const baseTop = Math.max(
+ 0,
+ edgeInsetsValue(existingInsets, 'top') - injectedTop,
+ );
+ const props = screenId ? registry.screenProps[screenId] : undefined;
+ if (props?.headerConfig?.translucent === true) {
+ syncNavigationBarScrollViewInset(controller, 0);
+ if (injectedTop >= 0.5) {
+ if (screenId) {
+ registry.screenSafeAreaInsetTops[screenId] = 0;
+ }
+ controller.__rnsNativeScriptNavigationBarSafeAreaTop = 0;
+ controller.additionalSafeAreaInsets = edgeInsetsWith(
+ baseTop,
+ edgeInsetsValue(existingInsets, 'left'),
+ edgeInsetsValue(existingInsets, 'bottom'),
+ edgeInsetsValue(existingInsets, 'right'),
+ );
+ controllerView?.setNeedsLayout?.();
+ controllerView?.dispatchSafeAreaDidChangeNotification?.();
+ }
+ return;
+ }
+ const hostSafeAreaTop = navigationControllerIsPresentedModally(
+ navigationController,
+ )
+ ? 0
+ : navigationControllerHostSafeAreaTop(navigationController);
+ const targetTop =
+ navigationBar && navigationBar.hidden !== true
+ ? Math.max(
+ 0,
+ rectMaxY(navigationBar.frame),
+ hostSafeAreaTop + (navigationBar.frame?.size?.height ?? 0),
+ )
+ : 0;
+ const baseSafeAreaTop = Math.max(
+ 0,
+ edgeInsetsValue(controllerView?.safeAreaInsets, 'top') - injectedTop,
+ );
+ const scrollInjectedTop = screenId
+ ? registry.screenScrollInsetTops[screenId] ?? 0
+ : controller.__rnsNativeScriptNavigationBarContentInsetTop ?? 0;
+ const missingTop = Math.max(0, targetTop - baseSafeAreaTop);
+
+ // NATIVESCRIPT_PORT_DEVIATION: upstream's RNSScreen view stays in the
+ // UINavigationController hierarchy while UIKit dismisses modals, so its
+ // ScrollView safe area is refreshed by UIKit before the next React layout.
+ // A NativeScript hostView(controller) can observe a parented navigation
+ // controller while the child safe area is still status-only after dismissal.
+ // Clear stale injected safe area only once UIKit already propagated the full
+ // header height; otherwise keep the generic missing-top injection below.
+ if (
+ navigationController.parentViewController != null &&
+ (targetTop < 0.5 || missingTop < 0.5)
+ ) {
+ syncNavigationBarScrollViewInset(controller, 0);
+ if (injectedTop >= 0.5) {
+ if (screenId) {
+ registry.screenSafeAreaInsetTops[screenId] = 0;
+ }
+ controller.__rnsNativeScriptNavigationBarSafeAreaTop = 0;
+ controller.additionalSafeAreaInsets = edgeInsetsWith(
+ baseTop,
+ edgeInsetsValue(existingInsets, 'left'),
+ edgeInsetsValue(existingInsets, 'bottom'),
+ edgeInsetsValue(existingInsets, 'right'),
+ );
+ controllerView?.setNeedsLayout?.();
+ controllerView?.dispatchSafeAreaDidChangeNotification?.();
+ }
+ return;
+ }
+ if (missingTop < 0.5) {
+ if (scrollInjectedTop >= 0.5) {
+ syncNavigationBarScrollViewInset(controller, 0);
+ }
+ if (injectedTop >= 0.5) {
+ syncNavigationBarScrollViewInset(controller, 0);
+ controller.__rnsNativeScriptNavigationBarSafeAreaTop = 0;
+ controller.additionalSafeAreaInsets = edgeInsetsWith(
+ baseTop,
+ edgeInsetsValue(existingInsets, 'left'),
+ edgeInsetsValue(existingInsets, 'bottom'),
+ edgeInsetsValue(existingInsets, 'right'),
+ );
+ controllerView?.setNeedsLayout?.();
+ controllerView?.dispatchSafeAreaDidChangeNotification?.();
+ }
+ return;
+ }
+ if (Math.abs(missingTop - injectedTop) < 0.5) {
+ syncNavigationBarScrollViewInset(controller, 0);
+ return;
+ }
+
+ // NATIVESCRIPT_PORT_DEVIATION: upstream UINavigationController propagates
+ // the nav bar's adjusted top safe area to its RNSScreen child naturally. A
+ // NativeScript hostView(controller) can mount the same UIKit view through a
+ // generic container before UIKit refreshes the child safe area; copy only the
+ // missing top inset so ScrollView automatic inset adjustment matches the
+ // native ObjC implementation without moving the route view frame.
+ if (screenId) {
+ registry.screenSafeAreaInsetTops[screenId] = missingTop;
+ }
+ controller.__rnsNativeScriptNavigationBarSafeAreaTop = missingTop;
+ controller.additionalSafeAreaInsets = edgeInsetsWith(
+ baseTop + missingTop,
+ edgeInsetsValue(existingInsets, 'left'),
+ edgeInsetsValue(existingInsets, 'bottom'),
+ edgeInsetsValue(existingInsets, 'right'),
+ );
+ controllerView?.setNeedsLayout?.();
+ controllerView?.dispatchSafeAreaDidChangeNotification?.();
+ syncNavigationBarScrollViewInset(controller, 0);
+}
+export const __nativeScriptSyncNavigationBarSafeAreaInsetForTests =
+ syncNavigationBarSafeAreaInset;
+export function layoutNavigationBarWithinSafeArea(navigationController: any) {
+ 'worklet';
+ 'stack-stable-content-miss',
+ `stack=${stackId} reason=registry-transitioning updating=${
+ registry.stackUpdatingModals[stackId] === true ? 1 : 0
+ } pending=${registry.stackPendingReconcileKeys[stackId] ? 1 : 0}`,
+ );
+ return false;
+ }
+ const viewControllers = navigationController?.viewControllers;
+ const count = arrayCount(viewControllers);
+ if (count <= 0) {
+ traceWorkletEvent(
+ 'stack-stable-content-miss',
+ `stack=${stackId ?? ''} reason=empty`,
+ );
+ return false;
+ }
+ const stableControllers = stableNativeViewControllersFromArray(
+ viewControllers,
+ registry,
+ navigationController?.view?.window != null,
+ );
+ if (!stableControllers) {
+ traceWorkletEvent(
+ 'stack-stable-content-miss',
+ `stack=${stackId ?? ''} reason=controller-array count=${count}`,
+ );
+ return false;
+ }
+ const { controllers, usedTaggedFallback } = stableControllers;
+ if (controllers.length !== count) {
+ traceWorkletEvent(
+ 'stack-stable-content-miss',
+ `stack=${
+ stackId ?? ''
+ } reason=controller-count count=${count} controllers=${
+ controllers.length
+ }`,
+ );
+ return false;
+ }
+ if (
+ !navigationControllerHostsControllerViews(navigationController, controllers)
+ ) {
+ traceWorkletEvent(
+ 'stack-stable-content-miss',
+ `stack=${stackId ?? ''} reason=controller-hosting count=${count}`,
+ );
+ return false;
+ }
+ for (let index = 0; index < count; index += 1) {
+ const isTopController = index === count - 1;
+ const controller = arrayItem(viewControllers, index);
+ const screenId = controllerScreenId(controller);
+ const controllerView = screenControllerView(controller);
+ const contentWrapperView = screenId
+ ? liveScreenContentWrapperViewFromRegistry(screenId, registry)
+ : undefined;
+ const topContentHasVisibleHostedContent =
+ isTopController &&
+ screenHasVisibleHostedContent(screenId, registry, controllerView);
+ const topContentHasCommittedHostedContent =
+ isTopController &&
+ screenContentWrapperHasLiveMountedHostContent(
+ screenId,
+ registry,
+ contentWrapperView,
+ controllerView,
+ );
+ const topContentHasCertifiedStableExposure =
+ isTopController &&
+ screenHasStableReadyMountedContent(screenId, registry, controllerView);
+ const topContentIsStableEnough =
+ topContentHasCertifiedStableExposure &&
+ (topContentHasVisibleHostedContent ||
+ (!usedTaggedFallback && topContentHasCommittedHostedContent));
+ if (!screenId) {
+ traceWorkletEvent(
+ 'stack-stable-content-miss',
+ `stack=${stackId ?? ''} reason=missing-screen index=${index}`,
+ );
+ return false;
+ }
+ if (!controllerView) {
+ traceWorkletEvent(
+ 'stack-stable-content-miss',
+ `stack=${
+ stackId ?? ''
+ } screen=${screenId} reason=missing-controller-view`,
+ );
+ return false;
+ }
+ if (isTopController && !screenContentIsReady(screenId, registry)) {
+ traceWorkletEvent(
+ 'stack-stable-content-miss',
+ `stack=${stackId ?? ''} screen=${screenId} reason=top-not-ready`,
+ );
+ return false;
+ }
+ if (
+ isTopController &&
+ (controllerView.hidden === true ||
+ (controllerView.alpha ?? 1) <= 0.01 ||
+ controllerViewHasHiddenNavigationAncestor(
+ controllerView,
+ navigationController.view,
+ ))
+ ) {
+ traceWorkletEvent(
+ 'stack-stable-content-miss',
+ `stack=${stackId ?? ''} screen=${screenId} reason=top-hidden`,
+ );
+ return false;
+ }
+ if (
+ isTopController &&
+ (!contentWrapperView ||
+ !contentWrapperIsMountedInScreenView(
+ contentWrapperView,
+ controllerView,
+ ) ||
+ hiddenAncestorBetweenViewAndAncestor(
+ contentWrapperView,
+ controllerView,
+ ) != null ||
+ (controllerView.window != null && contentWrapperView.window == null) ||
+ !topContentIsStableEnough)
+ ) {
+ traceWorkletEvent(
+ 'stack-stable-content-miss',
+ `stack=${stackId ?? ''} screen=${screenId} reason=top-wrapper wrapper=${
+ contentWrapperView ? 1 : 0
+ } mounted=${
+ contentWrapperView &&
+ contentWrapperIsMountedInScreenView(
+ contentWrapperView,
+ controllerView,
+ )
+ ? 1
+ : 0
+ } wrapperWindow=${contentWrapperView?.window ? 1 : 0} visible=${
+ topContentHasVisibleHostedContent ? 1 : 0
+ } committed=${topContentHasCommittedHostedContent ? 1 : 0} certified=${
+ topContentHasCertifiedStableExposure ? 1 : 0
+ }`,
+ );
+ return false;
+ }
+ }
+ return true;
+}
+export function navigationStackTopScreenHasReadyContent(
+ navigationController: any,
+ registry: NativeScriptStackRegistry,
+) {
+ 'worklet';
+
+ const viewControllers = navigationController?.viewControllers;
+ const count = arrayCount(viewControllers);
+ if (count <= 0) {
+ return false;
+ }
+ const topController =
+ navigationController?.topViewController ??
+ arrayItem(viewControllers, count - 1);
+ const screenId = controllerScreenId(topController);
+ const controllerView = screenControllerView(topController);
+ if (
+ !screenId ||
+ !controllerView ||
+ controllerView.hidden === true ||
+ (controllerView.alpha ?? 1) <= 0.01 ||
+ !screenContentIsReady(screenId, registry)
+ ) {
+ return false;
+ }
+ return screenHasStableReadyMountedContent(screenId, registry, controllerView);
+}
+export function currentNavigationStackStableLayoutKey(
+ navigationController: any,
+) {
+ 'worklet';
+
+ return (
+ navigationController?.__nativeScriptStableStackLayoutKey ??
+ associatedObjectForKey(
+ navigationController,
+ STACK_STABLE_LAYOUT_KEY_ASSOCIATION_KEY,
+ ) ??
+ ''
+ );
+}
+export function navigationStackCanSkipStableLayout(
+ navigationController: any,
+ registry: NativeScriptStackRegistry,
+ layoutKey: string,
+) {
+ 'worklet';
+
+ return (
+ navigationStackHasStableReadyContent(navigationController, registry) &&
+ currentNavigationStackStableLayoutKey(navigationController) === layoutKey
diff --git a/.oracle-context/ns-rns-first-paint/metro.config.js b/.oracle-context/ns-rns-first-paint/metro.config.js
new file mode 100644
index 000000000..b75923c12
--- /dev/null
+++ b/.oracle-context/ns-rns-first-paint/metro.config.js
@@ -0,0 +1,312 @@
+const path = require('path');
+const fs = require('fs');
+const { getDefaultConfig } = require('expo/metro-config');
+const exclusionList = require(path.join(
+ path.dirname(require.resolve('metro-config/package.json')),
+ 'src/defaults/exclusionList'
+)).default;
+
+const projectRoot = __dirname;
+const forksRoot = path.resolve(projectRoot, '..');
+const nativeScriptRoot = path.resolve(forksRoot, '../NativeScriptRuntime');
+const appNodeModules = path.join(projectRoot, 'node_modules');
+const reactNativeScreensRoot = path.join(forksRoot, 'react-native-screens');
+const reactNativeScreensSourceRoot = path.join(reactNativeScreensRoot, 'src');
+const useReactNativeScreensLib =
+ process.env.RNS_DEMO_USE_SCREENS_SRC !== '1' &&
+ process.env.EXPO_PUBLIC_RNS_DEMO_USE_SCREENS_SRC !== '1';
+const reactNativeScreensPackageRoot =
+ useReactNativeScreensLib
+ ? path.join(reactNativeScreensRoot, 'lib/module')
+ : path.join(reactNativeScreensRoot, 'src');
+const reactNavigationRoot = path.join(forksRoot, 'react-navigation');
+const reactNavigationNodeModules = path.join(reactNavigationRoot, 'node_modules');
+
+const localPackages = {
+ '@nativescript/react-native': path.join(nativeScriptRoot, 'packages/react-native'),
+ '@react-navigation/core': path.join(reactNavigationRoot, 'packages/core/src'),
+ '@react-navigation/elements': path.join(reactNavigationRoot, 'packages/elements/src'),
+ '@react-navigation/native': path.join(reactNavigationRoot, 'packages/native/src'),
+ '@react-navigation/native-stack': path.join(reactNavigationRoot, 'packages/native-stack/src'),
+ '@react-navigation/routers': path.join(reactNavigationRoot, 'packages/routers/src'),
+ 'expo-router': path.join(forksRoot, 'expo/packages/expo-router'),
+ immer: path.join(reactNavigationNodeModules, 'immer'),
+ 'react-native-screens': reactNativeScreensPackageRoot,
+ 'use-sync-external-store': path.join(
+ reactNavigationNodeModules,
+ 'use-sync-external-store'
+ ),
+ valibot: path.join(reactNavigationNodeModules, 'valibot'),
+};
+
+const reactNavigationSourceRoots = [
+ localPackages['@react-navigation/core'],
+ localPackages['@react-navigation/elements'],
+ localPackages['@react-navigation/native'],
+ localPackages['@react-navigation/native-stack'],
+ localPackages['@react-navigation/routers'],
+];
+
+const localPackageRoots = [
+ localPackages['@nativescript/react-native'],
+ ...reactNavigationSourceRoots,
+ path.join(forksRoot, 'expo/packages/expo-router'),
+ localPackages.immer,
+ reactNativeScreensRoot,
+ localPackages['react-native-screens'],
+ reactNativeScreensSourceRoot,
+ localPackages['use-sync-external-store'],
+ localPackages.valibot,
+];
+
+const singletonModules = [
+ 'react',
+ 'react-native',
+ 'react-native-safe-area-context',
+ 'react-native-screens',
+ 'react-native-worklets',
+ 'react-native-reanimated',
+];
+const singletonForkRoots = [
+ nativeScriptRoot,
+ reactNavigationRoot,
+ reactNativeScreensRoot,
+];
+
+function escapePathForRegex(filePath) {
+ return filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+const config = getDefaultConfig(projectRoot);
+
+process.env.METRO_TRANSFORM_TRACE_UPSTREAM =
+ config.transformer.babelTransformerPath;
+config.transformer.babelTransformerPath = path.join(
+ projectRoot,
+ 'scripts/metro-transform-logger.js'
+);
+
+function resolveSourcePackage(moduleName, packageName, sourceRoot) {
+ if (moduleName !== packageName && !moduleName.startsWith(`${packageName}/`)) {
+ return null;
+ }
+ const subpath = moduleName === packageName ? 'index' : moduleName.slice(packageName.length + 1);
+ const basePath = path.join(sourceRoot, subpath);
+ const candidates = [
+ basePath,
+ `${basePath}.ios.js`,
+ `${basePath}.ios.tsx`,
+ `${basePath}.ios.ts`,
+ `${basePath}.native.js`,
+ `${basePath}.native.tsx`,
+ `${basePath}.native.ts`,
+ `${basePath}.tsx`,
+ `${basePath}.ts`,
+ `${basePath}.js`,
+ path.join(basePath, 'index.ios.js'),
+ path.join(basePath, 'index.ios.tsx'),
+ path.join(basePath, 'index.ios.ts'),
+ path.join(basePath, 'index.native.js'),
+ path.join(basePath, 'index.native.tsx'),
+ path.join(basePath, 'index.native.ts'),
+ path.join(basePath, 'index.tsx'),
+ path.join(basePath, 'index.ts'),
+ path.join(basePath, 'index.js'),
+ ];
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
+}
+
+function resolveReactNativeScreensFabricSpec(moduleName, originModulePath) {
+ const libModuleRoot = path.join(reactNativeScreensRoot, 'lib/module');
+ const packagePrefix = 'react-native-screens/';
+ let candidatePath = null;
+
+ if (moduleName.startsWith('.')) {
+ candidatePath = path.resolve(path.dirname(originModulePath), moduleName);
+ } else if (moduleName.startsWith(packagePrefix)) {
+ candidatePath = path.join(
+ reactNativeScreensPackageRoot,
+ moduleName.slice(packagePrefix.length)
+ );
+ }
+
+ if (
+ !candidatePath ||
+ !candidatePath.startsWith(libModuleRoot) ||
+ !candidatePath.includes(`${path.sep}fabric${path.sep}`) ||
+ !path.basename(candidatePath).includes('NativeComponent')
+ ) {
+ return null;
+ }
+
+ const sourceSubpath = path.relative(libModuleRoot, candidatePath);
+ const basePath = path.join(reactNativeScreensSourceRoot, sourceSubpath);
+ const candidates = [
+ `${basePath}.ios.tsx`,
+ `${basePath}.ios.ts`,
+ `${basePath}.native.tsx`,
+ `${basePath}.native.ts`,
+ `${basePath}.tsx`,
+ `${basePath}.ts`,
+ `${basePath}.js`,
+ ];
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
+}
+
+function resolveReactNativeScreensRelativePlatformFile(moduleName, originModulePath) {
+ if (!moduleName.startsWith('.')) {
+ return null;
+ }
+
+ const resolvedOriginModulePath = path.isAbsolute(originModulePath)
+ ? originModulePath
+ : path.resolve(projectRoot, originModulePath);
+ if (
+ /(?:^|\/)NativeScriptScreenStack\.ios(?:\.chunk[1-5])?\.[jt]sx?$/.test(
+ resolvedOriginModulePath
+ ) &&
+ /^\.\/NativeScriptScreenStack\.ios\.chunk[1-5]$/.test(moduleName)
+ ) {
+ const chunkPath = path.resolve(
+ path.dirname(resolvedOriginModulePath),
+ `${moduleName}.js`
+ );
+ if (fs.existsSync(chunkPath)) {
+ return chunkPath;
+ }
+ }
+ const packageRoots = [
+ path.join(reactNativeScreensRoot, 'lib/module'),
+ reactNativeScreensSourceRoot,
+ ];
+
+ if (!packageRoots.some((root) => resolvedOriginModulePath.startsWith(root))) {
+ return null;
+ }
+
+ const basePath = path.resolve(
+ path.dirname(resolvedOriginModulePath),
+ moduleName
+ );
+ const exactJavaScriptFile = `${basePath}.js`;
+ if (path.basename(basePath).includes('.ios.chunk') && fs.existsSync(exactJavaScriptFile)) {
+ return exactJavaScriptFile;
+ }
+ const candidates = [
+ `${basePath}.ios.js`,
+ `${basePath}.ios.tsx`,
+ `${basePath}.ios.ts`,
+ `${basePath}.native.js`,
+ `${basePath}.native.tsx`,
+ `${basePath}.native.ts`,
+ `${basePath}.tsx`,
+ `${basePath}.ts`,
+ exactJavaScriptFile,
+ path.join(basePath, 'index.ios.js'),
+ path.join(basePath, 'index.ios.tsx'),
+ path.join(basePath, 'index.ios.ts'),
+ path.join(basePath, 'index.native.js'),
+ path.join(basePath, 'index.native.tsx'),
+ path.join(basePath, 'index.native.ts'),
+ path.join(basePath, 'index.tsx'),
+ path.join(basePath, 'index.ts'),
+ path.join(basePath, 'index.js'),
+ ];
+
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
+}
+
+config.watchFolders = localPackageRoots;
+config.resolver.blockList = exclusionList(
+ [
+ ...singletonModules.map(
+ (moduleName) =>
+ new RegExp(
+ `^${escapePathForRegex(appNodeModules)}\\/.*\\/node_modules\\/${escapePathForRegex(moduleName)}(\\/.*)?$`
+ )
+ ),
+ ...singletonModules.flatMap((moduleName) =>
+ singletonForkRoots.map(
+ (root) =>
+ new RegExp(
+ `^${escapePathForRegex(path.join(root, 'node_modules', moduleName))}(\\/.*)?$`
+ )
+ )
+ ),
+ ...[
+ path.join(projectRoot, 'ios'),
+ path.join(projectRoot, 'ios/Pods'),
+ path.join(projectRoot, 'ios/build'),
+ path.join(projectRoot, '.expo/dev/logs'),
+ path.join(projectRoot, '.expo/xcodebuild.log'),
+ path.join(reactNativeScreensRoot, 'ios'),
+ path.join(reactNativeScreensRoot, 'android'),
+ path.join(reactNativeScreensRoot, 'FabricExample'),
+ path.join(reactNativeScreensRoot, 'TVOSExample'),
+ path.join(reactNativeScreensRoot, 'docs'),
+ path.join(reactNativeScreensRoot, 'apps'),
+ ].map(
+ (blockedPath) =>
+ new RegExp(`^${escapePathForRegex(blockedPath)}(\\/.*)?$`)
+ ),
+ new RegExp(
+ `^${escapePathForRegex(reactNativeScreensRoot)}\\/lib\\/.*\\.map$`
+ ),
+ new RegExp(
+ `^${escapePathForRegex(reactNativeScreensRoot)}\\/lib\\/.*\\.test\\.[cm]?js$`
+ ),
+ new RegExp(
+ `^${escapePathForRegex(reactNativeScreensRoot)}\\/src\\/.*\\.test\\.[jt]sx?$`
+ ),
+ ]
+);
+const extraNodeModules = {
+ ...config.resolver.extraNodeModules,
+ ...localPackages,
+};
+config.resolver.extraNodeModules = new Proxy(extraNodeModules, {
+ get(target, name) {
+ if (typeof name !== 'string') {
+ return target[name];
+ }
+ return target[name] ?? path.join(appNodeModules, name);
+ },
+});
+const upstreamResolveRequest = config.resolver.resolveRequest;
+config.resolver.resolveRequest = (context, moduleName, platform) => {
+ const reactNativeScreensRelativePlatformFile =
+ resolveReactNativeScreensRelativePlatformFile(
+ moduleName,
+ context.originModulePath
+ );
+ if (reactNativeScreensRelativePlatformFile) {
+ return {
+ type: 'sourceFile',
+ filePath: reactNativeScreensRelativePlatformFile,
+ };
+ }
+
+ const fabricSpecSourceFile = resolveReactNativeScreensFabricSpec(
+ moduleName,
+ context.originModulePath
+ );
+ if (fabricSpecSourceFile) {
+ return { type: 'sourceFile', filePath: fabricSpecSourceFile };
+ }
+
+ const sourceFile =
+ resolveSourcePackage(moduleName, '@react-navigation/core', localPackages['@react-navigation/core']) ??
+ resolveSourcePackage(moduleName, '@react-navigation/elements', localPackages['@react-navigation/elements']) ??
+ resolveSourcePackage(moduleName, '@react-navigation/native', localPackages['@react-navigation/native']) ??
+ resolveSourcePackage(moduleName, '@react-navigation/native-stack', localPackages['@react-navigation/native-stack']) ??
+ resolveSourcePackage(moduleName, '@react-navigation/routers', localPackages['@react-navigation/routers']) ??
+ resolveSourcePackage(moduleName, 'react-native-screens', localPackages['react-native-screens']);
+ if (sourceFile) {
+ return { type: 'sourceFile', filePath: sourceFile };
+ }
+ return upstreamResolveRequest
+ ? upstreamResolveRequest(context, moduleName, platform)
+ : context.resolveRequest(context, moduleName, platform);
+};
+
+module.exports = config;
diff --git a/.oracle-context/ns-rns-first-paint/push-header-delegate.ts b/.oracle-context/ns-rns-first-paint/push-header-delegate.ts
new file mode 100644
index 000000000..dcd9a0365
--- /dev/null
+++ b/.oracle-context/ns-rns-first-paint/push-header-delegate.ts
@@ -0,0 +1,420 @@
+ ctx?.retain?.(animationBlock);
+ ctx?.retain?.(completionBlock);
+ const didSchedule = animateAlongsideTransitionCompletion(
+ coordinator,
+ animationBlock,
+ completionBlock,
+ );
+ if (
+ didSchedule !== true &&
+ navigationController.__nativeScriptHeaderAppearanceToken === token
+ ) {
+ releaseCoordinatorBlocks();
+ navigationController.__nativeScriptHeaderAppearanceToken = undefined;
+ }
+}
+export const __nativeScriptConfigureNavigationAppearanceForTests =
+ configureNavigationAppearance;
+export function configureModalScreenController(
+ controller: any,
+ props: Readonly,
+ registry: NativeScriptStackRegistry,
+ ctx?: any,
+) {
+ 'worklet';
+
+ if (!isModalPresentation(props.stackPresentation)) {
+ return;
+ }
+ setControllerPresentedModalScreenId(controller, props.screenId);
+
+ // Direct port of RNSScreenView's stackPresentation/stackAnimation/
+ // gestureEnabled setters: the presented RNSScreen controller owns UIKit
+ // presentation state, not the surrounding stack.
+ controller.modalPresentationStyle = modalPresentationStyle(
+ props.stackPresentation,
+ );
+ controller.modalTransitionStyle = modalTransitionStyleForProps(props);
+ controller.modalInPresentation = props.gestureEnabled === false;
+ setupAdaptivePresentationControllerDelegate(controller, props, ctx);
+ const sheetPresentationController = controller.sheetPresentationController;
+ if (
+ props.stackPresentation === 'modal' &&
+ sheetPresentationController &&
+ 'prefersPageSizing' in sheetPresentationController
+ ) {
+ sheetPresentationController.prefersPageSizing = true;
+ }
+ updateFormSheetPresentationStyle(controller, props, registry, true);
+ setupBackdropTapGestureRecognizer(controller, props, ctx);
+}
+export function configureScreenControllerHeader(
+ controller: any,
+ props: Readonly,
+ ctx?: any,
+ isTopScreen = false,
+ canGoBack = false,
+ registry?: NativeScriptStackRegistry,
+ animated = false,
+ updateNavigationAppearance = true,
+) {
+ 'worklet';
+
+ const resolvedRegistry =
+ registry ?? getRegistry(globalThis as Record);
+ const headerConfig = props.headerConfig;
+ configureExtendedLayout(controller, headerConfig);
+ const navigationItem = controller?.navigationItem;
+ setNativeValueForKey(controller, 'title', headerConfig?.title ?? '');
+ if (navigationItem) {
+ if (headerConfig?.hidden === true) {
+ setNativeValueForKey(navigationItem, 'title', headerConfig?.title ?? '');
+ controller.__nativeScriptHeaderSubviewKey = headerSubviewRecordsKey(
+ props.screenId,
+ );
+ const navigationController = controller.navigationController;
+ if (navigationController && updateNavigationAppearance) {
+ configureNavigationAppearance(
+ navigationController,
+ headerConfig,
+ ctx,
+ resolvedRegistry,
+ animated,
+ );
+ }
+ emitHeaderHeightChange(controller, props, ctx);
+ return;
+export function prepareControllerForStackExposure(
+ navigationController: any,
+ screenId: string | undefined,
+ controller: any,
+ registry: NativeScriptStackRegistry,
+ ctx: any,
+ isTopScreen: boolean,
+ canGoBack: boolean,
+ refreshHostedContent = true,
+ configureHeader = true,
+ restoreHostView = true,
+) {
+ 'worklet';
+
+ if (!screenId || !controller) {
+ return false;
+ }
+ const props = registry.screenProps[screenId];
+ if (!props) {
+ return false;
+ }
+ if (restoreHostView) {
+ restoreScreenControllerViewFromHostHandle(controller, screenId, registry);
+ }
+ traceWorkletEvent(
+ 'prepareStackExposure-after-host-restore',
+ `screen=${screenId} restore=${restoreHostView ? 1 : 0}`,
+ );
+ const controllerView = ensureScreenControllerView(controller);
+ traceWorkletEvent(
+ 'prepareStackExposure-after-ensure-view',
+ `screen=${screenId} hasView=${controllerView ? 1 : 0}`,
+ );
+ const navigationView = navigationController?.view;
+ const targetBounds = navigationView?.bounds ?? navigationView?.frame;
+ traceWorkletEvent(
+ 'prepareStackExposure-after-target-bounds',
+ `screen=${screenId} hasBounds=${targetBounds ? 1 : 0}`,
+ );
+ if (controllerView) {
+ if (
+ controller.__nativeScriptViewIsSnapshot === true ||
+ controllerView.__nativeScriptOwningViewController !== controller
+ ) {
+ restoreScreenControllerView(controller, controllerView);
+ }
+ traceWorkletEvent(
+ 'prepareStackExposure-after-view-restore',
+ `screen=${screenId}`,
+ );
+ controllerView.hidden = false;
+ controllerView.alpha = 1;
+ controllerView.userInteractionEnabled = true;
+ controllerView.autoresizingMask = flexibleSizeMask();
+ traceWorkletEvent(
+ 'prepareStackExposure-after-view-props',
+ `screen=${screenId}`,
+ );
+ if (targetBounds && rectHasPositiveSize(targetBounds)) {
+ const targetFrame = rectWithZeroOrigin(targetBounds);
+ setViewFrameIfNeeded(controllerView, targetFrame);
+ setViewBoundsIfNeeded(
+ controllerView,
+ boundsRectForFilledHostedSubview(controllerView, targetFrame),
+ );
+ }
+ traceWorkletEvent('prepareStackExposure-after-frame', `screen=${screenId}`);
+ }
+ if (configureHeader) {
+ configureScreenControllerHeader(
+ controller,
+ props,
+ registry.screenContexts[screenId] ?? ctx,
+ isTopScreen,
+ canGoBack,
+ registry,
+ );
+ }
+ traceWorkletEvent(
+ 'prepareStackExposure-after-header',
+ `screen=${screenId} header=${configureHeader ? 1 : 0}`,
+ );
+ if (refreshHostedContent) {
+ certifyScreenContentReadyFromHostDescendants(screenId, registry);
+ }
+ const needsContentRefresh =
+ refreshHostedContent && !screenContentIsReady(screenId, registry);
+ traceWorkletEvent(
+ 'prepareStackExposure-before-content-ready',
+ `screen=${screenId} needsRefresh=${needsContentRefresh ? 1 : 0}`,
+ );
+ const didRefreshContent = refreshHostedContent
+ ? refreshScreenContentReady(
+ screenId,
+ controller,
+ registry,
+ needsContentRefresh,
+ )
+ : false;
+ traceWorkletEvent(
+ 'prepareStackExposure-after-content-ready',
+ `screen=${screenId} did=${didRefreshContent ? 1 : 0}`,
+ );
+ const needsContentRefreshAfterRefresh =
+ refreshHostedContent && !screenContentIsReady(screenId, registry);
+ if (controllerView && needsContentRefreshAfterRefresh) {
+ layoutScreenHostedReactSubviews(
+ controller,
+ controllerView,
+ registry,
+ screenId,
+ true,
+ true,
+ );
+ rememberScreenHostedViewRefreshKey(
+ controller,
+ screenId,
+ registry,
+ controllerView,
+ );
+ screenControllerLayoutDidChange(controller);
+ }
+ if (needsContentRefreshAfterRefresh) {
+ refreshScreenContentWrapperHost(
+ screenId,
+ registry,
+ true,
+ true,
+ true,
+ false,
+ 'prepare-stack-exposure',
+ );
+ }
+ prepareVisibleScreenForImmediateInteraction(
+ screenId,
+ registry,
+ controllerView,
+ false,
+ );
+ return didRefreshContent;
+}
+export function reparentNavigationControllerForModalContent(
+ );
+ }
+ return;
+ }
+ if (canAnimateNativeChange && previousKey != null && previousCount > 0) {
+ const isPush =
+ nextCount === previousCount + 1 &&
+ idsEqual(previousIds, availableIds.slice(0, previousCount));
+ const isPushFromNativeStack =
+ nativeIds.length > 0 &&
+ nextCount === nativeIds.length + 1 &&
+ idsEqual(nativeIds, availableIds.slice(0, nativeIds.length));
+ const isPop =
+ nextCount < previousCount &&
+ idsEqual(availableIds, previousIds.slice(0, nextCount));
+ traceWorkletEvent(
+ 'reconcileStack-diff',
+ `stack=${stackId} canAnimate=${canAnimateNativeChange ? 1 : 0} isPush=${
+ isPush ? 1 : 0
+ } isPushNative=${isPushFromNativeStack ? 1 : 0} isPop=${
+ isPop ? 1 : 0
+ } nativeLen=${nativeIds.length} prev=${previousCount} next=${nextCount}`,
+ );
+ if (
+ (isPush || isPushFromNativeStack) &&
+ nativeIds.length <= previousCount
+ ) {
+ const pushedScreenId = availableIds[nextCount - 1];
+ const pushedController = controllers[nextCount - 1];
+ prepareControllerForStackExposure(
+ navigationController,
+ pushedScreenId,
+ pushedController,
+ registry,
+ ctx,
+ true,
+ nextCount > 1,
+ true,
+ true,
+ true,
+ );
+ const didPreparePushedContent =
+ prepareScreenHostedContentForNativeTransition(
+ pushedController,
+ pushedScreenId,
+ registry,
+ );
+ const pushedContentIsReady = screenContentIsReady(
+ pushedScreenId,
+ registry,
+ );
+ traceWorkletEvent(
+ 'reconcileStack-push-after-prepare',
+ `stack=${stackId} screen=${pushedScreenId} content=${
+ didPreparePushedContent ? 1 : 0
+ } ready=${pushedContentIsReady ? 1 : 0}`,
+ );
+ if (!didPreparePushedContent || !pushedContentIsReady) {
+ registry.stackPendingReconcileKeys[stackId] = nextKey;
+ schedulePostLayoutStackReconcile(
+ stackId,
+ registry,
+ ctx,
+ true,
+ nextKey,
+ registry.stackTransactionRevisions[stackId],
+ );
+ traceWorkletEvent(
+ 'reconcileStack-push-deferred-content',
+ `stack=${stackId} screen=${pushedScreenId}`,
+ );
+ updateStackBackGestureState(navigationController);
+ return;
+ }
+ const token = markTransition(
+ stackId,
+ registry,
+ ctx,
+ false,
+ pushedScreenId,
+ false,
+ true,
+ false,
+ );
+ traceWorkletEvent(
+ 'reconcileStack-push-after-mark',
+ `stack=${stackId} screen=${pushedScreenId}`,
+ );
+ traceWorkletEvent(
+ 'reconcileStack-push-before-animate',
+ `stack=${stackId} fn=${typeof animateStackPush} parent=${
+ nativeObjectStableHandle(navigationController) ?? ''
+ } screen=${pushedScreenId}`,
+ );
+ traceWorkletEvent(
+ 'reconcileStack-push-before-native',
+ `stack=${stackId} screen=${pushedScreenId}`,
+ );
+ cancelActiveTouchesForStackContainer(stackId, registry);
+ let didAnimatePush = false;
+ try {
+ didAnimatePush = animateStackPush(
+ navigationController,
+ controllers,
+ pushedController,
+ pushedScreenId,
+ );
+ } catch (error) {
+ traceWorkletEvent(
+ 'reconcileStack-push-animate-error',
+ `stack=${stackId} error=${String(
+ (error as Error)?.message ?? error,
+ )}`,
+ );
+ throw error;
+ }
+ traceWorkletEvent(
+ 'reconcileStack-push-after-animate',
+ `stack=${stackId} did=${didAnimatePush ? 1 : 0}`,
+ );
+ if (didAnimatePush) {
+ applyStackNativeState(stackId, registry, {
+ count: nextCount,
+ key: nextKey,
+ });
+ const pushedControllerView = screenControllerView(pushedController);
+ if (pushedControllerView) {
+ prepareVisibleScreenForImmediateInteraction(
+ pushedScreenId,
+ registry,
+ pushedControllerView,
+ false,
+ );
+ }
+ cancelActiveTouchesForStackScreen(
+ registry,
+ registry.stackNativeKeys[ctx.props.stackId],
+ );
+ if (
+ shouldIgnoreNoOpNativeWillShow(
+ currentIds,
+ activeIds,
+ nextScreenId,
+ previousNativeIds,
+ )
+ ) {
+ traceWorkletEvent(
+ 'stack-willShow-noop-current',
+ `stack=${
+ ctx.props.stackId
+ } screen=${nextScreenId} current=${currentIds.join(
+ ',',
+ )} native=${previousNativeIds.join(',')}`,
+ );
+ return;
+ }
+ configureHeaderForWillShowViewController(
+ registry,
+ ctx,
+ viewController,
+ activeIds,
+ currentIds,
+ animated === true,
+ );
+ const nativeStackIsAlreadyShorter =
+ shouldEmitNativeStackChange(currentIds, activeIds) &&
+ previousNativeIds.length > currentIds.length;
+ const closing =
+ nativeStackIsAlreadyShorter ||
+ (nextIndex >= 0 &&
+ nextIndex < Math.max(0, currentIds.length - 1));
+ const affectedScreenId = closing
+ ? activeIds[activeIds.length - 1] ??
+ currentIds[currentIds.length - 1]
+ : nextScreenId ?? activeIds[activeIds.length - 1];
+ if (!nextScreenId) {
+ return;
+ }
+ if (registry.stackTransitioning[ctx.props.stackId]) {
+ return;
+ }
+ markTransition(
+ ctx.props.stackId,
+ registry,
+ ctx,
+ closing,
+ affectedScreenId,
+ true,
+ false,
+ );
+ },
+ navigationControllerDidShowViewControllerAnimated(
diff --git a/.oracle-context/ns-rns-first-paint/readiness-and-wrapper.ts b/.oracle-context/ns-rns-first-paint/readiness-and-wrapper.ts
new file mode 100644
index 000000000..438feb5d3
--- /dev/null
+++ b/.oracle-context/ns-rns-first-paint/readiness-and-wrapper.ts
@@ -0,0 +1,870 @@
+ if (
+ isModalPresentation(registry.screenProps[ids[index]]?.stackPresentation)
+ ) {
+ return index;
+ }
+ }
+ return -1;
+}
+export function configureExtendedLayout(
+ controller: any,
+ headerConfig?: ScreenStackHeaderConfigProps,
+) {
+ 'worklet';
+
+ if (!controller) {
+ return;
+ }
+ const headerIsVisible = headerConfig != null && headerConfig.hidden !== true;
+ controller.edgesForExtendedLayout =
+ headerIsVisible && headerConfig?.translucent !== true
+ ? rectEdgeAllExceptTop()
+ : rectEdgeAll();
+}
+export function createPlaceholderViewController() {
+ 'worklet';
+
+ const UIViewController = nativeClassValue('UIViewController');
+ if (!UIViewController || typeof UIViewController.alloc !== 'function') {
+ return null;
+ }
+ const placeholder = createNativeClassInstance(UIViewController);
+ if (placeholder?.view) {
+ placeholder.view.backgroundColor = nativeColor(
+ undefined,
+ 'systemBackgroundColor',
+ );
+ }
+ configureExtendedLayout(placeholder);
+ return placeholder;
+}
+export function createArray(values: any[]) {
+ 'worklet';
+
+ const NSArray = nativeValue('NSArray');
+ if (NSArray && typeof NSArray.arrayWithArray === 'function') {
+ try {
+ return NSArray.arrayWithArray(values);
+ } catch (_error) {
+ return values;
+ }
+ }
+ contentWrapperView: any,
+ childrenView?: any,
+) {
+ 'worklet';
+
+ const wrapper = rememberScreenContentWrapperView(
+ screenId,
+ registry,
+ contentWrapperView,
+ );
+ if (!wrapper) {
+ return null;
+ }
+ markNativeScriptScreenContentWrapperIdentity(wrapper, childrenView);
+ return wrapper;
+}
+export function screenContentWrapperVisibleDescendantCount(
+ view: any,
+ depth = 0,
+): number {
+ 'worklet';
+
+ if (
+ !view ||
+ depth > 32 ||
+ view.hidden === true ||
+ (typeof view.alpha === 'number' && view.alpha <= 0.01)
+ ) {
+ return 0;
+ }
+ const subviews = view.subviews;
+ const count = arrayCount(subviews);
+ let visibleCount = 0;
+ for (let index = 0; index < count; index += 1) {
+ const subview = arrayItem(subviews, index);
+ if (
+ !subview ||
+ subview.hidden === true ||
+ (typeof subview.alpha === 'number' && subview.alpha <= 0.01) ||
+ nativeObjectDescription(subview).includes(
+ 'NativeScriptDetachedChildrenTouchSentinel',
+ )
+ ) {
+ continue;
+ }
+ visibleCount += 1;
+ visibleCount += screenContentWrapperVisibleDescendantCount(
+ subview,
+ depth + 1,
+ );
+ }
+ return visibleCount;
+}
+export function discoverScreenContentWrapperView(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ screenView?: any,
+) {
+ 'worklet';
+
+ if (!screenId) {
+ return null;
+ }
+ const resolvedScreenView =
+ screenView ?? screenControllerView(registry.screens[screenId]);
+ const subviews = resolvedScreenView?.subviews;
+ const count = arrayCount(subviews);
+ for (let index = 0; index < count; index += 1) {
+ const subview = arrayItem(subviews, index);
+ const rootView = screenContentWrapperRootViewInTree(subview);
+ if (rootView) {
+ return rememberScreenContentWrapperView(screenId, registry, rootView);
+ }
+ if (screenContentWrapperComponentDescriptionMatches(subview)) {
+ return rememberScreenContentWrapperView(
+ screenId,
+ registry,
+ screenContentWrapperFabricViewForMountedView(
+ subview,
+ resolvedScreenView,
+ screenId,
+ ),
+ );
+ }
+ }
+ return null;
+}
+export function viewHasVisibleHostedReactContent(
+ view: any,
+ depth = 0,
+): boolean {
+ 'worklet';
+
+ if (
+ !view ||
+ depth > 24 ||
+ view.hidden === true ||
+ (typeof view.alpha === 'number' && view.alpha <= 0.01) ||
+ view.accessibilityElementsHidden === true
+ ) {
+ return false;
+ }
+ const description = nativeObjectDescription(view);
+ if (
+ description.includes('RCTParagraphComponentView') ||
+ description.includes('RCTScrollViewComponentView') ||
+ description.includes('RCTEnhancedScrollView') ||
+ description.includes('RCTText') ||
+ (description.includes('RCTViewComponentView') && depth > 0)
+ ) {
+ return true;
+ }
+ const subviews = view.subviews;
+ const count = arrayCount(subviews);
+ for (let index = 0; index < count; index += 1) {
+ if (
+ viewHasVisibleHostedReactContent(arrayItem(subviews, index), depth + 1)
+ ) {
+ return true;
+ }
+ }
+ return false;
+}
+export function screenViewHasVisibleHostedReactContent(
+ screenView: any,
+ excludedView?: any,
+) {
+ 'worklet';
+
+ const subviews = screenView?.subviews;
+ const count = arrayCount(subviews);
+ for (let index = 0; index < count; index += 1) {
+ const subview = arrayItem(subviews, index);
+ if (
+ !subview ||
+ (excludedView &&
+ (nativeObjectsEqual(subview, excludedView) ||
+ viewIsDescendantOfView(subview, excludedView))) ||
+ contentWrapperShellDescriptionMatches(subview)
+ ) {
+ continue;
+ }
+ if (viewHasVisibleHostedReactContent(subview)) {
+ return true;
+ }
+ }
+ return false;
+}
+export function screenHasSeparateVisibleHostedContent(
+ screenView: any,
+ contentWrapperView: any,
+) {
+ 'worklet';
+
+ return screenViewHasVisibleHostedReactContent(screenView, contentWrapperView);
+}
+export function screenContentWrapperChildHostView(contentWrapperView: any) {
+ 'worklet';
+
+ const childHost =
+ nativeScriptScreenContentWrapperChildrenViewForView(contentWrapperView);
+ if (
+ !childHost ||
+ childHost === contentWrapperView ||
+ nativeObjectsEqual(childHost, contentWrapperView)
+ ) {
+ return null;
+ }
+ return childHost;
+}
+export function rememberCommittedScreenContentWrapper(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ contentWrapperView: any,
+) {
+ 'worklet';
+
+ if (!screenId || !contentWrapperView) {
+ return;
+ }
+ const committedHandle =
+ nativeObjectStableHandle(contentWrapperView) ??
+ registry.screenContentWrapperHostHandles?.[screenId];
+ if (committedHandle) {
+ const committedHandles =
+ registry.screenContentWrapperCommittedHandles ??
+ (registry.screenContentWrapperCommittedHandles = {});
+ committedHandles[screenId] = committedHandle;
+ }
+}
+export function rememberMountedScreenContentWrapperFabricChild(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ contentWrapperView: any,
+) {
+ 'worklet';
+
+ if (!screenId || !contentWrapperView) {
+ return false;
+ }
+ const mountedHandle =
+ nativeObjectStableHandle(contentWrapperView) ??
+ registry.screenContentWrapperHostHandles?.[screenId];
+ if (!mountedHandle) {
+ return false;
+ }
+ const mountedHandles =
+ registry.screenContentWrapperMountedFabricChildHandles ??
+ (registry.screenContentWrapperMountedFabricChildHandles = {});
+ mountedHandles[screenId] = mountedHandle;
+ return true;
+}
+export function clearMountedScreenContentWrapperFabricChild(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+) {
+ 'worklet';
+
+ if (!screenId) {
+ return;
+ }
+ const mountedHandles = registry.screenContentWrapperMountedFabricChildHandles;
+ if (mountedHandles) {
+ mountedHandles[screenId] = undefined;
+ }
+}
+export function screenContentWrapperMatchesCommittedHandle(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ contentWrapperView: any,
+) {
+ 'worklet';
+
+ if (!screenId || !contentWrapperView) {
+ return false;
+ }
+ const committedHandle =
+ registry.screenContentWrapperCommittedHandles?.[screenId];
+ if (!committedHandle) {
+ return false;
+ }
+ const currentHandle =
+ nativeObjectStableHandle(contentWrapperView) ??
+ registry.screenContentWrapperHostHandles?.[screenId];
+ if (currentHandle && currentHandle === committedHandle) {
+ return true;
+ }
+ const committedWrapperView = nativeObjectFromHandle(committedHandle);
+ return !!(
+ committedWrapperView &&
+ (nativeObjectsEqual(committedWrapperView, contentWrapperView) ||
+ viewIsDescendantOfView(committedWrapperView, contentWrapperView) ||
+ viewIsDescendantOfView(contentWrapperView, committedWrapperView))
+ );
+}
+export function screenContentWrapperMatchesMountedFabricChild(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ contentWrapperView: any,
+) {
+ 'worklet';
+
+ if (!screenId || !contentWrapperView) {
+ return false;
+ }
+ const mountedHandle =
+ registry.screenContentWrapperMountedFabricChildHandles?.[screenId];
+ if (!mountedHandle) {
+ return false;
+ }
+ const currentHandle =
+ nativeObjectStableHandle(contentWrapperView) ??
+ registry.screenContentWrapperHostHandles?.[screenId];
+ if (currentHandle && currentHandle === mountedHandle) {
+ return true;
+ }
+ const mountedWrapperView = nativeObjectFromHandle(mountedHandle);
+ return !!(
+ mountedWrapperView &&
+ (nativeObjectsEqual(mountedWrapperView, contentWrapperView) ||
+ viewIsDescendantOfView(mountedWrapperView, contentWrapperView) ||
+ viewIsDescendantOfView(contentWrapperView, mountedWrapperView))
+ );
+}
+export function screenContentWrapperHasCommittedChildHostContent(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ contentWrapperView: any,
+) {
+ 'worklet';
+
+ if (!screenId || !contentWrapperView) {
+ return false;
+ }
+ const childHost = screenContentWrapperChildHostView(contentWrapperView);
+ const isNativeScriptWrapperRoot =
+ nativeScriptScreenContentWrapperRootViewForView(contentWrapperView) != null;
+ if (!isNativeScriptWrapperRoot && !childHost) {
+ return false;
+ }
+ const visibleDescendantCount =
+ registry.screenContentWrapperVisibleDescendantCounts?.[screenId] ??
+ screenContentWrapperVisibleDescendantCount(contentWrapperView);
+ if (
+ visibleDescendantCount > 0 ||
+ viewHasVisibleHostedReactContent(contentWrapperView)
+ ) {
+ rememberCommittedScreenContentWrapper(
+ screenId,
+ registry,
+ contentWrapperView,
+ );
+ return true;
+ }
+ if (
+ screenContentWrapperMatchesCommittedHandle(
+ screenId,
+ registry,
+ contentWrapperView,
+ )
+ ) {
+ return true;
+ }
+ return false;
+}
+export function screenContentWrapperHasLiveMountedHostContent(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ contentWrapperView: any,
+ screenView?: any,
+) {
+ 'worklet';
+
+ if (!screenId || !contentWrapperView) {
+ return false;
+ }
+ const resolvedScreenView =
+ screenView ?? screenControllerView(registry.screens[screenId]);
+ if (
+ resolvedScreenView &&
+ !contentWrapperIsMountedInScreenView(contentWrapperView, resolvedScreenView)
+ ) {
+ return false;
+ }
+ if (resolvedScreenView?.window != null && contentWrapperView.window == null) {
+ return false;
+ }
+ if (
+ screenContentWrapperVisibleDescendantCount(contentWrapperView) > 0 ||
+ viewHasVisibleHostedReactContent(contentWrapperView)
+ ) {
+ rememberCommittedScreenContentWrapper(
+ screenId,
+ registry,
+ contentWrapperView,
+ );
+ return true;
+ }
+ return false;
+}
+export function screenContentWrapperHasVisibleMountedFabricChildContent(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ contentWrapperView: any,
+ screenView?: any,
+) {
+ 'worklet';
+
+ if (
+ !screenId ||
+ !contentWrapperView ||
+ !screenContentWrapperMatchesMountedFabricChild(
+ screenId,
+ registry,
+ contentWrapperView,
+ )
+ ) {
+ return false;
+ }
+ const resolvedScreenView =
+ screenView ?? screenControllerView(registry.screens?.[screenId]);
+ if (
+ resolvedScreenView &&
+ !contentWrapperIsMountedInScreenView(contentWrapperView, resolvedScreenView)
+ ) {
+ return false;
+ }
+ const childHost = screenContentWrapperChildHostView(contentWrapperView);
+ const isNativeScriptWrapperRoot =
+ nativeScriptScreenContentWrapperRootViewForView(contentWrapperView) != null;
+ if (!isNativeScriptWrapperRoot && !childHost) {
+ return false;
+ }
+ const visibleDescendantCount =
+ registry.screenContentWrapperVisibleDescendantCounts?.[screenId] ??
+ screenContentWrapperVisibleDescendantCount(contentWrapperView);
+ if (
+ visibleDescendantCount > 0 ||
+ viewHasVisibleHostedReactContent(contentWrapperView)
+ ) {
+ rememberCommittedScreenContentWrapper(
+ screenId,
+ registry,
+ contentWrapperView,
+ );
+ return true;
+ }
+ return false;
+}
+export function normalizeScreenContentWrapperChildHost(
+ contentWrapperView: any,
+) {
+ 'worklet';
+
+ const childHost = screenContentWrapperChildHostView(contentWrapperView);
+ if (!contentWrapperView || !childHost) {
+ return false;
+ }
+ let didMutate = false;
+ if (!nativeObjectsEqual(childHost.superview, contentWrapperView)) {
+ childHost.removeFromSuperview?.();
+ contentWrapperView.addSubview?.(childHost);
+ didMutate = true;
+ }
+ const contentWrapperFrame = contentWrapperView.frame;
+ const contentWrapperBounds = contentWrapperView.bounds;
+ contentWrapperView,
+ screenView,
+ screenController,
+ screenOwner,
+ )
+ ) {
+ traceWorkletEvent(
+ 'content-wrapper-mount-defer-chain',
+ `screen=${screenId} wrapper=${
+ nativeObjectStableHandle(contentWrapperView) ?? ''
+ } screen=${nativeObjectStableHandle(screenView) ?? ''}`,
+ );
+ return false;
+ }
+ return true;
+}
+export const __nativeScriptScreenContentWrapperCanMountInScreenViewForTests =
+ screenContentWrapperCanMountInScreenView;
+export function screenHasVisibleHostedContent(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ screenView?: any,
+) {
+ 'worklet';
+
+ if (!screenId) {
+ return false;
+ }
+ const contentWrapperView = liveScreenContentWrapperViewFromRegistry(
+ screenId,
+ registry,
+ );
+ const resolvedScreenView =
+ screenView ?? screenControllerView(registry.screens?.[screenId]);
+ if (!contentWrapperView) {
+ return screenViewHasVisibleHostedReactContent(resolvedScreenView);
+ }
+ const wrapperMountedInScreenView =
+ !!resolvedScreenView &&
+ contentWrapperIsMountedInScreenView(contentWrapperView, resolvedScreenView);
+ if (
+ wrapperMountedInScreenView &&
+ viewHasVisibleHostedReactContent(contentWrapperView)
+ ) {
+ return true;
+ }
+ return screenHasSeparateVisibleHostedContent(
+ resolvedScreenView,
+ contentWrapperView,
+ );
+}
+export function screenHasHostReadyVisibleDescendants(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+) {
+ 'worklet';
+
+ if (
+ !screenId ||
+ !screenContentWrapperHasHostReadySignal(screenId, registry)
+ ) {
+ return false;
+ }
+ if (
+ (registry.screenContentWrapperVisibleDescendantCounts?.[screenId] ?? 0) > 0
+ ) {
+ return true;
+ }
+ return screenHasVisibleHostedContent(
+ screenId,
+ registry,
+ screenControllerView(registry.screens?.[screenId]),
+ );
+}
+export function screenHeaderSubviewsAreReady(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+) {
+ 'worklet';
+
+ if (!screenId) {
+ return false;
+ }
+ const expectedHeaderSubviewCount =
+ registry.screenHeaderSubviewExpectedCounts[screenId] ?? 0;
+ const headerSubviewRecords = headerSubviewRecordsForScreen(screenId);
+ const headerSubviewRecordCount = headerSubviewRecords.length;
+ if (
+ registry.screenHeaderConfigs[screenId] &&
+ registry.screenHeaderSubviewExpectedCountResolved[screenId] !== true &&
+ (expectedHeaderSubviewCount > 0 || headerSubviewRecordCount > 0)
+ ) {
+ traceWorkletEvent(
+ 'header-subview-not-ready',
+ `screen=${screenId} reason=expected-unresolved hasConfig=1 records=${headerSubviewRecordCount} expected=${expectedHeaderSubviewCount}`,
+ );
+ return false;
+ }
+ if (headerSubviewRecordCount < expectedHeaderSubviewCount) {
+ traceWorkletEvent(
+ 'header-subview-not-ready',
+ `screen=${screenId} reason=records records=${headerSubviewRecordCount} expected=${expectedHeaderSubviewCount}`,
+ );
+ return false;
+ }
+ if (expectedHeaderSubviewCount <= 0) {
+ return true;
+ }
+ for (const record of headerSubviewRecords) {
+ if (record.type === 'searchBar' || record.type === 'back') {
+ continue;
+ }
+ const sizeKey = headerSubviewPublishedSizeKey(record.nativeView);
+ if (!headerSubviewPublishedSizeIsReady(record)) {
+ traceWorkletEvent(
+ 'header-subview-not-ready',
+ `screen=${screenId} reason=size type=${record.type} size=${sizeKey}`,
+ );
+ return false;
+ }
+ }
+ return true;
+}
+export const __nativeScriptScreenHeaderSubviewsAreReadyForTests =
+ screenHeaderSubviewsAreReady;
+export function screenHasCommittedContentForNativeStack(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ screenView?: any,
+) {
+ 'worklet';
+
+ if (!screenId) {
+ return false;
+ }
+ const contentWrapperView = liveScreenContentWrapperViewFromRegistry(
+ screenId,
+ registry,
+ );
+ if (contentWrapperView) {
+ return (
+ screenContentWrapperHasVisibleMountedFabricChildContent(
+ screenId,
+ registry,
+ contentWrapperView,
+ screenView,
+ ) ||
+ screenContentWrapperHasCommittedChildHostContent(
+ screenId,
+ registry,
+ contentWrapperView,
+ )
+ );
+ }
+ return screenHasVisibleHostedContent(screenId, registry, screenView);
+}
+export function modalPresentationAncestorIdForContentScreen(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+) {
+ 'worklet';
+
+ if (!screenId) {
+ return undefined;
+ }
+ const separatorIndex = screenId.indexOf(':');
+ if (separatorIndex <= 0) {
+ return undefined;
+ }
+ const modalId = screenId.slice(0, separatorIndex);
+ const modalProps = registry.screenProps?.[modalId];
+ if (!modalProps || !isModalPresentation(modalProps.stackPresentation)) {
+ return undefined;
+ }
+ return modalId;
+}
+ }
+ if (screenRequiresContentWrapperForReady(screenId, registry)) {
+ return screenContentWrapperHasCommittedHostReadyContent(
+ screenId,
+ registry,
+ screenView,
+ );
+ }
+ return screenHasCommittedContentForNativeStack(
+ screenId,
+ registry,
+ screenView,
+ );
+}
+export function screenCanCertifyContentReady(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+) {
+ 'worklet';
+
+ if (!screenId) {
+ return false;
+ }
+ const requiresContentWrapper = screenRequiresContentWrapperForReady(
+ screenId,
+ registry,
+ );
+ const hasCommittedContent = screenHasCommittedContentForReady(
+ screenId,
+ registry,
+ );
+ if (screenCanUseNativeStackControllerViewForReady(screenId, registry)) {
+ return !!(
+ screenControllerView(registry.screens?.[screenId]) && hasCommittedContent
+ );
+ }
+ return (
+ screenHostIsReady(screenId, registry) &&
+ (!requiresContentWrapper || hasCommittedContent)
+ );
+}
+export function certifyScreenContentReadyFromHostDescendants(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+) {
+ 'worklet';
+
+ if (
+ !screenId ||
+ screenContentIsReady(screenId, registry) ||
+ !screenHostIsReady(screenId, registry) ||
+ !screenHasHostReadyVisibleDescendants(screenId, registry)
+ ) {
+ return false;
+ }
+ ensureScreenContentWrapperMounted(screenId, registry);
+ if (!screenCanCertifyContentReady(screenId, registry)) {
+ return false;
+ }
+ registry.screenContentReady[screenId] = true;
+ return true;
+}
+export function stackHasActiveUIKitTransitionCoordinator(
+ stackId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ screenId?: string,
+) {
+ 'worklet';
+
+ const navigationController = stackId ? registry.stacks?.[stackId] : undefined;
+ const screenController = screenId ? registry.screens?.[screenId] : undefined;
+ return !!(
+ transitionCoordinatorForController(navigationController) ||
+ transitionCoordinatorForController(screenController) ||
+ transitionCoordinatorForController(navigationController?.topViewController)
+ );
+}
+export function shouldDeferReadyContentWrapperRefreshDuringTransition(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+) {
+ '',
+ nativeObjectStableHandle(contentWrapperView) ?? '',
+ registry.screenNativePropsKeys?.[screenId] ?? '',
+ registry.screenContentReady?.[screenId] ? 'ready' : 'pending',
+ viewLayoutKey(screenView),
+ viewWindowOriginKey(screenView),
+ viewLayoutKey(contentWrapperView),
+ viewWindowOriginKey(contentWrapperView),
+ directFabricContentWrapperHasCoherentHostedViewport(contentWrapperView)
+ ? 'coherent'
+ : 'stale',
+ nativeObjectStableHandle(screenView?.superview) ?? '',
+ nativeObjectStableHandle(contentWrapperView?.superview) ?? '',
+ ].join('|');
+}
+export function screenHasCurrentStableReadyExposure(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ screenView: any,
+) {
+ 'worklet';
+
+ if (!screenId || !screenView) {
+ return false;
+ }
+ const expectedKey = screenStableReadyExposureKey(
+ screenId,
+ registry,
+ screenView,
+ );
+ return !!(
+ expectedKey &&
+ registry.screenStableReadyExposureKeys?.[screenId] === expectedKey
+ );
+}
+export function screenHasStableReadyMountedContentBase(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ screenView: any,
+) {
+ 'worklet';
+
+ if (!screenId || !screenView || !screenContentIsReady(screenId, registry)) {
+ return false;
+ }
+ const contentWrapperView = liveScreenContentWrapperViewFromRegistry(
+ screenId,
+ registry,
+ );
+ const hasCommittedWrapperContent =
+ contentWrapperView &&
+ screenContentWrapperHasLiveMountedHostContent(
+ screenId,
+ registry,
+ contentWrapperView,
+ screenView,
+ );
+ const isModalContainerWithNestedContent =
+ isModalPresentation(registry.screenProps?.[screenId]?.stackPresentation) &&
+ presentedModalHasNestedContentScreen(screenId, registry);
+ if (isModalContainerWithNestedContent && !hasCommittedWrapperContent) {
+ traceWorkletEvent(
+ 'screen-stable-content-miss',
+ `screen=${screenId} reason=modal-nested-container wrapper=${
+ contentWrapperView ? 1 : 0
+ }`,
+ );
+ return false;
+ }
+ if (
+ contentWrapperView &&
+ !directFabricContentWrapperHasCoherentHostedViewport(contentWrapperView)
+ ) {
+ traceWorkletEvent(
+ 'screen-stable-content-miss',
+ `screen=${screenId} reason=direct-fabric-child-viewport`,
+ );
+ return false;
+ }
+ return !!(
+ (!contentWrapperView ||
+ (contentWrapperIsMountedInScreenView(contentWrapperView, screenView) &&
+ hiddenAncestorBetweenViewAndAncestor(contentWrapperView, screenView) ==
+ null)) &&
+ (hasCommittedWrapperContent ||
+ screenHasVisibleHostedContent(screenId, registry, screenView))
+ );
+}
+export function rememberScreenStableReadyExposure(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ screenView: any,
+) {
+ 'worklet';
+
+ if (
+ !screenId ||
+ !screenView ||
+ screenView.userInteractionEnabled === false ||
+ !screenHasStableReadyMountedContentBase(screenId, registry, screenView)
+ ) {
+ return false;
+ }
+ const contentWrapperView = liveScreenContentWrapperViewFromRegistry(
+ screenId,
+ registry,
+ );
+ if (
+ contentWrapperView &&
+ contentWrapperView.__nativeScriptEmptyContentWrapperShell !== true &&
+ contentWrapperView.userInteractionEnabled === false
+ ) {
+ return false;
+ }
+ const exposureKey = screenStableReadyExposureKey(
+ screenId,
+ registry,
+ screenView,
+ );
+ if (!exposureKey) {
+ return false;
+ }
+ const exposureKeys =
+ registry.screenStableReadyExposureKeys ??
+ (registry.screenStableReadyExposureKeys = {});
+ exposureKeys[screenId] = exposureKey;
+ return true;
+}
+export function screenHasStableReadyMountedContent(
+ screenId: string | undefined,
+ registry: NativeScriptStackRegistry,
+ screenView: any,
+) {
+ 'worklet';
+
+ if (!screenHasStableReadyMountedContentBase(screenId, registry, screenView)) {
diff --git a/.oracle-context/ns-rns-first-paint/relevant-tests.ts b/.oracle-context/ns-rns-first-paint/relevant-tests.ts
new file mode 100644
index 000000000..4ca7a4bd2
--- /dev/null
+++ b/.oracle-context/ns-rns-first-paint/relevant-tests.ts
@@ -0,0 +1,597 @@
+ expect(refreshUIKitHostView).toHaveBeenCalled();
+ expect(
+ globalObject.__rnsNativeScriptLayoutHostedSubviewChain,
+ ).toHaveBeenCalled();
+ expect(registry.screenContentWrapperHostReadyKeys.screen).toBe(
+ 'wrapper-handle|77|window',
+ );
+ expect(registry.screenContentWrapperVisibleDescendantCounts.screen).toBe(
+ 77,
+ );
+ expect(registry.screenContentReady.screen).toBe(true);
+ } finally {
+ NativeScriptRuntime.refreshUIKitHostViewDirectOwner =
+ previousRefreshUIKitHostView;
+
+ if (previousNativeObjectFromHandle === undefined) {
+ delete (NativeScriptRuntime as any).nativeObjectFromHandle;
+ } else {
+ (NativeScriptRuntime as any).nativeObjectFromHandle =
+ previousNativeObjectFromHandle;
+ }
+
+ if (previousLayoutHostedSubviewChain === undefined) {
+ delete globalObject.__rnsNativeScriptLayoutHostedSubviewChain;
+ } else {
+ globalObject.__rnsNativeScriptLayoutHostedSubviewChain =
+ previousLayoutHostedSubviewChain;
+ }
+
+ if (previousRegistry === undefined) {
+ delete globalObject.__rnsNativeScriptStackRegistry;
+ } else {
+ globalObject.__rnsNativeScriptStackRegistry = previousRegistry;
+ }
+ }
+ });
+
+ it('does not certify a displayable empty wrapper as committed stack content', () => {
+ const NativeScriptRuntime = jest.requireActual(
+ '@nativescript/react-native',
+ );
+ const globalObject = global as Record;
+ const previousRegistry = globalObject.__rnsNativeScriptStackRegistry;
+ const previousReconcileStack = globalObject.__rnsNativeScriptReconcileStack;
+ const previousNativeObjectFromHandle = (NativeScriptRuntime as any)
+ .nativeObjectFromHandle;
+ const previousRefreshUIKitHostView =
+ NativeScriptRuntime.refreshUIKitHostViewDirectOwner;
+ const frame = {
+ origin: { x: 0, y: 0 },
+ size: { height: 874, width: 402 },
+ };
+ const window = {};
+ const screenView: any = {
+ __rnsNativeScriptScreenView: true,
+ alpha: 1,
+ bounds: frame,
+ frame,
+ hash: 'detail-screen-host',
+ hidden: false,
+ refreshSurfaceTouchHandler: jest.fn(),
+ subviews: [],
+ userInteractionEnabled: true,
+ window,
+ };
+ const emptyContentWrapperView: any = {
+ __rnsNativeScriptScreenContentWrapperChildrenView: null,
+ __rnsNativeScriptScreenContentWrapperRootView: true,
+ alpha: 1,
+ bounds: frame,
+ description: 'RNSScreenContentWrapper.NativeScript',
+ frame,
+ hash: 'detail-wrapper-host',
+ hidden: false,
+ subviews: [],
+ superview: screenView,
+ userInteractionEnabled: true,
+ window,
+ };
+ const detailController: any = {
+ __nativeScriptScreenId: 'detail',
+ __nativeScriptScreenView: screenView,
+ view: screenView,
+ };
+ const navigationController: any = {
+ topViewController: detailController,
+ view: { bounds: frame, frame, window },
+ viewControllers: [detailController],
+ };
+ const reconcileStack = jest.fn();
+ const registry: any = {
+ screenContentReady: {},
+ screenContentWrapperCommittedHandles: {},
+ screenContentWrapperHostHandles: {},
+ screenContentWrapperHostReadyKeys: {},
+ screenContentWrapperRefreshKeys: {},
+ screenContentWrapperViews: {},
+ screenContentWrapperVisibleDescendantCounts: {},
+ screenHostHandles: { detail: 'detail-screen-host' },
+ screenHostRefreshKeys: {},
+ screenParents: { detail: 'stack' },
+ screenProps: { detail: { screenId: 'detail' } },
+ screenSurfaceTouchRefreshKeys: {},
+ screens: { detail: detailController },
+ stackActiveScreenIds: { stack: ['root', 'detail'] },
+ stackContexts: { stack: {} },
+ stackNativeCounts: { stack: 1 },
+ stackNativeKeys: { stack: 'root' },
+ stackPendingReconcileKeys: {},
+ stackTransitioning: {},
+ stacks: { stack: navigationController },
+ stackUpdatingModals: {},
+ };
+
+ screenView.subviews = [emptyContentWrapperView];
+ globalObject.__rnsNativeScriptReconcileStack = reconcileStack;
+ globalObject.__rnsNativeScriptStackRegistry = registry;
+ NativeScriptRuntime.refreshUIKitHostViewDirectOwner = jest.fn(() => false);
+ (NativeScriptRuntime as any).nativeObjectFromHandle = jest.fn(
+ (handle: string) => {
+ if (handle === 'detail-wrapper-host') {
+ return emptyContentWrapperView;
+ }
+
+ if (handle === 'detail-screen-host') {
+ return screenView;
+ }
+
+ return null;
+ },
+ );
+
+ try {
+ nativeScriptScreenContentWrapperHostReadyOnUI(
+ 'detail',
+ 'detail-wrapper-host',
+ 0,
+ true,
+ );
+
+ expect(registry.screenContentReady.detail).toBeUndefined();
+ expect(
+ registry.screenContentWrapperCommittedHandles.detail,
+ ).toBeUndefined();
+ expect(
+ __nativeScriptScreenHasStableReadyMountedContentForTests(
+ 'detail',
+ registry,
+ screenView,
+ ),
+ ).toBe(false);
+ } finally {
+ NativeScriptRuntime.refreshUIKitHostViewDirectOwner =
+ previousRefreshUIKitHostView;
+
+ if (previousNativeObjectFromHandle === undefined) {
+ delete (NativeScriptRuntime as any).nativeObjectFromHandle;
+ } else {
+ (NativeScriptRuntime as any).nativeObjectFromHandle =
+ previousNativeObjectFromHandle;
+ }
+
+ if (previousReconcileStack === undefined) {
+ delete globalObject.__rnsNativeScriptReconcileStack;
+ } else {
+ globalObject.__rnsNativeScriptReconcileStack = previousReconcileStack;
+ }
+
+ if (previousRegistry === undefined) {
+ delete globalObject.__rnsNativeScriptStackRegistry;
+ } else {
+ globalObject.__rnsNativeScriptStackRegistry = previousRegistry;
+ }
+ }
+ });
+
+ it('waits for Fabric content certification before native-stack UIKit work', () => {
+ const NativeScriptRuntime = jest.requireActual(
+ '@nativescript/react-native',
+ );
+ const previousNativeObjectFromHandle = (NativeScriptRuntime as any)
+ .nativeObjectFromHandle;
+ const frame = {
+ origin: { x: 0, y: 0 },
+ size: { height: 874, width: 402 },
+ };
+ const window = {};
+ const rootScreenView: any = {
+ __rnsNativeScriptScreenView: true,
+ alpha: 1,
+ bounds: frame,
+ frame,
+ hash: 'root-screen-host',
+ hidden: false,
+ subviews: [],
+ userInteractionEnabled: true,
+ window,
+ };
+ const detailScreenView: any = {
+ __rnsNativeScriptScreenView: true,
+ alpha: 1,
+ bounds: frame,
+ frame,
+ hash: 'detail-screen-host',
+ hidden: false,
+ subviews: [],
+ userInteractionEnabled: true,
+ window,
+ };
+ const rootHostedLeaf: any = {
+ alpha: 1,
+ description: 'RCTViewComponentView',
+ hidden: false,
+ subviews: [],
+ userInteractionEnabled: true,
+ window,
+ };
+ const rootWrapperView: any = {
+ __rnsNativeScriptScreenContentWrapperRootView: true,
+ alpha: 1,
+ bounds: frame,
+ description: 'RNSScreenContentWrapper.NativeScript',
+ frame,
+ hash: 'root-wrapper-host',
+ hidden: false,
+ subviews: [rootHostedLeaf],
+ superview: rootScreenView,
+ userInteractionEnabled: true,
+ window,
+ };
+ const detailWrapperView: any = {
+ __rnsNativeScriptScreenContentWrapperRootView: true,
+ alpha: 1,
+ bounds: frame,
+ description: 'RNSScreenContentWrapper.NativeScript',
+ frame,
+ hash: 'detail-wrapper-host',
+ hidden: false,
+ subviews: [],
+ superview: detailScreenView,
+ userInteractionEnabled: true,
+ window,
+ };
+ const detailHostedLeaf: any = {
+ alpha: 1,
+ description: 'RCTViewComponentView',
+ hidden: false,
+ subviews: [],
+ userInteractionEnabled: true,
+ window,
+ };
+ rootHostedLeaf.superview = rootWrapperView;
+ rootScreenView.subviews = [rootWrapperView];
+ detailScreenView.subviews = [detailWrapperView];
+ const rootController: any = {
+ __nativeScriptScreenId: 'root',
+ __nativeScriptScreenView: rootScreenView,
+ view: rootScreenView,
+ };
+ const detailController: any = {
+ __nativeScriptScreenId: 'detail',
+ __nativeScriptScreenView: detailScreenView,
+ view: detailScreenView,
+ };
+ const navigationController: any = {
+ topViewController: rootController,
+ view: { bounds: frame, frame, window },
+ viewControllers: [rootController],
+ };
+ const registry: any = {
+ screenContentReady: { root: true },
+ screenContentWrapperCommittedHandles: { root: 'root-wrapper-host' },
+ screenContentWrapperHostHandles: {
+ root: 'root-wrapper-host',
+ },
+ screenContentWrapperHostReadyKeys: {},
+ screenContentWrapperMountedFabricChildHandles: {
+ root: 'root-wrapper-host',
+ },
+ screenContentWrapperRefreshKeys: {},
+ screenContentWrapperViews: {
+ root: rootWrapperView,
+ },
+ screenContentWrapperVisibleDescendantCounts: {
+ root: 1,
+ },
+ screenHostHandles: {
+ detail: 'detail-screen-host',
+ root: 'root-screen-host',
+ },
+ screenParents: { detail: 'stack', root: 'stack' },
+ screenProps: {
+ detail: { activityState: 2, screenId: 'detail' },
+ root: { activityState: 2, screenId: 'root' },
+ },
+ screenSurfaceTouchRefreshKeys: {},
+ screens: { detail: detailController, root: rootController },
+ stackActiveScreenIds: { stack: ['root', 'detail'] },
+ stackContexts: { stack: {} },
+ stackMountedScreenIds: { stack: ['root', 'detail'] },
+ stackNativeCounts: { stack: 1 },
+ stackNativeKeys: { stack: 'root' },
+ stackPendingReconcileKeys: {},
+ stackTransitioning: {},
+ stackUsesFabricMountedChildren: { stack: true },
+ stackUpdatingModals: {},
+ stacks: { stack: navigationController },
+ };
+
+ (NativeScriptRuntime as any).nativeObjectFromHandle = jest.fn(
+ (handle: string) => {
+ if (handle === 'root-wrapper-host') {
+ return rootWrapperView;
+ }
+
+ if (handle === 'detail-wrapper-host') {
+ return detailWrapperView;
+ }
+
+ if (handle === 'root-screen-host') {
+ return rootScreenView;
+ }
+
+ if (handle === 'detail-screen-host') {
+ return detailScreenView;
+ }
+
+ return null;
+ },
+ );
+
+ try {
+ expect(
+ __nativeScriptStackTopScreenCanStartNativeUpdateForTests(
+ ['root', 'detail'],
+ registry,
+ 'stack',
+ ),
+ ).toBe(false);
+ expect(
+ __nativeScriptStackCanReconcileFromPropsUpdateForTests(
+ 'stack',
+ registry,
+ ['root', 'detail'],
+ ),
+ ).toBe(false);
+ expect(registry.screenContentReady.detail).toBeUndefined();
+
+ registry.screenProps.detail.stackPresentation = 'modal';
+ registry.screenContentReady.detail = undefined;
+
+ expect(
+ __nativeScriptStackTopScreenCanStartNativeUpdateForTests(
+ ['root', 'detail'],
+ registry,
+ 'stack',
+ ),
+ ).toBe(false);
+ expect(
+ __nativeScriptStackCanReconcileFromPropsUpdateForTests(
+ 'stack',
+ registry,
+ ['root', 'detail'],
+ ),
+ ).toBe(false);
+
+ registry.screenContentWrapperHostHandles.detail = 'detail-wrapper-host';
+ registry.screenContentWrapperViews.detail = detailWrapperView;
+ registry.screenContentWrapperVisibleDescendantCounts.detail = 0;
+ registry.screenProps.detail.stackPresentation = undefined;
+
+ expect(
+ __nativeScriptStackTopScreenCanStartNativeUpdateForTests(
+ ['root', 'detail'],
+ registry,
+ 'stack',
+ ),
+ ).toBe(false);
+ expect(registry.screenContentReady.detail).toBeUndefined();
+
+ registry.screenProps.detail.stackPresentation = 'modal';
+ registry.screenContentReady.detail = undefined;
+
+ expect(
+ __nativeScriptStackTopScreenCanStartNativeUpdateForTests(
+ ['root', 'detail'],
+ registry,
+ 'stack',
+ ),
+ ).toBe(false);
+ expect(registry.screenContentReady.detail).toBeUndefined();
+ registry.screenProps.detail.stackPresentation = undefined;
+
+ registry.screenContentWrapperMountedFabricChildHandles.detail =
+ 'detail-wrapper-host';
+
+ expect(
+ __nativeScriptStackTopScreenCanStartNativeUpdateForTests(
+ ['root', 'detail'],
+ registry,
+ 'stack',
+ ),
+ ).toBe(false);
+ expect(registry.screenContentReady.detail).toBeUndefined();
+ expect(
+ registry.screenContentWrapperCommittedHandles.detail,
+ ).toBeUndefined();
+ expect(
+ __nativeScriptStackCanReconcileFromPropsUpdateForTests(
+ 'stack',
+ registry,
+ ['root', 'detail'],
+ ),
+ ).toBe(false);
+
+ detailHostedLeaf.superview = detailWrapperView;
+ detailWrapperView.subviews = [detailHostedLeaf];
+ registry.screenContentWrapperVisibleDescendantCounts.detail = 1;
+ registry.screenProps.detail.stackPresentation = undefined;
+ registry.screenContentReady.detail = undefined;
+
+ expect(
+ __nativeScriptStackTopScreenCanStartNativeUpdateForTests(
+ ['root', 'detail'],
+ registry,
+ 'stack',
+ ),
+ ).toBe(true);
+ expect(
+ __nativeScriptStackCanReconcileFromPropsUpdateForTests(
+ 'stack',
+ registry,
+ ['root', 'detail'],
+ ),
+ ).toBe(true);
+ expect(registry.screenContentReady.detail).toBe(true);
+
+ registry.screenContentReady.detail = undefined;
+ registry.stackNativeKeys.stack = undefined;
+ registry.stackNativeCounts.stack = 0;
+
+ expect(
+ __nativeScriptStackTopScreenCanStartNativeUpdateForTests(
+ ['root', 'detail'],
+ registry,
+ 'stack',
+ ),
+ ).toBe(true);
+ expect(
+ __nativeScriptStackCanReconcileFromPropsUpdateForTests(
+ 'stack',
+ const pushSource = stackSource.slice(
+ stackSource.indexOf('const isPush ='),
+ stackSource.indexOf('if (isPop && nativeIds.length > nextCount)'),
+ );
+ const popSource = stackSource.slice(
+ stackSource.indexOf('if (isPop && nativeIds.length > nextCount)'),
+ stackSource.indexOf(
+ 'const previousTopScreenId = previousIds[previousCount - 1]',
+ ),
+ );
+ const beforeNativeIndex = pushSource.indexOf(
+ "'reconcileStack-push-before-native'",
+ );
+ const animateCallIndex = pushSource.indexOf(
+ 'didAnimatePush = animateStackPush(',
+ );
+ const markTransitionIndex = pushSource.indexOf(
+ 'const token = markTransition',
+ );
+ const prepareExposureIndex = pushSource.indexOf(
+ 'prepareControllerForStackExposure(',
+ );
+ const prepareVisibleIndex = pushSource.indexOf(
+ 'prepareVisibleScreenForImmediateInteraction(',
+ pushSource.indexOf('if (didAnimatePush) {'),
+ );
+ const emitTransitionIndex = pushSource.indexOf(
+ "emitTransition(ctx, 'start', false, pushedScreenId)",
+ );
+
+ expect(stackSource).not.toContain('stackContentReadyRetryTokens');
+ expect(stackSource).not.toContain('function scheduleContentReadyReconcile');
+ expect(stackSource).not.toContain(
+ 'function pushedScreenHostedContentIsReady',
+ );
+ expect(pushSource).not.toContain('configureScreenControllerHeader(');
+ expect(pushSource).not.toContain('configureSourceBackButton(');
+ expect(stackSource).toContain('configureHeaderForWillShowViewController(');
+ expect(pushSource).toContain('prepareControllerForStackExposure(');
+ expectSourceToContain(
+ pushSource,
+ 'prepareControllerForStackExposure(\n' +
+ ' navigationController,\n' +
+ ' pushedScreenId,\n' +
+ ' pushedController,\n' +
+ ' registry,\n' +
+ ' ctx,\n' +
+ ' true,\n' +
+ ' nextCount > 1,\n' +
+ ' true,\n' +
+ ' true,\n' +
+ ' true,\n' +
+ ' );',
+ );
+ expect(pushSource).not.toContain('pushedScreenHasReadyHostDescendants');
+ expect(pushSource).not.toContain('pushedScreenHasVisibleHostedContent');
+ expect(pushSource).not.toContain('shouldForcePushedContentRefresh');
+ expectSourceToContain(
+ stackSource,
+ 'const contentIsReady = screenContentIsReady(screenId, registry);\n' +
+ ' const hasCommittedNativeStackContent = screenHasCommittedContentForNativeStack(\n' +
+ ' screenId,\n' +
+ ' registry,\n' +
+ ' controllerView,\n' +
+ ' );\n' +
+ 'const alreadyPreparedCommittedContent =\n' +
+ ' contentIsReady &&\n' +
+ ' (!contentWrapperView || wrapperAlreadyMounted) &&\n' +
+ ' hasCommittedNativeStackContent;',
+ );
+ const nativeTransitionPrepareSource = stackSource.slice(
+ stackSource.indexOf(
+ 'function prepareScreenHostedContentForNativeTransition',
+ ),
+ stackSource.indexOf(
+ 'export const __nativeScriptPrepareScreenHostedContentForNativeTransitionForTests',
+ ),
+ );
+ expect(nativeTransitionPrepareSource).not.toContain(
+ 'screenHasCurrentStableReadyExposure',
+ );
+ expectSourceToContain(
+ stackSource,
+ 'refreshScreenContentReady(screenId, controller, registry, true);\n\n' +
+ ' const didNormalizeWrapper = ensureScreenContentWrapperMounted(',
+ );
+ expectSourceToContain(
+ stackSource,
+ 'const needsContentRefreshAfterRefresh =\n' +
+ ' refreshHostedContent && !screenContentIsReady(screenId, registry);',
+ );
+ expectSourceToContain(
+ stackSource,
+ 'if (controllerView && needsContentRefreshAfterRefresh) {\n' +
+ ' layoutScreenHostedReactSubviews(',
+ );
+ expectSourceToContain(
+ stackSource,
+ 'if (needsContentRefreshAfterRefresh) {\n' +
+ ' refreshScreenContentWrapperHost(',
+ );
+ expectSourceToContain(
+ pushSource,
+ 'const didPreparePushedContent =\n' +
+ ' prepareScreenHostedContentForNativeTransition(\n' +
+ ' pushedController,\n' +
+ ' pushedScreenId,\n' +
+ ' registry,\n' +
+ ' );',
+ );
+ expectSourceToContain(
+ pushSource,
+ 'const pushedContentIsReady = screenContentIsReady(\n' +
+ ' pushedScreenId,\n' +
+ ' registry,\n' +
+ ' );',
+ );
+ const postAnimateSource = pushSource.slice(
+ pushSource.indexOf('if (didAnimatePush) {'),
+ pushSource.indexOf(
+ 'scheduleTransitionFallback(',
+ pushSource.indexOf('if (didAnimatePush) {'),
+ ),
+ );
+ expect(postAnimateSource).not.toContain(
+ 'layoutNavigationStackViews(navigationController);',
+ );
+ expect(postAnimateSource).not.toContain('flushKnownUIKitHostView(');
+ expect(postAnimateSource).toContain(
+ 'prepareVisibleScreenForImmediateInteraction(',
+ );
+ expect(popSource).not.toContain(
+ 'prepareVisibleScreenForImmediateInteraction(',
+ );
+ expect(popSource).toContain('prepareRevealedScreenForNativePop(');
+ expect(
+ popSource.indexOf('prepareRevealedScreenForNativePop('),
+ ).toBeLessThan(popSource.indexOf('const token = markTransition'));
+ expect(popSource).toContain('if (!didPrepareRevealedScreen) {');
+ expect(popSource).not.toContain('prepareControllerForStackExposure(');
+ expect(pushSource).not.toMatch(
+ /if\s*\(\s*!didPreparePushedContent\s*&&\s*!screenContentIsReady\(\s*pushedScreenId,\s*registry,?\s*\)\s*\)/,
+ );
+ expect(pushSource).not.toContain(
+ '!screenHeaderSubviewsAreReady(pushedScreenId, registry)',
+ );
diff --git a/.oracle-context/ns-rns-first-paint/rns-port-trace-export-src.json b/.oracle-context/ns-rns-first-paint/rns-port-trace-export-src.json
new file mode 100644
index 000000000..7aa07d37e
--- /dev/null
+++ b/.oracle-context/ns-rns-first-paint/rns-port-trace-export-src.json
@@ -0,0 +1,127 @@
+{
+ "demo": [],
+ "exportedAt": 43694914,
+ "nsrns": [
+ "[NSRNS_TRACE 43694435] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00",
+ "[NSRNS_TRACE 43694437] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00",
+ "[NSRNS_TRACE 43694437] content-wrapper-ensure screen=Home-FXqHwBeMBsCdyK5TIjTRS screenView=0x15d960e00 wrapper=0x15d960700 super=0x15d960e00 mounted=1 wrapperWindow=0 screenWindow=0",
+ "[NSRNS_TRACE 43694446] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694447] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694450] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694450] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694454] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694455] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694456] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694456] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x176200e00 container=0x16ee1de00 native=0x16f9af100 children=0x16f9af100 resolved=0x16f9af100 screenView=0x16f9cca80",
+ "[NSRNS_TRACE 43694456] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1",
+ "[NSRNS_TRACE 43694456] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694456] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694461] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694461] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694461] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694468] touch-refresh view=0x16f9cca80 screen=1 should=1 own=1 attached=1 window=0x104a00000 super=0x16f9eed80",
+ "[NSRNS_TRACE 43694468] screen-touch-refresh screen=Detail--si7XIE3EagJ78yAR7spP did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function",
+ "[NSRNS_TRACE 43694469] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694469] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694470] content-wrapper-resolve-nearest-screen-child screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16fa08700 screenView=0x16f9cca80 resolved=0x176201500 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694470] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x176201500 container=0x16ee1e100 native=0x16fa08700 children=0x16fa08700 resolved= screenView=0x16f9cca80",
+ "[NSRNS_TRACE 43694471] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694471] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x16f9af100 container=0x16f9af100 native= children= resolved=0x16f9af100 screenView=0x16f9cca80",
+ "[NSRNS_TRACE 43694471] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1",
+ "[NSRNS_TRACE 43694471] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694471] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694475] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694475] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694475] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694482] touch-refresh view=0x16f9cca80 screen=1 should=1 own=1 attached=1 window=0x104a00000 super=0x16f9eed80",
+ "[NSRNS_TRACE 43694483] screen-touch-refresh screen=Detail--si7XIE3EagJ78yAR7spP did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function",
+ "[NSRNS_TRACE 43694483] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694483] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694484] content-wrapper-resolve-fallback screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16fa75900 screenView=0x16f9cca80 resolved= super=",
+ "[NSRNS_TRACE 43694484] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x16fa75900 container=0x16fa75900 native= children= resolved= screenView=0x16f9cca80",
+ "[NSRNS_TRACE 43694487] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694487] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694497] stack-host-transaction-enter stack=rn-ns-stack-1 active=Home-FXqHwBeMBsCdyK5TIjTRS,Detail--si7XIE3EagJ78yAR7spP revision=2 fabricChildren=3 props=1",
+ "[NSRNS_TRACE 43694500] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00",
+ "[NSRNS_TRACE 43694500] content-wrapper-ensure screen=Home-FXqHwBeMBsCdyK5TIjTRS screenView=0x15d960e00 wrapper=0x15d960700 super=0x15d960e00 mounted=1 wrapperWindow=0 screenWindow=0",
+ "[NSRNS_TRACE 43694509] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694509] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694514] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00",
+ "[NSRNS_TRACE 43694514] content-wrapper-host-ready-resolved screen=Home-FXqHwBeMBsCdyK5TIjTRS handle=0x15d960700 resolved=0x15d960700 registered=0x15d960700 window=0 descendants=77",
+ "[NSRNS_TRACE 43694515] content-wrapper-host-ready screen=Home-FXqHwBeMBsCdyK5TIjTRS stack=rn-ns-stack-1 modal=0 ready=1 previousReady=1 descendants=77 window=0",
+ "[NSRNS_TRACE 43694515] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00",
+ "[NSRNS_TRACE 43694515] content-wrapper-host-ready-noop-reject screen=Home-FXqHwBeMBsCdyK5TIjTRS stack=rn-ns-stack-1 reason=offwindow previous=0x15d960700 handle=0x15d960700 previousReady=0x15d960700|77|window ready=0x15d960700|77|offwindow window=0 pending= native=Home-FXqHwBeMBsCdyK5TIjTRS\u001fDetail--si7XIE3EagJ78yAR7spP didAppear=1",
+ "[NSRNS_TRACE 43694515] content-wrapper-host-ready-state screen=Home-FXqHwBeMBsCdyK5TIjTRS stack=rn-ns-stack-1 contentReady=1 wrapperCanStart=0 canUse=1 ctx=1 defer=0 skipHosted=0 prepareTab=0 transitioning=1",
+ "[NSRNS_TRACE 43694515] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00",
+ "[NSRNS_TRACE 43694515] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00",
+ "[NSRNS_TRACE 43694515] content-wrapper-ensure screen=Home-FXqHwBeMBsCdyK5TIjTRS screenView=0x15d960e00 wrapper=0x15d960700 super=0x15d960e00 mounted=1 wrapperWindow=0 screenWindow=0",
+ "[NSRNS_TRACE 43694521] screen-touch-refresh-decision screen=Home-FXqHwBeMBsCdyK5TIjTRS reason=content-wrapper-host-ready screenView=0x15d960e00 controller=4372564992 wrapper=0x15d960700 current=0 skip=0 skipHosted=0 transitioning=1 defer=0 nonTopDefer=0 offwindow=0 forceLayout=0 forceTouch=0 requestedTouch=0 reattach=0 alreadyLayout=0",
+ "[NSRNS_TRACE 43694522] screen-touch-refresh screen=Home-FXqHwBeMBsCdyK5TIjTRS did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function",
+ "[NSRNS_TRACE 43694522] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00",
+ "[NSRNS_TRACE 43694522] content-wrapper-ensure screen=Home-FXqHwBeMBsCdyK5TIjTRS screenView=0x15d960e00 wrapper=0x15d960700 super=0x15d960e00 mounted=1 wrapperWindow=0 screenWindow=0",
+ "[NSRNS_TRACE 43694531] content-wrapper-host-ready-total 16ms screen=Home-FXqHwBeMBsCdyK5TIjTRS",
+ "[NSRNS_TRACE 43694535] stack-host-transaction-enter stack=rn-ns-stack-1 active=Home-FXqHwBeMBsCdyK5TIjTRS,Detail--si7XIE3EagJ78yAR7spP revision=2 fabricChildren=3 props=1",
+ "[NSRNS_TRACE 43694537] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694537] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694540] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694540] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694544] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694545] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694572] transition-fallback-deferred-coordinator-miss stack=rn-ns-stack-1 screen=Detail--si7XIE3EagJ78yAR7spP closing=0",
+ "[NSRNS_TRACE 43694575] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1",
+ "[NSRNS_TRACE 43694586] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9afd40 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694586] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x176201880 container=0x16ee1e400 native=0x16f9afd40 children=0x16f9ad180 resolved=0x16f9af100 screenView=0x16f9cca80",
+ "[NSRNS_TRACE 43694586] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1",
+ "[NSRNS_TRACE 43694586] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694587] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694591] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694591] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694592] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694604] touch-refresh view=0x16f9cca80 screen=1 should=1 own=1 attached=1 window=0x104a00000 super=0x16f9eed80",
+ "[NSRNS_TRACE 43694605] screen-touch-refresh screen=Detail--si7XIE3EagJ78yAR7spP did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function",
+ "[NSRNS_TRACE 43694605] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694605] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694606] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694606] content-wrapper-transaction-sync screen=Detail--si7XIE3EagJ78yAR7spP children=1 mounted=1 ready=1",
+ "[NSRNS_TRACE 43694606] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694606] content-wrapper-host-ready-resolved screen=Detail--si7XIE3EagJ78yAR7spP handle=0x16f9af100 resolved=0x16f9af100 registered=0x16f9af100 window=0 descendants=1",
+ "[NSRNS_TRACE 43694606] content-wrapper-host-ready-ignore-offwindow-stale screen=Detail--si7XIE3EagJ78yAR7spP stack=rn-ns-stack-1 descendants=1",
+ "[NSRNS_TRACE 43694608] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694608] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x176200e00 container=0x16ee1de00 native=0x16f9af100 children=0x16f9af100 resolved=0x16f9af100 screenView=0x16f9cca80",
+ "[NSRNS_TRACE 43694608] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1",
+ "[NSRNS_TRACE 43694608] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694608] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694613] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694613] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694614] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694621] touch-refresh view=0x16f9cca80 screen=1 should=1 own=1 attached=1 window=0x104a00000 super=0x16f9eed80",
+ "[NSRNS_TRACE 43694622] screen-touch-refresh screen=Detail--si7XIE3EagJ78yAR7spP did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function",
+ "[NSRNS_TRACE 43694622] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694622] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694624] content-wrapper-resolve-nearest-screen-child screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16fa08700 screenView=0x16f9cca80 resolved=0x176201500 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694624] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x176201500 container=0x16ee1e100 native=0x16fa08700 children=0x16fa08700 resolved= screenView=0x16f9cca80",
+ "[NSRNS_TRACE 43694624] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694624] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x16f9af100 container=0x16f9af100 native= children= resolved=0x16f9af100 screenView=0x16f9cca80",
+ "[NSRNS_TRACE 43694624] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1",
+ "[NSRNS_TRACE 43694624] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694625] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694630] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694630] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694630] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694638] touch-refresh view=0x16f9cca80 screen=1 should=1 own=1 attached=1 window=0x104a00000 super=0x16f9eed80",
+ "[NSRNS_TRACE 43694639] screen-touch-refresh screen=Detail--si7XIE3EagJ78yAR7spP did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function",
+ "[NSRNS_TRACE 43694639] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694639] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694640] content-wrapper-resolve-fallback screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16fa75900 screenView=0x16f9cca80 resolved= super=",
+ "[NSRNS_TRACE 43694640] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x16fa75900 container=0x16fa75900 native= children= resolved= screenView=0x16f9cca80",
+ "[NSRNS_TRACE 43694643] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694643] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694649] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694649] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694652] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694652] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1",
+ "[NSRNS_TRACE 43694657] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80",
+ "[NSRNS_TRACE 43694657] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80"
+ ],
+ "reason": "push"
+}
\ No newline at end of file
diff --git a/HANDOFF-FABLE5.md b/HANDOFF-FABLE5.md
new file mode 100644
index 000000000..26d216c6f
--- /dev/null
+++ b/HANDOFF-FABLE5.md
@@ -0,0 +1,1412 @@
+# Handoff for Claude (Fable 5) — NativeScript react-native-screens Parity
+
+> Written 2026-07-05 by a fresh assistant, distilled from the Codex session
+> `019f0fa7` (thread "NativeScriptRuntime", cwd `/Users/dj/Developer/NativeScriptRuntime`)
+> plus the on-disk `HANDOFF.md` / `PROGRESS.md` / `RN_API.md`.
+>
+> This is a concise entry point. The authoritative running log is the existing
+> `HANDOFF.md` (176 KB, dated updates through 2026-07-02) and `PROGRESS.md`
+> (803 KB). Read those for full detail. This file captures the goal, the current
+> state as of the last session turn (2026-07-05 20:52 EDT), and how to continue.
+
+## Goal
+
+Reach practical **parity with upstream `react-native-screens` (RNS)** for the
+NativeScript TypeScript / UIKit port. The port must follow upstream RNS
+mechanics as closely as possible. Where NativeScript lacks a primitive that
+upstream RNS/native/Fabric relies on, add the **generic** primitive to the
+NativeScript React Native runtime / Fabric / UI-worklet surface — do not add
+port-specific hacks.
+
+The goal is **not complete**. Do not mark it complete until the port matches
+original RNS behavior on the dedicated simulators in pixels, latency, touch
+reliability, and modal / stack / tab behavior.
+
+## Hard rules (from the user — keep following these)
+
+- No subagents.
+- No hacks that hide races: no fixed delays, timers, or retry loops.
+- Do **not** add native ObjC/Swift module code for the RNS port. UIKit
+ views/controllers are implemented in TS through NativeScript. If a primitive
+ is missing, expose it generically from the runtime, then use it from TS.
+- Pixel-visible / SimDeck behavior is the source of truth. Passing Jest + AX /
+ layout state is **not** proof — tests have gone green while pixels were wrong.
+- Work one parity bug at a time, always comparing against the original RNS app.
+- Test on the dedicated **simulators only** — no physical iPhone/iPad for this PR.
+- Keep RN-module work isolated from the `refactor` (Node-API) branch.
+
+## Repos & branch
+
+- **Runtime (this repo):** `/Users/dj/Developer/NativeScriptRuntime`
+ - Branch: **`codex/rn-module-fabric-turbomodule-worklets`** (draft PR #46),
+ created from `refactor` at `f3d0b3f4ac6f6ff5753d321e7bb7ecc7e78f3443`.
+ - `refactor` branch is reserved for the separate Node-API / direct-engine
+ backend PR — do not mix RN-module work into it.
+ - HEAD: `2730d717 docs: update RNS parity progress`. Working tree is **dirty**
+ (~38 changed files); this is expected. Do not revert prior/user changes.
+- **Screens fork (most active code):** `/Users/dj/Developer/RNModuleForks/react-native-screens`
+- **Port demo app (simulator testbed):** `/Users/dj/Developer/RNModuleForks/nativescript-uikit-demo`
+- **Original comparison app:** `/Users/dj/Developer/RNModuleForks/nativescript-uikit-demo-original`
+
+Always check status before editing (both repos are dirty):
+
+```sh
+git -C /Users/dj/Developer/NativeScriptRuntime status --short
+git -C /Users/dj/Developer/RNModuleForks/react-native-screens status --short
+```
+
+## Simulators & Metro
+
+- **Port sim:** `NS Screens Only iPhone 17 178137`
+ - UDID `BF759806-2EBB-49ED-AD8E-413A7790ADE0`, bundle `org.nativescript.uikit.demo`
+ - Port Metro on `8082`, dev-client URL:
+ `nativescriptuikitdemo://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8082`
+- **Original sim:** `NS Screens Original iPhone 17 178137`
+ - UDID `3931FD88-6C29-44AA-BD73-4A40C4334B5B`, bundle `org.nativescript.uikit.demo.original`
+ - Original Metro on `8083`, dev-client URL:
+ `originalnativetabsdemo://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8083`
+
+Drive/inspect the sim with **SimDeck** (`simdeck use `, browser at
+`http://127.0.0.1:4310`). If SimDeck returns malformed HTTP / connection
+refusals, fall back to Callstack's **`agent-device`** CLI (npm package
+`agent-device`, not `@callstack/…`):
+
+```sh
+npx --yes agent-device open org.nativescript.uikit.demo --platform ios --udid BF759806-2EBB-49ED-AD8E-413A7790ADE0
+npx --yes agent-device snapshot -i --platform ios --udid BF759806-2EBB-49ED-AD8E-413A7790ADE0
+xcrun simctl io BF759806-2EBB-49ED-AD8E-413A7790ADE0 screenshot /tmp/port.png
+```
+
+> Note: earlier in the week `simctl`/CoreSimulatorService was intermittently
+> returning `Connection refused` / `DEVICE_NOT_FOUND`, which blocked pixel
+> verification for several turns. As of the 2026-07-05 session the simulator was
+> working again and pixel probes were running. If it breaks again, that harness
+> state — not the port code — is usually the blocker.
+
+## Update 2026-07-05 ~23:30 EDT (Fable session): modal blank body FIXED, latency parity still open
+
+Root causes found (simulator + `NS_NS_FABRIC_DEBUG=1` native mutation log + lldb
+view-hierarchy dump — all three were needed):
+
+1. **Present-before-children (ordering).** Fabric applies `Update` mutations
+ before `Insert` mutations within one transaction. The immediate reconcile
+ added on 2026-07-05 fired from the root stack's `updateProps` —
+ mid-transaction — so `presentViewController` ran before ANY of the modal
+ subtree inserts, and present's ~150 ms of synchronous UIKit work then
+ blocked the rest of the mutation loop. Upstream `RNSScreenStackView` only
+ presents from `mountingTransactionDidMount` → `dispatch_async(main)` →
+ `maybeAddToParentAndUpdateContainer` (see ios/RNSScreenStack.mm:1283-1370)
+ — i.e. after all inserts *and* layout.
+ **Fix:** `stackCanApplyPendingPushBeforeMountedChildren` now refuses when
+ the pending (not-yet-Fabric-mounted) screens include a modal presentation
+ (`pendingPushScreenIdsIncludeModalPresentation`, chunk2). The modal present
+ defers to the existing transaction path → `schedulePostLayoutStackReconcile`
+ (the port's equivalent of upstream's post-layout dispatch_async).
+
+2. **Wrong view remembered as content wrapper (paint).** In dev, RNS's
+ `DebugContainer` wraps modal content in RN's `AppContainer` (for LogBox),
+ which renders a sibling `DebuggingOverlay` view AFTER the content wrapper.
+ The port's heuristic wrapper resolution (nearest full-size RCTView child)
+ flip-flopped between the real wrapper and the overlay wrapper, remembered
+ the overlay, and `applyScreenControllerBackgroundColor` painted it with the
+ screen background — an opaque full-size cover ABOVE the real content
+ (hit-testing passed through, so it was "interactive but invisible", and on
+ cold presents it never repaired). Upstream identifies the wrapper by native
+ class (`RNSScreenContentWrapper`).
+ **Fix:** `ScreenContentWrapper` (iOS) now renders with
+ `nativeID="RNSScreenContentWrapper"`, and `screenContentWrapperForFabricChild`
+ rejects any resolved view that is not the explicit wrapper when one exists
+ (`viewIsExplicitNativeScriptScreenContentWrapper` /
+ `screenViewExplicitNativeScriptScreenContentWrapperChild`, chunk2 + chunk5).
+
+3. **Wasteful mid-update work (latency, partially fixed).**
+ - `ensureSelectedTabStackContainerHosted` reparented the LIVE root stack
+ container on every stack update (any intermediate UIKit wrapper view made
+ its superview check fail) — ~60 ms per present. Now it leaves
+ window-attached, correctly-contained stack views alone.
+ - Duplicate `transactionCommitted` deliveries and duplicate
+ `schedulePostLayoutStackReconcile` dispatches piled dead blocks on the
+ main queue ahead of the presentation-critical reconcile. Both now dedupe
+ via `stackPostLayoutReconcilePending`.
+
+**Verified with pixels (port sim):** modal body now paints INSIDE the sheet
+from the first visible frame (350 ms capture shows the sheet mid-slide with
+full body content — upstream-matching behavior; previously the body was blank
+at 250/450 ms and permanently blank on cold presents). Push/pop lanes still
+pass. All suites green: screens Jest (367), tsc, bob build, runtime JS tests
+(after fixing two stale source-shape assertions the previous session left
+failing: runtime-callback-policy + uikit-host-detached-wrapper-api),
+check:ffi-boundaries, demo typecheck.
+
+**Still open — present-start latency.** The original app shows full
+`modal-content` at 250 ms (all 7 verifier gates pass on the original sim; the
+present must start ≈ +100 ms there). The port's present now starts ≈
++250-300 ms, so `present-250ms` still fails (the 250 ms frame is still root).
+Measured no-trace breakdown after this session's fixes: press→commit (JS)
+~50-90 ms, Fabric mutation loop ~90 ms (per-child worklet chains:
+content-wrapper resolve/layout/certify + host creation, 15-30 ms per screen
+child — upstream's mountChildComponentView is bookkeeping-only), post-loop
+reconcileModal ~50 ms (incl. two ~12 ms layoutHostedReactSubviews passes).
+Next surgical direction: make the mid-transaction worklets bookkeeping-only
+(defer wrapper layout/certify/touch-refresh work to the single
+transactionCommitted pass), dedupe the double prepare-layout in
+reconcileModal, and re-measure with `NS_NS_FABRIC_DEBUG=1`
+(SIMCTL_CHILD_NS_NS_FABRIC_DEBUG=1 + `log stream` — see below).
+
+### Latency pass (same session, later): present-start moved from ~+250-300 ms to ~+140-170 ms
+
+Three more upstream-shaped cuts landed after the correctness fixes (all fork,
+all suites green):
+
+- **Bookkeeping-only mid-transaction mounts.** `mountFabricScreenContentChild`
+ now returns after registry bookkeeping while a screen is inside a Fabric
+ mounting transaction (`beginScreenFabricMountBatch` set from the screen's
+ `mounted`/`update`/`mountingTransactionWillMount` hooks, cleared in
+ `transactionCommitted`/`hostReady`). The full layout/readiness/touch tail
+ runs once per screen from the `transactionCommitted` sync — mirroring
+ upstream `RNSScreenView.mountChildComponentView` (insert-only) +
+ one container update per transaction.
+- **Snapshot ≠ delta.** The screen `transactionCommitted` treated a non-empty
+ `transaction.children` *snapshot* as "children changed", so every screen
+ (including the unchanged Home base screen, 78 descendants) re-ran the full
+ wrapper layout chain on every transaction (~120 ms trace during a modal
+ present). Now requires `hasModifiedChildren === true`.
+- (Earlier in the session: live root-stack container rehost removed;
+ duplicate transactionCommitted / post-layout reconcile dispatches deduped.)
+
+Measured effect (trace-mode, same push/pop→present flow): loop-end→reconcile
+gap 264 ms → 116 ms; `modal-present-call` at +427 vs +575. **Pixels at 250 ms:
+the sheet now covers the root ~80 % risen with the full body content painted
+inside it** (previously the 250 ms frame was still root). The original at
+250 ms shows the settled sheet — the port is ~60-90 ms behind on present
+start, purely latency.
+
+`present-250ms` still fails, partly for a verifier-classification reason worth
+knowing: the port's 250 ms frame (sheet mostly risen, content aboard, root
+fully covered) matches NEITHER verifier state — `modal-sheet-started` requires
+the root's Present button still visible (`presentBlue`), `modal-content`
+requires the Dismiss button at its settled position. Do not weaken the gate;
+close the latency gap instead.
+
+Remaining measured hotspots (next targets, real ms):
+1. `reconcileModal` ≈ 104 ms (slow-worklet label) — controller/header
+ configuration (`configureScreenControllerHeader`,
+ `configureNavigationAppearance` UINavigationBarAppearance work) plus two
+ mandated ~13 ms layout passes (`layoutNavigationStackViews
+ 'modal-base-refresh'` + `layoutPresentedModalNestedContentScreen`; tests
+ 'repairs stale hosted RN wrapper frames after UIKit modal presentation' and
+ 'starts modal presentation from the upstream modal controller list…' pin
+ the first pass — removing it needs a fresh stale-frame repro first).
+2. Mutation-loop residuals ~60-70 ms: lazy UIKit host creation for the new
+ nested-stack / modal-screen hosts (~27-32 ms each, `createController` +
+ configure through the bridge) and the stack/screen `update()` worklets
+ (register/publish/containment). Upstream's per-view cost is µs ObjC.
+ Candidate: make `createController` alloc-only and defer configure to the
+ post-transaction pass.
+3. Repeated `content-wrapper-resolve-root` / `stack-host-record-published`
+ chains (~8-15 ms each) still fire several times per transaction.
+
+### Second latency pass (2026-07-06): FULL verifier pass at 350 ms; 250 ms still open
+
+- **Recycled-wrapper registry bug fixed (the big one).** After push/pop, RN
+ recycles the popped screen's wrapper component view into the next screen
+ (the modal-header), but the port's cross-screen wrapper registry still
+ mapped the view to the dead screen, so wrapper resolution returned null on
+ every mount (`skip-registered-screen`), the prepared fast-path never
+ engaged, and reconcileModal ran a forced double layout on every
+ present-after-pop. Registrations are now validated (registered screen must
+ still host the view) and cleared when stale. `modal-present-call` worklet:
+ 104 ms → 19 ms. This likely also removes forced refreshes in other
+ push-after-pop lanes that reuse recycled wrappers.
+- **Runtime transaction-delivery dedupe** (see RN_API.md 2026-07-06 note):
+ the host-creation replay no longer delivers a partial transactionCommitted
+ mid-transaction (`isApplyingMountingTransaction` guard); didMount delivers
+ the complete one exactly once. TS handlers got idempotence early-exits so
+ repeat deliveries are cheap. NOTE: runtime .mm changed — the demo app binary
+ must be rebuilt (`xcodebuild -workspace NativeScriptUIKitDemo.xcworkspace
+ -scheme NativeScriptUIKitDemo -configuration Debug -sdk iphonesimulator
+ -derivedDataPath build/DerivedData build`, then `simctl install`).
+- **Verifier state: full pass at `FIRST_PAINT_DELAY_MS=350`** (all 7 gates,
+ `present-350ms=modal-content`). At 250 ms the port's sheet is ~92-97 %
+ risen with the full body aboard; the original is settled. Video-frame
+ comparison (simctl recordVideo on both sims, identical tap protocol) puts
+ the port's present-start roughly 150-350 ms behind the original — note the
+ verifier's delays measure from the simdeck tap *command return*, not touch
+ delivery, so treat its absolute numbers as relative only.
+- Remaining measured latency work: mutation-loop host creation for fresh
+ hosts (~20-30 ms each — candidate: alloc-only `createController`, configure
+ deferred post-transaction) and stack/screen `update()` publish/containment
+ chains. press→commit is shared JS with the original and likely equal.
+
+### Fundamental audit + touch overhaul (2026-07-06, "we are doing something fundamentally wrong")
+
+The user reported taps (e.g. "Push Detail") not reacting instantly and general
+performance far below the original. A 4-agent architecture audit (full JSON
+reports preserved in `docs/audits/2026-07-06-fundamental-audit-*.json`) plus
+video/burst measurements identified and fixed three port-specific mechanisms
+sitting on EVERY interaction:
+
+1. **Per-tap worklet hit-testing (removed).** `RNSScreenNativeScriptView`
+ overrode `hitTest:withEvent:` as a worklet doing registry scans and
+ recursive bridged subtree walks; UIKit hit-tests several times per touch,
+ so every tap paid multiple ObjC→JS crossings before delivery — and the
+ override returned null on wrapper-resolution failure, making taps DEAD
+ instead of falling through (the historical "unreliable touches").
+ Upstream RNSScreenView has no hitTest override. Removed (chunk4); the
+ stack-level hitTest (header routing + edge-pop, upstream has an ObjC
+ equivalent) and the header-subview wrapper hitTest were kept.
+2. **Duplicate RCTSurfaceTouchHandler per screen (removed).**
+ `screenRequiresOwnSurfaceTouchHandler` returned true for EVERY screen in a
+ registered stack, attaching a second surface handler whose gesture
+ negotiation coupled with the root handler on every touch. Upstream
+ attaches one extra handler only for window-detached modals. Now only
+ modal presentations (and their nested content) qualify (chunk2).
+3. **Worklet fan-out on every Fabric commit (gated, runtime).** Fabric
+ delivers mounting-transaction callbacks to every registered observer view
+ for every transaction in the app; the runtime crossed into the worklet
+ (`mountingTransactionWillMount`) for every live host on every commit —
+ a one-line text update paid ~10 crossings with tree walks
+ (`ensureScreenContentWrapperMounted` etc.). Now gated natively on whether
+ the transaction actually touches the host's tag
+ (NativeScriptUIViewComponentView.mm, upstream-mirror of the native
+ mutation scan). REQUIRES APP BINARY REBUILD.
+
+Measured effect (burst-screenshot protocol, both sims, identical taps):
+before — port's non-nav header tap ~+900 ms and push-start ~+650 ms behind
+the original; after — both land in the same first ~460 ms sample bucket as
+the original (indistinguishable at this resolution). One header-counter
+commit's main-thread span dropped from ~258 ms (21 lifecycle crossings, 6
+transaction deliveries) before the gating. All lanes re-verified: push, pop,
+header custom-view tap (counter increments), present, dismiss; screens Jest
+367 green after updating the touch-policy/hitTest source-shape tests.
+
+**Remaining fundamentals (the roadmap, from the audits — see
+docs/audits/*.json for full evidence):**
+- Ownership inversion: upstream makes the Fabric component view the
+ controller's view (`RNSScreen.mm initWithView:self`); the port evicts
+ children into TS-owned UIKit trees, forcing an 84-map registry and
+ ensure/certify/refresh repair machinery. Adopting the upstream invariant
+ would let most of chunk2/chunk3's compensation code be deleted.
+- Per-lifecycle JSON: every host lifecycle call re-serializes and re-parses
+ props + nativeMountInfo under a recursive mutex even when unused
+ (runUIKitHostLifecycle / NativeApiJsi). Make payloads lazy/revision-gated.
+- hostReady churn: notifyHostReadyIfNeeded funnels from layoutSubviews,
+ didMoveToWindow and ~23 prop setters, each rebuilding a subview-topology
+ key; upstream has no readiness protocol at all.
+- `registerNativeScriptStackController` re-runs full host-record publishing +
+ tab containment on 5 hooks; `uikitHostPropsJson` re-serializes on every
+ React render of every host, and inline-callback identity churn re-applies
+ props natively on unrelated commits.
+
+Useful new tooling from this session:
+- `SIMCTL_CHILD_NS_NS_FABRIC_DEBUG=1 xcrun simctl launch …` +
+ `xcrun simctl spawn log stream --predicate 'eventMessage CONTAINS "NS_NS_FABRIC_DEBUG"'`
+ gives the native mutation/mount timeline without JS-trace overhead.
+- `xcrun lldb -p ` + `expression -l objc -O -- [view recursiveDescription]`
+ settles "mounted but invisible" questions definitively.
+
+## Current state (as of prior session turn, 2026-07-05 20:52 EDT — superseded by the update above)
+
+The active sub-problem is **modal first-paint parity**: tapping to present a
+modal should show the modal body content by ~250 ms, matching upstream RNS.
+
+**Fixed this session (verified with tests + trace):**
+
+- Root/parent stack now reconciles from the **stack-host update path** as soon
+ as `stackCanReconcileFromPropsUpdate` passes, instead of waiting for the next
+ Fabric transaction. This moved the `presentViewController` call from ~+229 ms
+ to **~+131 ms** after the press — the root-stack timing bug is resolved.
+- Nested modal-content stacks are **guarded** so they do not reconcile
+ immediately before their content wrapper exists (that guard was needed because
+ the broadened immediate-reconcile initially fired too early for
+ `Modal:modal-header` before modal props were registered).
+- Added pre-presentation runtime flush attempts via the existing generic
+ `flushUIKitHostView` / owner-stack flush APIs (`flushKnownUIKitHostView`).
+
+**Still broken (the open bug to continue):**
+
+- At the 250 ms capture the modal **shell is visible but the body is blank**.
+ The nested modal content **wrapper mounts too late (~+230–250 ms)**, so the
+ Fabric child carrying the modal body is not present when the frame is taken.
+ The pixel verifier reports `unknown` instead of `modal-content`.
+- The plain host flush and the owner/stack flush did **not** pull the wrapper
+ mount earlier in the no-trace run.
+
+**Diagnosis / next direction (Codex's own conclusion):** this is no longer a
+tap-handling problem. It is that **Fabric child / content-wrapper
+materialization for the nested modal screen is delayed vs upstream RNS**. The
+correct next fix is likely in the **NativeScript RN-module / Fabric host API
+ordering** (making the modal content wrapper materialize synchronously *before*
+`presentViewController`), not another modal-specific workaround. Follow the
+mental model: (1) what does upstream RNS do, (2) what exact primitive does it
+rely on, (3) does NativeScript expose that to TS/worklets, (4) if not add the
+generic primitive, (5) only then patch the port.
+
+## Files touched
+
+### Screens fork — `/Users/dj/Developer/RNModuleForks/react-native-screens` (primary work)
+
+- `src/components/native-stack/native-script/NativeScriptScreenStack.ios.tsx`
+ and its build chunks (`…ios.chunk2.tsx`, `…ios.chunk5.tsx`) — stack host
+ update reconcile path, modal-content guard, pre-presentation flush hooks,
+ selected-tab accessibility publishing.
+- `src/components/tabs/native-script/NativeScriptTabs.ios.tsx` — selected
+ embedded-stack ownership stamping and readiness checks.
+- `src/components/native-stack/native-script/NativeScriptScreenStack.test.ts`
+ and `…/tabs/native-script/NativeScriptTabs.test.ts` — assertions updated to
+ require immediate reconcile when the ready predicate passes (transaction path
+ kept as fallback).
+- Built output under `lib/module/…` / `lib/commonjs/…` via `npx bob build`.
+
+### Runtime (this repo) — changed vs HEAD (`git status`, ~38 files)
+
+- `packages/react-native/ios/` — `NativeScriptUIView.mm/.h`,
+ `NativeScriptUIKitHost.h`, `NativeScriptUIViewManager.mm`,
+ `NativeScriptNativeApiModule.mm`, and `Fabric/NativeScriptUIViewComponentView.mm/.h`
+ (host-refresh / Fabric host primitives — e.g. the scroll-view zero-origin fill
+ frame fix and `flushUIKitHostView` surface).
+- `packages/react-native/src/` — `index.ts`, `index.d.ts`,
+ `NativeScriptUIViewNativeComponent.ts` (renamed the RNS-named trace hook to the
+ generic `__nativeScriptUIKitHostTraceEvents` / `[NativeScript UIKitHost]`).
+- `packages/react-native/test/*.test.js` (16 files) — runtime JS guard tests.
+- `NativeScript/ffi/shared/bridge/*.mm` and `NativeScript/ffi/hermes/NativeApiJsi.mm`
+ — JSI/FFI bridge fixes (JS subclass prototype/accessor/init identity — see the
+ recent `fix:` commits in `git log`).
+- Tracking docs: `HANDOFF.md`, `PROGRESS.md`, `RN_API.md`.
+
+## How to run / test
+
+Screens (fast inner loop — run after every change):
+
+```sh
+cd /Users/dj/Developer/RNModuleForks/react-native-screens
+./node_modules/.bin/jest src/components/native-stack/native-script/NativeScriptScreenStack.test.ts --runInBand
+./node_modules/.bin/jest src/components/tabs/native-script/NativeScriptTabs.test.ts --runInBand
+./node_modules/.bin/tsc --noEmit --pretty false --skipLibCheck
+npx bob build # rebuild lib/ so the demo/simulator picks up changes
+```
+
+Runtime (when the RN-module / Fabric API surface changes):
+
+```sh
+cd /Users/dj/Developer/NativeScriptRuntime
+for f in packages/react-native/test/*.test.js; do node "$f"; done
+npm run check:ffi-boundaries
+```
+
+Demo typecheck:
+
+```sh
+cd /Users/dj/Developer/RNModuleForks/nativescript-uikit-demo
+npm run typecheck
+```
+
+**Simulator pixel gate (the real pass/fail).** Rebuild the fork, restart the
+no-trace Metro on 8082, relaunch the app, then run the first-paint pixel probe.
+The verifier and its env profile (from the last session):
+
+```sh
+cd /Users/dj/Developer/RNModuleForks/nativescript-uikit-demo
+npm run start -- --clear --port 8082 # no-trace server (clean signal)
+xcrun simctl terminate BF759806-2EBB-49ED-AD8E-413A7790ADE0 org.nativescript.uikit.demo || true
+xcrun simctl openurl BF759806-2EBB-49ED-AD8E-413A7790ADE0 'nativescriptuikitdemo://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8082'
+FIRST_PAINT_OUT_DIR=/tmp/rns-first-paint FIRST_PAINT_PROFILE=port \
+FIRST_PAINT_DELAY_MS=250 SECOND_PAINT_DELAY_MS=450 \
+node scripts/verify-react-nav-first-paint-pixels.js
+```
+
+For trace-based timing (to see wrapper-mount / present-call timestamps) set
+`EXPO_PUBLIC_NS_RNS_TRACE=1 EXPO_PUBLIC_RNS_DEMO_TRACE=1
+EXPO_PUBLIC_RNS_DEMO_TRACE_EXPORT=1` on the Metro start, then read the exported
+trace from the app container:
+
+```sh
+DATA_DIR=$(xcrun simctl get_app_container BF759806-2EBB-49ED-AD8E-413A7790ADE0 org.nativescript.uikit.demo data)
+cat "$DATA_DIR/Documents/rns-port-trace-export.json" # shape varies; parse defensively
+```
+
+Always run the **no-trace** pixel gate for the actual verdict (trace overhead
+skews the ~250 ms edge). Stop Metro (`Ctrl-C` / clear ports 8082 & 8083) when
+done so no stray servers are left.
+
+## Verification discipline
+
+- SimDeck screenshot / pixel comparison against the original app is the final
+ gate — Jest green + AX descendants is not enough.
+- Compare port vs original on both UDIDs for: repeated tab switches into React
+ Nav, first push after tab switch, repeated push/pop, modal present/dismiss
+ (incl. interactive dismiss), toolbar / menu / custom-header actions.
+- Do not claim the branch is PR-ready. Known-passing lanes exist, but the modal
+ first-paint blank-body bug is open and PR hygiene (removing diagnostic
+ leftovers) is pending.
+
+## Open questions
+
+- Which generic NativeScript Fabric/host primitive can force the nested modal
+ content wrapper to mount **synchronously before** `presentViewController`?
+ That is the crux of the remaining bug.
+- Is there residual trace/diagnostic scaffolding in the fork that should be
+ stripped before the PR?
+- Confirm the modal/native title & toolbar-padding items (currently "watch
+ only") don't regress once first-paint is fixed — get a fresh pixel repro
+ before touching that code.
+
+## Where to read more
+
+- `HANDOFF.md` — full running log, dated updates, "Known Broken Behavior",
+ "Suspect Code Path", "Next Surgical Direction", "Commands Likely Needed".
+- `PROGRESS.md` — turn-by-turn progress detail.
+- `RN_API.md` — the runtime / Fabric / worklet API surface added for the port.
+
+## 2026-07-06 (late session): interactive dismissal + back swipe fixed end to end
+
+Both user-reported gesture bugs are fixed and verified by deterministic
+simulator lanes on the port build:
+
+- `scripts/verify-modal-interactive-dismiss.js` (7 steps): **PASS** at
+ `SETTLE_MS=9000` (at 4000 only step 5 — push-after-modal-cycle — can lag
+ past the assert; it completes and step 6 passes).
+- `scripts/verify-back-swipe-pop.js` (5 steps, NEW): **PASS** at 4000 and
+ 9000. Push → edge-swipe pop → push → pop, two full cycles.
+- Fork Jest 311/311, `tsc` clean, `bob build` clean.
+
+### Root causes found and fixed (in dependency order)
+
+1. **Modal swipe-dismiss corrupted state (user bug 6)** — the screen host's
+ `mountChild` hook re-registered the screen with
+ `controller.__nativeScriptScreenContext` when that expando was absent on
+ the wrapper it received, clobbering `registry.screenContexts[id]` with
+ `undefined`. Native-initiated dismissals then had no emit channel, so
+ `onDismissed` never reached react-navigation and its route state kept the
+ dead Modal (later pushes rendered as modals per native-stack semantics).
+ Fix: `liveScreenContextForRecord` (chunk2) — a live emit channel is never
+ replaced by a dead one; remember/retain paths route through it.
+2. **Back swipe did nothing (user bug 4), part 1** — the port never installed
+ a delegate on `interactivePopGestureRecognizer` (upstream sets the stack
+ view as delegate, RNSScreenStack.mm:321); UIKit's default delegate refuses
+ when custom bar buttons are installed. On iOS 26 the (undelegated)
+ `interactiveContentPopGestureRecognizer` also suppressed the classic edge
+ pop.
+3. **Worklet dead-closure crash genre** — delegate objects built via
+ `ctx.delegate` inside *some* rehydrated worklet contexts throw
+ `TypeError: undefined is not a function` from the FIRST captured call:
+ every captured module function in that specific closure instance is
+ `undefined`. Confirmed empirically (probe traces + `NS_NS_CB_DEBUG=1`
+ source dumps in `Callbacks.mm`); healthy at build-time in the same code
+ path, dead at ObjC dispatch for particular instances. The same genre
+ causes the boot-time `NativeScript failed to run UIKit host
+ RNSTabsScreenIOS` logs (still open, see below).
+ **Working pattern** (now used for the gesture delegate, same as the modal
+ completion-handler fix): NativeClass **class methods with capture-free
+ bodies** that resolve handlers through `globalThis` keys installed by
+ `installRefreshVisibleStackContentHandler()` (worklet-module entry-point
+ pass, provably healthy).
+4. **Back swipe, part 2 (the fix shape)** — `RNSScreenStackNativeScriptView`
+ (the stack container view, like upstream's RNSScreenStackView) now
+ conforms to `UIGestureRecognizerDelegate`; `installNativeBackGestureDelegate`
+ assigns it to both UIKit pop recognizers (`native-pop`,
+ `native-content-pop`). Decision logic lives in module worklets
+ (`stackContainerBackGesture*`, chunk4, placed AFTER their captured deps —
+ the `declares worklet helpers before worklets that capture them` test
+ guards this ordering; violating it recreates the dead-closure genre at
+ module scope). Content-pop interop now defaults ON
+ (`__NSRNS_ENABLE_NATIVE_CONTENT_POP_GESTURE !== false`), matching upstream
+ iOS 26.
+5. **Interactive pop left UIKit wedged** — two port-only behaviors broke the
+ percent-driven transition: (a) `updateNativeBackGesture` toggled
+ `popGesture.enabled` by `count>1`, disabling the recognizer mid-gesture
+ (coordinator leak, `nav.transitionCoordinator` stuck non-nil, all later
+ pushes silently swallowed; verified via lldb). Upstream never toggles
+ `enabled` — the delegate gates. (b) `cancelTouchesInParent` called
+ `UIGestureRecognizer.reset()` directly (UIKit-internal API). Both removed.
+
+### Runtime (NativeScriptRuntime repo) changes
+
+- `NativeScript/ffi/shared/bridge/Callbacks.mm`: `NS_NS_CB_DEBUG=1` (launch
+ with `SIMCTL_CHILD_NS_NS_CB_DEBUG=1`) logs callback exceptions with the
+ stored function's `toString()` — keep; it is the tool for the dead-closure
+ hunt.
+- `packages/react-native/native-api/**` is a **vendored copy**; syncing is
+ manual `cp` (see `scripts/build_react_native_turbomodule.sh:73-85` — the
+ full script fails at metagen). Runtime .mm edits do nothing until copied +
+ `xcodebuild` + `simctl install`.
+- `wrapDelegateMethods` bind experiment was reverted (worklets hydration
+ already binds `this` via `valueUnpacker`; not the failure mode).
+
+### Open items (next session)
+
+1. **Dead-closure root cause** — the one systemic bug left. Deterministic
+ repro at every boot: `log show … 'NativeScript failed to run UIKit host'`
+ (tabs host, `reactNativeFabricViewLayoutTraits` capture dead inside
+ `fabricLayoutRectForMountedTabsChild`). Facts: bundle-mode OFF, classic
+ `valueUnpacker` (eval + bind objectToUnpack), unpacked closure record has
+ entries but the VALUES are undefined for specific instances; chunk import
+ graph is acyclic. Suspect the serialize side captured a module-scope
+ `__closure` map evaluated before the factory consts it references (Babel
+ plugin emits `const X = factory({dep: dep, …})` — order matters), or the
+ C++ Serializable cycle handling. The order-guard Jest test enumerates
+ violations — currently zero; extend it to index.ts/tabs if the boot error
+ persists.
+2. **Latency tail**: push-after-modal-cycle can exceed 4 s (lane step 5) and
+ the first present after tab mount got slower with the delegate work —
+ re-profile (`EXPO_PUBLIC_NS_RNS_TRACE=1`, `reconcileStack-sync`,
+ `modal-present-call`), then resume the perf roadmap (host-creation diet,
+ update() chains, present-250ms gate vs original).
+3. PR hygiene: strip diagnostic traces added this session
+ (`native-dismiss-branch`, `screen-host-dispose`, `clear-screen-record`,
+ `screen-controller-retain`, didShow flag dump — all traceEventsEnabled
+ gated, but review), drop `HANDOFF-FABLE5.md` scratch sections, re-run the
+ full battery (fork Jest + tsc + runtime Jest + ffi + both lanes + pixel
+ first-paint gate + present-latency vs original).
+4. `emitScreenDismissedForController` in `viewDidDisappear` + the didShow
+ native-dismiss path both emit now; double-emission is guarded by
+ `__nativeScriptDismissEventEmitted`, but audit for pops-with-preventNativeDismiss.
+
+## 2026-07-07: worklet capture-order root cause FIXED; latency lever identified
+
+### Dead-closure genre: root cause found and fixed
+The worklets Babel plugin rewrites every module-level worklet into
+`var X = Factory({dep1, dep2, ...})` evaluated in source order. Any captured
+module function declared LATER in the file is `undefined` at factory time and
+is baked dead into `X.__closure` — the map the serializer ships. Live JS-thread
+calls still work (lexical var assigned by call time), so the failure only
+appears in rehydrated copies. Verified end-to-end on
+`reactNativeFabricViewLayoutTraits -> reactNativeFabricViewLayoutTraitsForHandle`
+(bundle inspection: factory arg evaluates one line before the dep's factory).
+
+Fixes (all committed):
+- `scratchpad worklet-order-audit/fix.js` reordered 14 declarations in
+ packages/react-native/src/index.ts (+ hoisted the `*GlobalName` const block
+ above first worklet use — const-after-use makes the captured KEY undefined,
+ which produced the `defineProperty` "property is not configurable" boot
+ crash when two definers collided on the literal "undefined" key).
+- Fork: 18 moves in tabs, 5 split, 8 chunk4, 1 chunk3. The fork's
+ `declares worklet helpers before worklets that capture them` Jest test only
+ covers stack chunks + headerconfig + tabs and skips NESTED function
+ references and method-name false-positives; the session auditor (kept at
+ the scratchpad, reproduce from this doc if lost: TS AST, top-level function
+ decls, worklet directive, identifier refs excluding property/method names,
+ flag refs with higher declaration index; plus a variant flagging top-level
+ const/let referenced by earlier worklets) found real hazards the test
+ missed. PORT THE IMPROVED CHECKER INTO THE JEST TEST + add an equivalent
+ test for packages/react-native/src/index.ts.
+- Boot-time `NativeScript failed to run UIKit host RNSTabsScreenIOS` errors:
+ GONE after the reorders (was every boot).
+- `updateSurfaceTouchHandlerOriginsForView` (chunk2): try/catch guard — dead
+ native wrapper during dismissal teardown threw "requires a native receiver"
+ and aborted the app (reproduced under trace Metro).
+
+### Verified state
+- Fork Jest 367/367 (stack + tabs), runtime guard tests 37/37, tsc + bob
+ clean, boot logs clean.
+- Both gesture lanes pass functionally; back swipe and interactive modal
+ dismissal remain correct.
+
+### THE remaining defect: press -> transition-start latency (user bug 3)
+Measured with demo traces (`[RNS_DEMO_TRACE]` press vs transition-start):
+- original: present 60-88 ms, push 40-66 ms, pop 35-55 ms.
+- port: present 737-1053 ms, push 1007-1073 ms, pop ~444 ms — AND unstable:
+ on cold tab mount or without a follow-up React commit, presents/pushes can
+ exceed 4 s (lane step-1/3/5 'root' failures at SETTLE_MS=4000; pass at 9000).
+
+Push timeline breakdown (port, trace run, press = +0):
+- +186 Fabric commit reaches stack host props update (`stack-host-update`);
+ Detail screen host created ~+194; screen content children mount +303.
+- +306..+588 DEAD GAP waiting for `content-wrapper-direct-fabric-layout`
+ positive=1 (wrapper layout needs the screen to be sized — which upstream
+ gets by PUSHING FIRST; chicken-and-egg).
+- +660 screen-host-ready -> +676 reconcile isPush -> +693 pushViewController.
+- The stack host's own `transactionCommitted` for the INSERT commit arrived
+ at **+941**, i.e. with the app's SECOND commit — the first commit's didMount
+ notification for the stack host appears to be suppressed/deferred
+ (check `NativeScriptUIViewComponentView.mm` willMount gating
+ `transactionTouchesThisHost` + `isApplyingMountingTransaction` replay skip +
+ TS `stack-host-update-defer-transaction` interplay). When that notification
+ is late/absent, the push rides the screen-host-ready fallback, whose timing
+ depends on wrapper layout => the 1-4 s variance.
+
+Next surgical steps (in order):
+1. Runtime: make the stack host's transactionCommitted fire for the commit
+ that INSERTS a child (didMount path) — verify with NS_NS_FABRIC_DEBUG=1
+ that the first commit delivers; the +941 second-commit delivery is the bug.
+2. Fork: drop the content-ready gate on the PUSH path (upstream
+ RNSScreenStack pushes at updateContainer time with no content gating; RN
+ lays content into the already-pushed screen during the animation). The
+ blank-first-paint causes this gate papered over were fixed at the runtime
+ level (willMount gating, replay dedupe, mount batching) — re-verify with
+ the first-paint pixel gate afterwards (it currently fails `present-350ms`
+ because nothing is on screen at 350 ms; original shows the sheet by then).
+3. Re-run: both lanes at SETTLE_MS=4000 (no-trace Metro), first-paint gate at
+ 350 ms, and the press->transition-start measurement (target: within ~2x of
+ original). `scripts/measure-present-latency.js` exists in the demo repo.
+4. Tab first-mount hang (user bug 1): re-measure after (1)+(2) — the same
+ commit-delivery + gating machinery dominates the tab's first stack mount.
+
+### PR hygiene still pending
+- Strip/gate diagnostic traces added during debugging (native-dismiss-branch,
+ screen-host-dispose, clear-screen-record, screen-controller-retain,
+ didShow flag dump, adaptive-delegate-*, presentation-did-dismiss).
+- Demo repo is NOT a git repo; lane scripts live on disk only
+ (verify-modal-interactive-dismiss.js, verify-back-swipe-pop.js,
+ measure-present-latency.js).
+
+## 2026-07-07 (later): latency gap — four blockers found and fixed
+
+Profiling tool added: `NS_NS_HOST_PROFILE=1` (launch via SIMCTL_CHILD_) logs
+every host lifecycle crossing >=2 ms (`NS_NS_HOST_PROFILE phase=
+ms=`) plus `txn-will/txn-did` brackets per transaction — this is what
+found everything below. Section timers (`traceSlowWorklet`) were added inside
+the screen transactionCommitted hook and tabs mountChild.
+
+### Fixed
+1. **Permanent main-thread layout loop (the big one).**
+ `refreshContainerViewFrameAndHost` ran from `layoutSubviews` and called
+ `[_containerView setNeedsLayout]` unconditionally — a self-invalidating
+ 60/120 Hz loop per mounted host, saturating the main thread forever.
+ This starved Fabric mount transactions: on no-trace builds presses looked
+ completely dead (trace builds only worked because tracing throttled the
+ loop). Removed the unconditional invalidation
+ (`NativeScriptUIViewComponentView.mm`); frame changes still invalidate via
+ refreshContainerViewFrameIfNeeded.
+2. **490 ms in ONE worklet call**: the new screen's `transactionCommitted`
+ replayed every Fabric child through `mountFabricScreenContentChild` with
+ the batch flag already cleared, so each child paid the full
+ layout/readiness tail; `layoutHostedSubviewChain` + `layoutIfNeeded` also
+ ran even when Fabric had already laid the wrapper out. Now: batch stays
+ active during the replay (children = bookkeeping, upstream
+ mountChildComponentView), ONE readiness/touch/host-ready pass afterwards,
+ and the manual chain layout runs only for the zero-size (modal cold
+ materialization) seed path.
+3. **~40 ms × N repeated hostReady events**: the runtime re-emits hostReady on
+ every host frame refresh; the screen hostReady hook now early-exits when
+ the content is certified for the same host view (dead-wrapper/new-host
+ cases still take the full pass).
+4. **~900 ms tab-switch hang (user bug 1)**:
+ `embeddedNavigationControllerRecordForMountedTabsChild` probed the
+ cross-chunk host-record resolver per BFS-visited view (64 views), and that
+ resolver scans EVERY registered stack with superview-chain walks per call.
+ Dropped the per-descendant probe (the descent reaches the stack container
+ view, whose association-based direct record answers) and added a
+ stacks-generation-keyed negative memo in
+ `stackNavigationControllerRecordForHostViewFromExternalHost`.
+ Result: 907 ms -> 41-75 ms.
+
+Also: `syncUIKitHostPropsFromNative` now skips the per-crossing JSON.parse
+when the payload string is unchanged per registration (kept although the
+measured win was small — payload strings usually differ by revision).
+
+### Measured state (dev Metro, demo-trace-only, press -> transition-start)
+- original: present 60-88 ms, push 40-66 ms, pop 35-55 ms
+- port BEFORE this session: push ~1050 ms (and unbounded/no-op on no-trace
+ builds due to the layout loop)
+- port AFTER: push 480-660 ms, present 615-690 ms, pop ~465 ms; UIKit
+ pushViewController fires at ~330 ms; worklet->JS emit lag is ~5 ms.
+- Milestones for a push (+0 press): +153 stack-host-update (React render +
+ commit + host-update crossings), +312 reconcile isPush, +330
+ pushViewController, +492 willShow (main-thread contention with the
+ transactionCommitted crossing), +497 JS transition-start.
+
+### Remaining gap (~280 ms vs original) — next levers, in order
+1. +0->+153 commit window: the port's screen/stack host updateProps +
+ update() crossings run INSIDE the Fabric commit; original commits in
+ ~40 ms. Candidate: defer update() work out of the commit (upstream
+ updateProps is bookkeeping-only).
+2. +153->+312: child-insert -> reconcile notification path (the insert
+ mutation reaches the stack host's mountChild, but the reconcile rides a
+ scheduled hop). Candidate: reconcile directly from the stack's
+ transactionCommitted (now cheap and synchronous with didMount).
+3. +330->+492: crossings contending with the transition on main
+ (mountingTransactionWillMount 33 ms x2 per commit is pure crossing
+ overhead for a trivial hook — candidate: skip the willMount crossing for
+ hosts whose hook is a no-op, or batch it into didMount).
+
+### Verified after all changes
+- Both lanes PASS at SETTLE_MS=4000 on NO-TRACE Metro (the strict verdict).
+- Fork Jest 367/367 (two stale source-shape assertions updated:
+ descendant host-record probe removal), runtime guard tests 37/37, tsc
+ clean, boot logs clean.
+- KNOWN INSTRUMENT FLAW: `verify-react-nav-first-paint-pixels.js` at 350 ms
+ fails with byte-identical screenshots across runs — simdeck screenshot
+ returns cached/stale frames at sub-second cadence; the gate measures the
+ tool, not the app. Lane classifiers with 4 s settles are unaffected. Use
+ the willShow trace milestone (~330 ms) for present-start until screenshots
+ are trustworthy (consider `xcrun simctl io screenshot` comparison).
+- Demo change: EXPO_PUBLIC_RNS_DEMO_TRACE no longer force-enables fork
+ worklet tracing (only EXPO_PUBLIC_NS_RNS_TRACE does) so press->transition
+ latency can be measured without the tracing handicap.
+
+## 2026-07-07 (final): remaining-floor analysis + what was kept/reverted
+
+Additional profiling infra (all committed):
+- `NS_NS_HOST_PROFILE_INNER` (Callbacks/module): runSync-lambda-interior ms —
+ proved crossings have ZERO mutex wait; all cost is real JS in the hooks.
+- `__NS_NS_HOST_PROFILE` worklet global (set at install when env present) +
+ `NS_NS_HOST_PROFILE_SECTIONS` in runUIKitHostLifecycleFromNative — proved
+ the shared prologue (mount-info/props JSON) is now ~0 ms; the cost is hook
+ bodies.
+- Section timers: screen mounted() (`screen-mounted-*`),
+ configureScreenController (`configure-layout-hosted`, `configure-header`,
+ `configure-layout-nav`).
+
+Landed this pass:
+- Stack `refresh` hook skips while the stack is transitioning (UIKit lays the
+ moving views out repeatedly mid-animation; each refresh re-ran controller/
+ container registration at 10-50 ms on main). Post-transition repair still
+ runs from didShow/finishTransition. Both lanes green.
+- Stack transactionCommitted reconciles push/pop-shaped targets DIRECTLY
+ (upstream updateContainer is synchronous at didMount); only targets
+ involving modal presentation keep the post-layout dispatch hop (upstream
+ dispatch_async before presentViewController).
+
+Attempted and REVERTED (do not blindly retry — tests encode why):
+- Trimming the screen willMount hook to bookkeeping-only broke presents and
+ pushes outright: the pre-mutation `ensureScreenControllerView` +
+ `normalizeScreenControllerViewFrame` + `ensureScreenContentWrapperMounted`
+ are load-bearing (mutations need the materialized view chain).
+- Gating `layoutHostedSubviewChain` by level-0 geometry (positive size /
+ size-matches-root) broke: (a) modal presentation (dismiss lane steps 1/3/5
+ no-op), (b) the stale half-height-viewport repair (`does not stable-skip
+ direct Fabric content whose hosted viewport is still half-height`) because
+ stale frames live DEEPER than level 0. The correct future shape is an
+ explicit `contentIsFreshFromFabric` flag threaded from the mounted() path
+ only (fresh Fabric mounts cannot have stale nested frames; repairs keep the
+ walk).
+
+Current measured floor (dev Metro, demo-trace only):
+- push 470-490 ms, present 590-720 ms, pop ~470 ms press->transition-start
+ (JS emit lag ~5 ms; UIKit push begins ~330 ms after press).
+- Structure: ~150 ms React/commit+host-update crossings inside the mount,
+ ~90 ms hook bodies (willMount ensure ~34, committed ~35, stack update ~10,
+ header subviews ~16), remainder reconcile/prepare + UIKit.
+- Tab-switch mounted(): configure ~440 ms (layout-hosted walk + header
+ config); tabs child-mount search fixed earlier (907 -> ~70 ms).
+
+End-game moves for true parity (in order of expected value):
+1. **Batched native ensure/normalize helper** (generic runtime API): the hook
+ bodies spend their time in dozens of tiny interop calls
+ (frame reads/writes, superview checks). One ObjC helper per pattern
+ (materialize controller view + normalize frame + wrapper-mounted check)
+ turns ~30 calls into 1 (~34 ms -> ~2 ms per touched screen). Same for
+ layoutHostedSubviewChain (there is already a native
+ NativeScriptLayoutHostedSubviewChain used by NativeScriptUIView — expose
+ it to the worklet surface and have chunk3 call it instead of the JS walk).
+2. **Release-build parity measurement**: both apps currently run dev Metro;
+ the port executes its port logic in unoptimized dev JS while upstream's
+ equivalent is compiled ObjC — the honest "bridge cost" comparison needs a
+ release bundle at least once.
+3. `contentIsFreshFromFabric` flag (above) to skip repair walks on fresh
+ mounts safely.
+
+All gates green at this commit: both lanes at SETTLE_MS=4000 (no-trace),
+fork Jest 367/367, tsc clean, runtime guard tests 37/37, boot logs clean.
+
+## 2026-07-07 (physics): measured interop cost + fan-out verdict
+
+On-device micro-benchmark (worklet, dev Hermes): plain JS 100k ops = 4.9 ms;
+1k string builds = 0.2 ms; **1k bridged property reads (view.bounds) =
+8.8 ms → ~9 µs per interop call** (JSI host object -> metadata -> ffi
+marshal; not raw objc_msgSend). Therefore the port's latency is COUNT-driven:
+a 34 ms hook = ~4k native touches; a push at ~500 ms = ~50k, where upstream
+does ~100. The architectural divergence is the wrapper/registry/repair state
+machine fanning out per lifecycle event — not per-call overhead and not the
+crossing mechanism (runSync interior == total; no mutex wait).
+
+Landed: all 390 statement-form traceWorkletEvent call sites gated (their
+template args did handle conversions/string builds even with tracing off).
+Numbers hold at push ~500 ms / present ~650 ms / pop ~480 ms on dev Metro.
+
+The honest path to 50 ms-class parity, in order:
+1. Count-reduction by architecture: adopt upstream's hosting shape for the
+ screen (component view IS the controller view; children stay Fabric-owned;
+ no wrapper reparenting -> the readiness/repair/touch machinery and its
+ tens of thousands of reads become structurally unnecessary). The props
+ plumbing (attachController/mountChildrenDirectlyToChildrenView etc.)
+ already half-exists; this is the real port-parity refactor.
+2. Native batch helpers for whatever walks remain (~9 µs -> ~50 ns per op).
+3. Release-bundle measurement for the residual JS share.
+Beware: bob rebuilds mid-lane cause bundle-retransform races — always
+relaunch and let Metro finish transforming before running lanes (first lane
+after a rebuild can report false first-action no-ops).
+
+## 2026-07-07 (attribution): per-section call counters + the stable-skip wins
+
+Tooling that made this possible (all committed):
+- Bridge counters: `NativeScript/ffi/shared/bridge/InteropProfiler.h` holds
+ C++17 inline atomics (`nsInteropProfiler::gCalls/gNs`) ticked by every
+ `ffi_call` AND both GSD fast-path `invoker(ctx)` sites. The engine TUs
+ include bridge sources inside `namespace nativescript { namespace {` — the
+ counters must live in a header included at FILE scope by each engine .mm,
+ or you get anon-namespace duplicates/link errors.
+- `NS_NS_HOST_PROFILE_INNER ... interopCalls=N interopMs=X` per phase from
+ the module; `__nsInteropCalls()` on the worklet global; fork-side
+ `traceSlowWorklet`/`traceTimestamp` fire under `__NS_NS_HOST_PROFILE`
+ (bypassing trace gates) and print `NSRNS_PROF