diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..82ec2b0
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,65 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ schedule:
+ - cron: '0 6 * * *' # daily: React Native nightly changes every day
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ mode: [development, production]
+ rn: ['0.87', '0.86']
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v2
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ - run: bun install --frozen-lockfile
+ - run: npm run typecheck
+ - run: npm test
+ env:
+ UNIFIBER_MODE: ${{ matrix.mode }}
+ UNIFIBER_RN: ${{ matrix.rn }}
+
+ coverage:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v2
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ - run: bun install --frozen-lockfile
+ - run: npm run coverage
+
+ nightly:
+ # RN nightly + the React it targets (RN bundles a renderer built from React main). Never blocks a merge.
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ strategy:
+ fail-fast: false
+ matrix:
+ mode: [development, production]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v2
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ - run: bun install --frozen-lockfile
+ - name: React Native nightly + the React it targets
+ run: |
+ REACT_RANGE="$(npm view react-native@nightly peerDependencies.react)"
+ REACT_VERSION="$(npm view "react@${REACT_RANGE}" version --json | node -e "const v = JSON.parse(require('fs').readFileSync(0, 'utf8')); console.log(Array.isArray(v) ? v.at(-1) : v)")"
+ bun add -d react-native@nightly @react-native/babel-preset@nightly "react@${REACT_VERSION}"
+ node -e "console.log('react-native', require('react-native/package.json').version, '/ react', require('react/package.json').version)"
+ - run: npm test
+ env:
+ UNIFIBER_MODE: ${{ matrix.mode }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8db1464
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+bench/out/
+coverage/
+*.lcov
+.DS_Store
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c9d9c03
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Jacek Pudysz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..304e6d5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,90 @@
+
+
+# unifiber
+
+**One small, tested place for everything React Native styling libraries read from React's private fiber tree.**
+
+Built for [Unistyles](https://github.com/jpudysz/react-native-unistyles) and Uniwind Pro.
+
+`React 19` · `React Native 0.86 – 0.87` · `iOS + Android` · `Hermes-first` · `MIT`
+
+
+
+---
+
+## Why this exists
+
+Unistyles and Uniwind both do something React Native doesn't officially support: they update the native shadow tree **without a React render**. Theme switches, breakpoints and insets reach native views in a single commit. To do that, both read React's private fiber fields: `__internalInstanceHandle`, `stateNode.node`, `memoizedState`, `return`, `alternate` and more.
+
+Until now, each library kept its own copy of those reads. They drifted apart, and both had bugs that only show up on real apps.
+
+unifiber is the one module that knows what a fiber looks like. It answers questions such as "which native view is this ref?", "is it hidden, and why?" and "what must refresh?". It's tested against **React Native's own components** and **Unistyles' real code**. A daily CI run against React Native nightly catches changes to React's internals before they ship.
+
+## API
+
+`Fiber` and `Reanimated` are plain objects. A **`Host`** is a native view's creation fiber behind an opaque type: free to keep, and stable, so the same view is always the same `Host` (`===` and `WeakMap` keys work).
+
+### ref → Host
+| | |
+|---|---|
+| `getHostFor(ref)` | Native-view refs, RN composites (ScrollView, FlatList / SectionList / VirtualizedList, ImageBackground, KeyboardAvoidingView, RefreshControl), and class components that render their view directly (Button on Android). Call once per link and keep the result. |
+| `getShadowNodeFor(ref)` | Hot path when you don't keep a Host. |
+| `getHostForInstanceHandle(handle)` | For fibers handed back from C++ (`ShadowNodeFamily::getInstanceHandle`). |
+| `getContentContainerFor(ref)` | ScrollView / list content container, through RN's public `getInnerViewRef()`. |
+
+### Host → native
+| | |
+|---|---|
+| `getShadowNode(host)` | The node in React's `stateNode.node` wrapper. |
+| `getShadowNodes(host, out?)` | Every node React may diff against or re-attach: one after a commit, two while a render is in flight. |
+| `getNativeTag(host)` | `undefined` once the view has unmounted. |
+| `getParentHost(host)` | The nearest native view above (ActivityIndicator's wrapper, on both platforms). |
+| `patchRenderedProps(host, patch)` | Replaces React's diff baseline on both halves. **The only write**, used by Uniwind's baseline. |
+
+### Hidden state
+| | |
+|---|---|
+| `isHidden(host)` | Inside a Suspense fallback, a frozen screen or a hidden ``: suspend instead of unlinking. |
+| `isSuspended` · `isFrozen` · `isInHiddenActivity` | Why. Frozen means react-freeze (react-native-screens `freezeOnBlur`, react-navigation). |
+| `collectVisibleAncestors(hosts, visit)` | Batch refresh. Each visible native view from `hosts` up to the root, visited once, in one shared pass. |
+| `collectAncestors(hosts, visit)` | The same walk, with hidden views included. These are the semantics of Uniwind's `collectHostNodes` today. |
+
+### Diagnostics
+| | |
+|---|---|
+| `verify(host, expected)` | Style keys React would re-send, i.e. undo, on this view's next render. |
+| `getOwners(host)` | *Development builds only:* the components that created the view, e.g. `['ProfileCard', 'ProfileScreen']`. |
+| `getCreationStack(host)` | *Development builds only:* where the JSX was written, for Metro to symbolicate. |
+
+### Adapters: when a 3rd-party component owns the view
+Reanimated's `AnimatedComponent` keeps its own copy of a view's style for CSS transitions, so a native-only theme change gets overridden. The adapter keeps that copy in sync, so the theme change **animates** instead.
+
+```ts
+const owners = Fiber.createOwnerRegistry({
+ name: 'unistyles',
+ epoch: () => themeVersion, // bump on every runtime change
+ contexts: [ScopedThemeContext],
+ contextKey: (_ctx, scope) => scope?.name, // stable scopes are renamed in place
+ resolve: (props, read) => freshStyles(props, read(ScopedThemeContext)),
+}, [Reanimated.cssAdapter], warnOnce)
+
+owners.link(host) // in the link path: ~86 ns for a plain view
+owners.refresh(hosts) // after a native-only update
+```
+
+An adapter that throws is switched off for the session and reported once. Private Reanimated fields are pinned by a contract test against the 4.5.1 and 4.7.0 sources.
+
+## Compatibility
+
+| | |
+|---|---|
+| React | 19.2, and 19.3.1 with RN nightly (fiber tags pinned for 19.x) |
+| React Native | 0.87.1, 0.86.3 · iOS, Android · development and production. CI also runs React Native **nightly** (main) every day; 0.89.0-nightly-20261016 passes |
+| Reanimated | 4.5.1, 4.7.0 |
+| Unistyles | 3.5.3 and `main` (its real code runs in the suite) |
+| Uniwind | its lookups copied verbatim into the suite |
+| Engine | Hermes (legacy Hermes benchmarked) |
+
+## License
+
+MIT
diff --git a/bench/build.mjs b/bench/build.mjs
new file mode 100644
index 0000000..3052e3c
--- /dev/null
+++ b/bench/build.mjs
@@ -0,0 +1,34 @@
+// esbuild bundle of a bench entry with the production renderer. Imports are hoisted: use metro.mjs for app numbers.
+import path from 'node:path'
+import { createRequire } from 'node:module'
+
+const require = createRequire(import.meta.url)
+const { ESBUILD, RN, REACT, SCHEDULER } = require('../test/harness/paths.cjs')
+const esbuild = require(ESBUILD)
+const map = {
+ react: path.join(REACT, 'cjs', 'react.production.js'),
+ scheduler: path.join(SCHEDULER, 'cjs', 'scheduler-unstable_mock.production.js'),
+ 'react-native-renderer': path.join(RN, 'Libraries', 'Renderer', 'implementations', 'ReactFabric-prod.js'),
+}
+const [entry, out] = process.argv.slice(2)
+await esbuild.build({
+ entryPoints: [entry],
+ outfile: out,
+ bundle: true,
+ format: 'iife',
+ target: 'es2019', // Hermes: no class fields / ?? lowering issues
+ define: { 'process.env.NODE_ENV': '"production"', __DEV__: 'false' },
+ plugins: [{
+ name: 'rn',
+ setup(build) {
+ build.onResolve({ filter: /^(react|scheduler|react-native-renderer)$/ }, (a) => ({ path: map[a.path] }))
+ build.onResolve({ filter: /(ReactNativePrivateInterface|react-native\/react-private-interface)$/ }, () => ({ path: 'pi', namespace: 'stub' }))
+ build.onResolve({ filter: /(ReactNativePrivateInitializeCore|react-native\/setup-env)$/ }, () => ({ path: 'init', namespace: 'stub' }))
+ build.onLoad({ filter: /.*/, namespace: 'stub' }, (a) => ({
+ contents: a.path === 'pi' ? 'module.exports = globalThis.__unifiberPrivateInterface' : '',
+ loader: 'js',
+ }))
+ },
+ }],
+ logLevel: 'warning',
+})
diff --git a/bench/collect.ts b/bench/collect.ts
new file mode 100644
index 0000000..e2f2544
--- /dev/null
+++ b/bench/collect.ts
@@ -0,0 +1,244 @@
+// Batch refresh walks on a 3-screen stack (2 frozen). Run with bench/metro.mjs or bench/run-hermes.mjs.
+declare const require: any
+declare const print: undefined | ((...args: Array) => void)
+import { collectAncestors, collectVisibleAncestors, getParentHost, isHidden, type Host } from '../src/host'
+import { Fiber } from '../src/index'
+
+const log = typeof print === 'function' ? print : console.log
+const now: () => number = typeof performance !== 'undefined' ? () => performance.now() : () => Date.now()
+
+;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = false
+require('../test/harness/stubs.cjs').install()
+const React = require('react')
+const ReactFabric = require('react-native-renderer')
+const Scheduler = require('scheduler')
+const h = React.createElement
+
+// react-freeze 1.0.4, inlined
+const infinite = { then() {} }
+const Suspender = ({ freeze, children }: any) => {
+ if (freeze) throw infinite
+ return children
+}
+const Freeze = ({ freeze, children }: any) => h(React.Suspense, { fallback: null }, h(Suspender, { freeze }, children))
+
+const SHELL = 20
+const SCREEN = 15
+const LEAVES = 300
+const Wrap = ({ d, children }: any) => (d === 0 ? children : h(Wrap, { d: d - 1 }, h('RCTView', null, children)))
+const refs: Array> = [[], [], []]
+const screen = (i: number, frozen: boolean, n: number) =>
+ h(Freeze, { key: i, freeze: frozen },
+ h(Wrap, { d: SCREEN },
+ ...Array.from({ length: LEAVES }, (_, k) => h('RCTView', { key: k, n, ref: (r: any) => r && (refs[i][k] = r) }))))
+const app = (frozen: [boolean, boolean, boolean], n: number) => h(Wrap, { d: SHELL }, ...frozen.map((f, i) => screen(i, f, n)))
+
+const commit = (element: any) => {
+ ReactFabric.render(element, 1, null, true)
+ Scheduler.unstable_flushAll()
+}
+commit(app([false, false, false], 0))
+const hosts: Array = refs.flat().map((r) => Fiber.getHostFor(r)!)
+const fibers = refs.flat().map((r) => r.__internalInstanceHandle)
+
+// Unistyles ShadowRegistry/index.ts:78-130
+const unistylesToday = () => {
+ const refreshedTags = new Set()
+ const hostFibers: Array = []
+ let count = 0
+ for (const fiber of fibers) {
+ let hidden = false
+ hostFibers.length = 0
+ for (let current: any = fiber; current; current = current.return) {
+ if ((current.tag === 13 || current.tag === 22) && current.memoizedState !== null) hidden = true
+ if (current.tag !== 5) continue
+ if (refreshedTags.has(current.stateNode?.canonical?.nativeTag)) break
+ hostFibers.push(current)
+ }
+ if (hidden) continue
+ for (const hostFiber of hostFibers) {
+ refreshedTags.add(hostFiber.stateNode?.canonical?.nativeTag)
+ count++
+ }
+ }
+ return count
+}
+// Uniwind fiberBaseline.ts:216-237, counting hosts
+const uniwindToday = () => {
+ const visited = new Set()
+ let count = 0
+ for (const fiber of fibers) {
+ for (let current = fiber; current && !visited.has(current); current = current.return) {
+ visited.add(current)
+ if (current.alternate) visited.add(current.alternate)
+ if (current.tag === 5) count++
+ }
+ }
+ return count
+}
+// naive: isHidden per host, then its ancestors
+const perHost = () => {
+ const seen = new Set()
+ let count = 0
+ for (const host of hosts) {
+ if (isHidden(host)) continue
+ for (let current: Host | undefined = host; current && !seen.has(current); current = getParentHost(current)) {
+ seen.add(current)
+ count++
+ }
+ }
+ return count
+}
+const collect = () => {
+ let count = 0
+ collectVisibleAncestors(hosts, () => count++)
+ return count
+}
+
+// registry-free: walk the committed tree down from HostRoot.current
+let fiberCount = 0
+const topDown = () => {
+ let root: any = fibers[0]
+ while (root.return) root = root.return
+ let count = 0
+ let visited = 0
+ const stack: Array = [root.stateNode.current]
+ while (stack.length) {
+ let fiber = stack.pop()
+ for (; fiber; fiber = fiber.sibling) {
+ visited++
+ if (fiber.tag === 22 && fiber.memoizedState !== null) continue
+ if (fiber.tag === 5) count++
+ if (fiber.child) stack.push(fiber.child)
+ }
+ }
+ fiberCount = visited
+ return count
+}
+// Uniwind fiberBaseline.ts:216-237, collecting nodes as it does
+const uniwindCollectHostNodes = () => {
+ const visited = new Set()
+ const nodes = new Set()
+ for (const fiber of fibers) {
+ for (let current = fiber; current && !visited.has(current); current = current.return) {
+ visited.add(current)
+ if (current.alternate) visited.add(current.alternate)
+ if (current.tag !== 5) continue
+ for (const target of [current, current.alternate]) {
+ const node = target?.stateNode?.node
+ if (node) nodes.add(node)
+ }
+ }
+ }
+ return nodes.size
+}
+const shipped = () => {
+ let count = 0
+ collectAncestors(hosts, () => count++)
+ return count
+}
+// A: no optional chaining
+const variantNoOptional = (list: ReadonlyArray, visit: (h: any) => void) => {
+ const visited: Array = []
+ for (let h = 0; h < list.length; h++) {
+ for (let current = list[h]; current !== null && current !== undefined; current = current.return) {
+ if (current.tag !== 5) continue
+ const stateNode = current.stateNode
+ if (stateNode === null) break
+ const canonical = stateNode.canonical
+ if (visited[canonical.nativeTag] === true) break
+ visited[canonical.nativeTag] = true
+ visit(canonical.internalInstanceHandle)
+ }
+ }
+}
+// B: Set instead of the tag-indexed array
+const variantSet = (list: ReadonlyArray, visit: (h: any) => void) => {
+ const visited = new Set()
+ for (let h = 0; h < list.length; h++) {
+ for (let current = list[h]; current; current = current.return) {
+ if (current.tag !== 5) continue
+ const canonical = current.stateNode?.canonical
+ if (canonical === undefined || visited.has(canonical.nativeTag)) break
+ visited.add(canonical.nativeTag)
+ visit(canonical.internalInstanceHandle)
+ }
+ }
+}
+// C: stamp on React's canonical object (writes React's memory: lower bound only)
+let pass = 0
+const variantStamp = (list: ReadonlyArray, visit: (h: any) => void) => {
+ const stamp = ++pass
+ for (let h = 0; h < list.length; h++) {
+ for (let current = list[h]; current; current = current.return) {
+ if (current.tag !== 5) continue
+ const canonical = current.stateNode?.canonical
+ if (canonical === undefined || canonical.__unifiberPass === stamp) break
+ canonical.__unifiberPass = stamp
+ visit(canonical.internalInstanceHandle)
+ }
+ }
+}
+// D: no callback
+const variantNoCallback = (list: ReadonlyArray) => {
+ const visited: Array = []
+ let count = 0
+ for (let h = 0; h < list.length; h++) {
+ for (let current = list[h]; current; current = current.return) {
+ if (current.tag !== 5) continue
+ const canonical = current.stateNode?.canonical
+ if (canonical === undefined || visited[canonical.nativeTag]) break
+ visited[canonical.nativeTag] = true
+ count++
+ }
+ }
+ return count
+}
+const counted = (fn: (list: ReadonlyArray, visit: (h: any) => void) => void) => () => {
+ let count = 0
+ fn(hosts, () => count++)
+ return count
+}
+
+const cases: Record number> = {
+ 'unistyles today': unistylesToday,
+ 'uniwind today (no hidden check)': uniwindToday,
+ 'isHidden per host + walk': perHost,
+ 'collectVisibleAncestors': collect,
+ 'uniwind collectHostNodes (nodes)': uniwindCollectHostNodes,
+ 'collectAncestors (shipped)': shipped,
+ 'collectAncestors A: no optional chaining': counted(variantNoOptional),
+ 'collectAncestors B: Set': counted(variantSet),
+ 'collectAncestors C: stamp on canonical': counted(variantStamp),
+ 'collectAncestors D: no callback': () => variantNoCallback(hosts),
+ 'top-down walk, no registry': topDown,
+}
+const PASSES = Number((globalThis as any).PASSES ?? 200)
+const time = (fn: () => number) => {
+ const start = now()
+ for (let i = 0; i < PASSES; i++) fn()
+ return ((now() - start) * 1000) / PASSES
+}
+const measure = (label: string) => {
+ for (const fn of Object.values(cases)) time(fn)
+ const results: Record> = {}
+ for (let round = 0; round < 7; round++) {
+ const names = round % 2 ? Object.keys(cases).reverse() : Object.keys(cases)
+ for (const name of names) (results[name] ??= []).push(time(cases[name]))
+ }
+ log(`-- ${label}`)
+ for (const name of Object.keys(cases)) {
+ const m = results[name].sort((a, b) => a - b)[3]
+ log(`${name.padEnd(34)} ${m.toFixed(0).padStart(7)} µs hosts visited ${String(cases[name]()).padStart(4)}`)
+ }
+}
+
+log(`${hosts.length} linked leaves, 3 screens, ~${(SHELL + SCREEN) * 2 + 8} fibers deep; µs per batch, median of 7 ABBA rounds`)
+commit(app([false, true, true], 1))
+measure('screen 0 focused, 1-2 frozen (never toggled back)')
+commit(app([true, false, true], 2))
+commit(app([false, true, true], 3))
+commit(app([false, true, true], 4))
+measure('after navigating 0 -> 1 -> 0 (halves disagree on screens 0-1)')
+topDown()
+log(`committed tree: ${fiberCount} fibers visited by the top-down walk`)
diff --git a/bench/get-shadow-node.ts b/bench/get-shadow-node.ts
new file mode 100644
index 0000000..938a8b6
--- /dev/null
+++ b/bench/get-shadow-node.ts
@@ -0,0 +1,228 @@
+// ref -> shadow node / Host: the shipped API vs the alternatives in bench/shadowNode.ts.
+declare const require: any
+declare const print: undefined | ((...args: Array) => void)
+import { Fiber, Reanimated } from '../src/index'
+import { getHostFor, getHostForFn, getHostHandle, hostShadowNode, getHostLiteral, getShadowNode, getShadowNodeOfKind, Kind, shadowNodeResolver, unistylesToday } from './shadowNode'
+
+const log = typeof print === 'function' ? print : console.log
+const now: () => number = typeof performance !== 'undefined' ? () => performance.now() : () => Date.now()
+
+;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = false
+require('../test/harness/stubs.cjs').install()
+const React = require('react')
+const ReactFabric = require('react-native-renderer')
+const Scheduler = require('scheduler')
+const h = React.createElement
+
+// RN 0.87 ref shapes (ScrollView.js:1281)
+class ScrollView extends React.Component {
+ _native: any = null
+ setNative = (instance: any) => {
+ if (instance) {
+ this._native = Object.assign(instance, {
+ getScrollResponder: () => this._native,
+ getNativeScrollRef: () => this._native,
+ })
+ }
+ const ref = this.props.forwardedRef
+ if (typeof ref === 'function') ref(this._native)
+ }
+ render() {
+ return h('RCTScrollView', { ref: this.setNative, style: this.props.style }, h('RCTScrollContentView', null, this.props.children))
+ }
+}
+const ListContext = React.createContext(null)
+class VirtualizedList extends React.Component {
+ _scrollRef: any = null
+ getScrollResponder() {
+ return this._scrollRef?.getScrollResponder()
+ }
+ render() {
+ return h(ListContext, { value: this }, h(ScrollView, { forwardedRef: (r: any) => (this._scrollRef = r), style: this.props.style }))
+ }
+}
+class FlatList extends React.Component {
+ _listRef: any = null
+ getScrollResponder() {
+ return this._listRef?.getScrollResponder()
+ }
+ render() {
+ return h(VirtualizedList, { ref: (r: any) => (this._listRef = r), style: this.props.style })
+ }
+}
+class KeyboardAvoidingView extends React.Component {
+ viewRef = React.createRef()
+ render() {
+ return h('RCTView', { ref: this.viewRef, style: this.props.style })
+ }
+}
+
+const hosts: Array = []
+const composites: Array = []
+const collect = (into: Array) => (r: any) => {
+ if (r) into.push(r)
+}
+const tree = h(
+ 'RCTView',
+ null,
+ ...Array.from({ length: 900 }, (_, i) => h('RCTView', { key: `v${i}`, style: { flex: 1 }, ref: collect(hosts) })),
+ ...Array.from({ length: 50 }, (_, i) => h(FlatList, { key: `l${i}`, ref: collect(composites) })),
+ ...Array.from({ length: 50 }, (_, i) => h(KeyboardAvoidingView, { key: `k${i}`, ref: collect(composites) })),
+)
+ReactFabric.render(tree, 1, null, true)
+Scheduler.unstable_flushAll()
+
+const lists = composites.filter((c) => c instanceof FlatList)
+const kavs = composites.filter((c) => c instanceof KeyboardAvoidingView)
+const mixed = hosts.slice(0, 800).concat(lists, kavs)
+const mixedKinds = mixed.map((r) => (r instanceof FlatList ? Kind.List : r instanceof KeyboardAvoidingView ? Kind.KeyboardAvoidingView : Kind.Host))
+const mixedResolvers = mixedKinds.map(shadowNodeResolver)
+
+for (let i = 0; i < mixed.length; i++) {
+ const a = getShadowNodeOfKind(mixed[i], mixedKinds[i])
+ if (!a || a !== mixedResolvers[i](mixed[i]) || (mixedKinds[i] !== Kind.KeyboardAvoidingView && a !== unistylesToday(mixed[i]))) {
+ throw new Error(`strategies disagree at ${i}`)
+ }
+}
+if (getShadowNode(lists[0]) !== getShadowNodeOfKind(lists[0], Kind.List)) throw new Error('generic list mismatch')
+
+let sink = 0
+const kept: Array = new Array(900)
+// bound once, as consumers should under Metro's inline requires
+const { getShadowNodeFor: boundGetShadowNodeFor, getHostFor: boundGetHostFor } = Fiber
+const owners = Fiber.createOwnerRegistry({ name: 'bench', epoch: () => 0, resolve: () => undefined }, [Reanimated.cssAdapter])
+const linkedHosts = hosts.map((r) => boundGetHostFor(r))
+const boundGate = Reanimated.cssAdapter.gate
+const keptCtor = hosts.map(getHostForFn)
+const keptHandles = hosts.map(getHostHandle)
+const FiberObject = { getHostFor: getHostHandle, getShadowNode: hostShadowNode }
+class FiberStatic {
+ static getHostFor(ref: any) {
+ return getHostHandle(ref)
+ }
+ static getShadowNode(host: any) {
+ return hostShadowNode(host)
+ }
+}
+const exportsObject: any = { getHostHandle, hostShadowNode, Fiber: FiberObject, FiberStatic }
+const hostResolver = shadowNodeResolver(Kind.Host)
+const listResolver = shadowNodeResolver(Kind.List)
+const cases: Record void> = {
+ 'hosts: unistyles today': () => {
+ for (let i = 0; i < hosts.length; i++) sink += unistylesToday(hosts[i]) ? 1 : 0
+ },
+ 'hosts: getShadowNode(ref)': () => {
+ for (let i = 0; i < hosts.length; i++) sink += getShadowNode(hosts[i]) ? 1 : 0
+ },
+ 'hosts: getHostFor(ref).getShadowNode()': () => {
+ for (let i = 0; i < hosts.length; i++) sink += getHostFor(hosts[i])?.getShadowNode() ? 1 : 0
+ },
+ 'hosts: getHostFor(ref), kept': () => {
+ for (let i = 0; i < hosts.length; i++) kept[i] = getHostFor(hosts[i])
+ },
+ 'hosts: function-ctor Host, kept': () => {
+ for (let i = 0; i < hosts.length; i++) kept[i] = getHostForFn(hosts[i])
+ },
+ 'hosts: {fiber} literal, kept': () => {
+ for (let i = 0; i < hosts.length; i++) kept[i] = getHostLiteral(hosts[i])
+ },
+ 'hosts: fiber as handle, kept': () => {
+ for (let i = 0; i < hosts.length; i++) kept[i] = getHostHandle(hosts[i])
+ },
+ 'hosts: Host.getShadowNode(getHostHandle(ref))': () => {
+ for (let i = 0; i < hosts.length; i++) sink += hostShadowNode(getHostHandle(hosts[i])) ? 1 : 0
+ },
+ 'kept: ctorHost.getShadowNode()': () => {
+ for (let i = 0; i < keptCtor.length; i++) sink += keptCtor[i].getShadowNode() ? 1 : 0
+ },
+ 'kept: mod.getShadowNode(h) (named import)': () => {
+ for (let i = 0; i < keptHandles.length; i++) sink += exportsObject.hostShadowNode(keptHandles[i]) ? 1 : 0
+ },
+ 'kept: mod.Fiber.getShadowNode(h) (object)': () => {
+ for (let i = 0; i < keptHandles.length; i++) sink += exportsObject.Fiber.getShadowNode(keptHandles[i]) ? 1 : 0
+ },
+ 'kept: mod.FiberStatic.getShadowNode(h) (class)': () => {
+ for (let i = 0; i < keptHandles.length; i++) sink += exportsObject.FiberStatic.getShadowNode(keptHandles[i]) ? 1 : 0
+ },
+ 'hosts: mod.getHostHandle(ref), kept': () => {
+ for (let i = 0; i < hosts.length; i++) kept[i] = exportsObject.getHostHandle(hosts[i])
+ },
+ 'hosts: mod.Fiber.getHostFor(ref), kept': () => {
+ for (let i = 0; i < hosts.length; i++) kept[i] = exportsObject.Fiber.getHostFor(hosts[i])
+ },
+ 'kept: Host.getShadowNode(handle)': () => {
+ for (let i = 0; i < keptHandles.length; i++) sink += hostShadowNode(keptHandles[i]) ? 1 : 0
+ },
+ 'hosts: Fiber.getShadowNodeFor(ref) (shipped)': () => {
+ for (let i = 0; i < hosts.length; i++) sink += Fiber.getShadowNodeFor(hosts[i]) ? 1 : 0
+ },
+ 'hosts: Fiber.getHostFor(ref), kept (shipped)': () => {
+ for (let i = 0; i < hosts.length; i++) kept[i] = Fiber.getHostFor(hosts[i])
+ },
+ 'hosts: getShadowNodeFor(ref) (shipped, bound)': () => {
+ for (let i = 0; i < hosts.length; i++) sink += boundGetShadowNodeFor(hosts[i]) ? 1 : 0
+ },
+ 'hosts: getHostFor(ref), kept (shipped, bound)': () => {
+ for (let i = 0; i < hosts.length; i++) kept[i] = boundGetHostFor(hosts[i])
+ },
+ 'hosts: cssAdapter.gate(host) only (bound)': () => {
+ for (let i = 0; i < linkedHosts.length; i++) sink += boundGate(linkedHosts[i]!) ? 1 : 0
+ },
+ 'hosts: inline collapsable read (floor)': () => {
+ for (let i = 0; i < linkedHosts.length; i++) sink += (linkedHosts[i] as any).memoizedProps?.collapsable === false ? 1 : 0
+ },
+ 'hosts: owners.link(host) (Reanimated gate, shipped)': () => {
+ for (let i = 0; i < linkedHosts.length; i++) owners.link(linkedHosts[i])
+ },
+ 'mixed 900: Fiber.getShadowNodeFor(ref) (shipped)': () => {
+ for (let i = 0; i < mixed.length; i++) sink += Fiber.getShadowNodeFor(mixed[i]) ? 1 : 0
+ },
+ 'hosts: getShadowNode(ref, kind)': () => {
+ for (let i = 0; i < hosts.length; i++) sink += getShadowNodeOfKind(hosts[i], Kind.Host) ? 1 : 0
+ },
+ 'hosts: resolver per wrapper': () => {
+ for (let i = 0; i < hosts.length; i++) sink += hostResolver(hosts[i]) ? 1 : 0
+ },
+ 'lists: unistyles today': () => {
+ for (let i = 0; i < lists.length; i++) sink += unistylesToday(lists[i]) ? 1 : 0
+ },
+ 'lists: getShadowNode(ref) fiber descent': () => {
+ for (let i = 0; i < lists.length; i++) sink += getShadowNode(lists[i]) ? 1 : 0
+ },
+ 'lists: resolver per wrapper': () => {
+ for (let i = 0; i < lists.length; i++) sink += listResolver(lists[i]) ? 1 : 0
+ },
+ 'mixed 900: getShadowNode(ref)': () => {
+ for (let i = 0; i < mixed.length; i++) sink += getShadowNode(mixed[i]) ? 1 : 0
+ },
+ 'mixed 900: getHostFor(ref).getShadowNode()': () => {
+ for (let i = 0; i < mixed.length; i++) sink += getHostFor(mixed[i])?.getShadowNode() ? 1 : 0
+ },
+ 'mixed 900: getShadowNode(ref, kind)': () => {
+ for (let i = 0; i < mixed.length; i++) sink += getShadowNodeOfKind(mixed[i], mixedKinds[i]) ? 1 : 0
+ },
+ 'mixed 900: resolver per wrapper': () => {
+ for (let i = 0; i < mixed.length; i++) sink += mixedResolvers[i](mixed[i]) ? 1 : 0
+ },
+}
+
+const PASSES = Number((globalThis as any).PASSES ?? 400)
+const time = (fn: () => void) => {
+ const start = now()
+ for (let i = 0; i < PASSES; i++) fn()
+ return ((now() - start) * 1000) / PASSES // µs per pass
+}
+for (const fn of Object.values(cases)) time(fn)
+const results: Record> = {}
+for (let round = 0; round < 11; round++) {
+ const names = round % 2 ? Object.keys(cases).reverse() : Object.keys(cases)
+ for (const name of names) (results[name] ??= []).push(time(cases[name]))
+}
+const median = (xs: Array) => xs.slice().sort((a, b) => a - b)[xs.length >> 1]
+log(`hosts ${hosts.length}, lists ${lists.length}, mixed ${mixed.length}; µs per pass, median of 11 ABBA rounds`)
+for (const name of Object.keys(results)) {
+ const n = name.startsWith('lists') ? lists.length : name.startsWith('hosts') || name.startsWith('kept') ? hosts.length : mixed.length
+ const m = median(results[name])
+ log(`${name.padEnd(42)} ${m.toFixed(1).padStart(8)} µs ${((m * 1000) / n).toFixed(1).padStart(7)} ns/ref`)
+}
+if (sink < 0) log(sink)
diff --git a/bench/hermes.mjs b/bench/hermes.mjs
new file mode 100644
index 0000000..de96592
--- /dev/null
+++ b/bench/hermes.mjs
@@ -0,0 +1,15 @@
+// Transpiles classes like Metro does (legacy Hermes has none). node bench/hermes.mjs [passes]
+import fs from 'node:fs'
+import { createRequire } from 'node:module'
+
+const require = createRequire(import.meta.url)
+const { BABEL_CORE } = require('../test/harness/paths.cjs')
+const babel = require(BABEL_CORE)
+const [input, output, passes = '2000'] = process.argv.slice(2)
+const { code } = babel.transformSync(fs.readFileSync(input, 'utf8'), {
+ babelrc: false,
+ configFile: false,
+ compact: false,
+ plugins: [require.resolve('@babel/plugin-transform-classes'), require.resolve('@babel/plugin-transform-block-scoping')],
+})
+fs.writeFileSync(output, `globalThis.PASSES = ${passes};\n${code}`)
diff --git a/bench/hidden.ts b/bench/hidden.ts
new file mode 100644
index 0000000..0c8fb49
--- /dev/null
+++ b/bench/hidden.ts
@@ -0,0 +1,41 @@
+// isHidden vs today's walk on a deep frozen-then-thawed screen. V8.
+import { createRequire } from 'node:module'
+import { isHidden } from '../src/host'
+import { Fiber } from '../src/index'
+
+const require = createRequire(import.meta.url)
+const { React, render, View } = require('../test/harness/fabric.cjs')({ mode: 'production' })
+const h = React.createElement
+const { Freeze } = require(require('../test/harness/paths.cjs').REACT_FREEZE)
+
+const DEPTH = Number(process.env.DEPTH ?? 200)
+const Wrap = ({ d, children }: any) => (d === 0 ? children : h(Wrap, { d: d - 1 }, h(View, null, children)))
+let instance: any
+const leaves = (n: number) => Array.from({ length: 100 }, (_, i) => h(View, { key: i, ref: (r: any) => r && i === 0 && (instance = r), n }))
+const app = (freeze: boolean, n: number) => h(Wrap, { d: DEPTH / 2 }, h(Freeze, { freeze }, h(Wrap, { d: DEPTH / 2 }, ...leaves(n))))
+const tag = render(app(false, 0))
+const host = Fiber.getHostFor(instance)!
+const creation = instance.__internalInstanceHandle
+const today = (fiber: any) => {
+ for (let c = fiber?.return; c; c = c.return) if ((c.tag === 13 || c.tag === 22) && c.memoizedState !== null) return true
+ return false
+}
+const time = (name: string, fn: () => unknown) => {
+ for (let i = 0; i < 2000; i++) fn()
+ const runs: Array = []
+ for (let r = 0; r < 11; r++) {
+ const t = performance.now()
+ for (let i = 0; i < 20000; i++) fn()
+ runs.push(((performance.now() - t) * 1e6) / 20000)
+ }
+ console.log(`${name.padEnd(36)} ${runs.sort((a, b) => a - b)[5].toFixed(0).padStart(6)} ns -> ${fn()}`)
+}
+console.log(`depth ~${DEPTH * 2 + 4} fibers`)
+time('never toggled: today', () => today(creation))
+time('never toggled: isHidden(host)', () => isHidden(host))
+render(app(true, 1), tag)
+render(app(false, 2), tag)
+time('frozen+thawed: today', () => today(creation))
+time('frozen+thawed: isHidden(host)', () => isHidden(host))
+render(app(true, 3), tag)
+time('frozen: isHidden(host)', () => isHidden(host))
diff --git a/bench/link-path.ts b/bench/link-path.ts
new file mode 100644
index 0000000..26838c9
--- /dev/null
+++ b/bench/link-path.ts
@@ -0,0 +1,63 @@
+// Per-link overhead on 901 views. V8: compare ratios only.
+import { createRequire } from 'node:module'
+import * as U from '../src/fiber'
+import { Fiber } from '../src/index'
+import { isHidden } from '../src/host'
+import { reanimatedCSSAdapter } from '../src/adapters/reanimated'
+import { createOwnerRegistry, type StyleSource } from '../src/adapters/registry'
+
+const require = createRequire(import.meta.url)
+const { React, render, View } = require('../test/harness/fabric.cjs')({ mode: 'production' })
+const h = React.createElement
+
+const refs: Array = []
+render(h(View, { ref: (r: any) => refs.push(r) }, ...Array.from({ length: 900 }, (_, i) => h(View, { key: i, style: { flex: 1 }, ref: (r: any) => refs.push(r) }))))
+const source: StyleSource = { name: 's', epoch: () => 0, resolve: () => undefined }
+const registry = createOwnerRegistry(source, [reanimatedCSSAdapter])
+const weak = new WeakMap()
+let sink = 0
+
+const cases: Record void> = {
+ 'hostFiberOf only (baseline)': () => {
+ for (const r of refs) sink += U.hostFiberOf(r) ? 1 : 0
+ },
+ '+ registry.link (collapsable gate)': () => {
+ for (const r of refs) registry.link(Fiber.getHostFor(r))
+ },
+ '+ unconditional walk to class/host': () => {
+ for (const r of refs) {
+ let current = (U.hostFiberOf(r) as any)?.return
+ while (current && current.tag !== U.Tag.ClassComponent && current.tag !== U.Tag.HostComponent) current = current.return
+ sink += current ? 1 : 0
+ }
+ },
+ '+ isHidden walk (to root)': () => {
+ for (const r of refs) sink += isHidden(U.hostFiberOf(r) as any) ? 1 : 0
+ },
+ '+ WeakMap set+delete per link': () => {
+ for (const r of refs) {
+ const f = U.hostFiberOf(r)
+ weak.set(r, f)
+ weak.delete(r)
+ }
+ },
+}
+const time = (fn: () => void, n: number) => {
+ const start = process.hrtime.bigint()
+ for (let i = 0; i < n; i++) fn()
+ return Number(process.hrtime.bigint() - start) / 1e6 / n
+}
+for (const fn of Object.values(cases)) time(fn, 2000) // warm up
+const results: Record> = {}
+for (let round = 0; round < 15; round++) {
+ const order = round % 2 ? Object.keys(cases).reverse() : Object.keys(cases)
+ for (const name of order) (results[name] ??= []).push(time(cases[name], 500))
+}
+const median = (xs: Array) => xs.sort((a, b) => a - b)[xs.length >> 1]
+const base = median(results['hostFiberOf only (baseline)'])
+console.log(`${refs.length} host fibers, ms per pass (median of 15), delta vs baseline`)
+for (const [name, xs] of Object.entries(results)) {
+ const m = median(xs)
+ console.log(`${name.padEnd(40)} ${m.toFixed(4)} ${name.includes('baseline') ? '' : '+' + (m - base).toFixed(4)}`)
+}
+if (sink < 0) console.log(sink)
diff --git a/bench/metro.mjs b/bench/metro.mjs
new file mode 100644
index 0000000..16c9b5e
--- /dev/null
+++ b/bench/metro.mjs
@@ -0,0 +1,56 @@
+// A bench entry built like an app ships (Metro, RN's default config, inline requires), run on Hermes.
+// HERMES=/path/to/hermes node bench/metro.mjs [passes] [--no-inline-requires]
+import { execFileSync } from 'node:child_process'
+import fs from 'node:fs'
+import path from 'node:path'
+import { createRequire } from 'node:module'
+
+const require = createRequire(import.meta.url)
+const { ROOT, RN, SCHEDULER } = require('../test/harness/paths.cjs')
+const Metro = require('metro')
+const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')
+
+const args = process.argv.slice(2)
+const inlineRequires = !args.includes('--no-inline-requires')
+const [entry, passes = '400'] = args.filter((a) => !a.startsWith('--'))
+const hermes = process.env.HERMES
+
+if (!entry || !hermes || !fs.existsSync(hermes)) {
+ console.error('usage: HERMES=/path/to/hermes node bench/metro.mjs [passes] [--no-inline-requires]')
+ process.exit(1)
+}
+
+const outDir = path.join(ROOT, 'bench', 'out')
+const stubDir = path.join(outDir, 'metro-stubs')
+fs.mkdirSync(stubDir, { recursive: true })
+fs.writeFileSync(path.join(stubDir, 'private-interface.js'), 'module.exports = globalThis.__unifiberPrivateInterface\n')
+fs.writeFileSync(path.join(stubDir, 'empty.js'), 'module.exports = {}\n')
+fs.writeFileSync(path.join(stubDir, 'passes.js'), `globalThis.PASSES = ${Number(passes)}\n`)
+
+const aliases = {
+ 'react-native-renderer': path.join(RN, 'Libraries', 'Renderer', 'implementations', 'ReactFabric-prod.js'),
+ scheduler: path.join(SCHEDULER, 'cjs', 'scheduler-unstable_mock.production.js'),
+ 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface': path.join(stubDir, 'private-interface.js'),
+ 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore': path.join(stubDir, 'empty.js'),
+ 'react-native/react-private-interface': path.join(stubDir, 'private-interface.js'),
+ 'react-native/setup-env': path.join(stubDir, 'empty.js'),
+}
+
+const config = mergeConfig(await getDefaultConfig(ROOT), {
+ projectRoot: ROOT,
+ watchFolders: [ROOT],
+ cacheStores: [],
+ resolver: {
+ sourceExts: ['ts', 'tsx', 'js', 'cjs', 'json'],
+ resolveRequest: (context, moduleName, platform) =>
+ aliases[moduleName] ? { type: 'sourceFile', filePath: aliases[moduleName] } : context.resolveRequest(context, moduleName, platform),
+ },
+ serializer: { getModulesRunBeforeMainModule: () => [], getPolyfills: () => [path.join(stubDir, 'passes.js')] },
+ transformer: { getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, inlineRequires } }) },
+})
+
+const name = `${path.basename(entry, '.ts')}${inlineRequires ? '' : '-no-inline'}`
+const bundle = path.join(outDir, `${name}.metro.js`)
+
+await Metro.runBuild(config, { entry: path.resolve(entry), out: bundle, platform: 'ios', dev: false, minify: true })
+execFileSync(hermes, ['-O', '-w', bundle], { stdio: 'inherit' })
diff --git a/bench/run-hermes.mjs b/bench/run-hermes.mjs
new file mode 100644
index 0000000..1387376
--- /dev/null
+++ b/bench/run-hermes.mjs
@@ -0,0 +1,24 @@
+// esbuild bundle -> class transform -> Hermes. HERMES must be the VM binary (hermes, not hermesc).
+// HERMES=/path/to/hermes node bench/run-hermes.mjs [passes]
+import { execFileSync } from 'node:child_process'
+import fs from 'node:fs'
+import path from 'node:path'
+
+const [entry, passes = '400'] = process.argv.slice(2)
+const hermes = process.env.HERMES
+
+if (!entry || !hermes || !fs.existsSync(hermes)) {
+ console.error('usage: HERMES=/path/to/hermes node bench/run-hermes.mjs [passes]')
+ process.exit(1)
+}
+
+const here = import.meta.dirname
+const name = path.basename(entry, '.ts')
+const outDir = path.join(here, 'out')
+const bundle = path.join(outDir, `${name}.js`)
+const hermesBundle = path.join(outDir, `${name}-hermes.js`)
+
+fs.mkdirSync(outDir, { recursive: true })
+execFileSync(process.execPath, [path.join(here, 'build.mjs'), entry, bundle], { stdio: 'inherit' })
+execFileSync(process.execPath, [path.join(here, 'hermes.mjs'), bundle, hermesBundle, passes], { stdio: 'inherit' })
+execFileSync(hermes, ['-O', '-w', hermesBundle], { stdio: 'inherit' })
diff --git a/bench/shadowNode.ts b/bench/shadowNode.ts
new file mode 100644
index 0000000..5e8c5bd
--- /dev/null
+++ b/bench/shadowNode.ts
@@ -0,0 +1,129 @@
+// Design alternatives for ref -> shadow node, benchmarked by bench/get-shadow-node.ts.
+
+export const Kind = {
+ Host: 0,
+ List: 1, // FlatList, SectionList, VirtualizedList: getScrollResponder() returns ScrollView's augmented host instance
+ KeyboardAvoidingView: 2,
+ ImageBackground: 3,
+ RefreshControl: 4,
+ Touchable: 5, // Button on Android: a Touchable class above its host
+ ActivityIndicator: 6, // ref is the spinner, style is on the wrapper host above it
+} as const
+export type Kind = (typeof Kind)[keyof typeof Kind]
+
+type ShadowNode = object
+
+const nodeOfHandle = (handle: any): ShadowNode | undefined => handle?.stateNode?.node
+
+const firstHostBelow = (fiber: any) => {
+ while (fiber && fiber.tag !== 5) {
+ fiber = fiber.child
+ }
+
+ return fiber
+}
+
+const hostAbove = (fiber: any) => {
+ fiber = fiber?.return
+
+ while (fiber && fiber.tag !== 5) {
+ fiber = fiber.return
+ }
+
+ return fiber
+}
+
+// 1. generic: host fast path, then descend from the composite
+export const getShadowNode = (ref: any): ShadowNode | undefined => {
+ const handle = ref?.__internalInstanceHandle
+
+ if (handle !== undefined) {
+ return handle.stateNode?.node
+ }
+
+ return nodeOfHandle(firstHostBelow(ref?._reactInternals))
+}
+
+// 2. the caller passes the component kind
+export const getShadowNodeOfKind = (ref: any, kind: Kind): ShadowNode | undefined => {
+ switch (kind) {
+ case Kind.Host:
+ return ref?.__internalInstanceHandle?.stateNode?.node
+ case Kind.List:
+ return nodeOfHandle(ref?.getScrollResponder?.()?.__internalInstanceHandle)
+ case Kind.KeyboardAvoidingView:
+ return nodeOfHandle(ref?.viewRef?.current?.__internalInstanceHandle)
+ case Kind.ImageBackground:
+ return nodeOfHandle(ref?._viewRef?.__internalInstanceHandle)
+ case Kind.RefreshControl:
+ return nodeOfHandle(ref?._nativeRef?.__internalInstanceHandle)
+ case Kind.Touchable:
+ return nodeOfHandle(firstHostBelow(ref?._reactInternals))
+ case Kind.ActivityIndicator:
+ return nodeOfHandle(hostAbove(ref?.__internalInstanceHandle))
+ }
+}
+
+// 3. a resolver picked once per wrapper
+const resolvers: Record ShadowNode | undefined> = {
+ [Kind.Host]: (ref) => ref?.__internalInstanceHandle?.stateNode?.node,
+ [Kind.List]: (ref) => nodeOfHandle(ref?.getScrollResponder?.()?.__internalInstanceHandle),
+ [Kind.KeyboardAvoidingView]: (ref) => nodeOfHandle(ref?.viewRef?.current?.__internalInstanceHandle),
+ [Kind.ImageBackground]: (ref) => nodeOfHandle(ref?._viewRef?.__internalInstanceHandle),
+ [Kind.RefreshControl]: (ref) => nodeOfHandle(ref?._nativeRef?.__internalInstanceHandle),
+ [Kind.Touchable]: (ref) => nodeOfHandle(firstHostBelow(ref?._reactInternals)),
+ [Kind.ActivityIndicator]: (ref) => nodeOfHandle(hostAbove(ref?.__internalInstanceHandle)),
+}
+export const shadowNodeResolver = (kind: Kind) => resolvers[kind]
+
+// today's Unistyles findShadowNodeForHandle, for comparison
+export const unistylesToday = (handle: any) =>
+ handle?.__internalInstanceHandle?.stateNode?.node ??
+ handle?.getScrollResponder?.()?.getNativeScrollRef?.()?.__internalInstanceHandle?.stateNode?.node ??
+ handle?.getNativeScrollRef?.()?.__internalInstanceHandle?.stateNode?.node ??
+ handle?._viewRef?.__internalInstanceHandle?.stateNode?.node ??
+ handle?.viewRef?.current?.__internalInstanceHandle?.stateNode?.node ??
+ handle?._nativeRef?.__internalInstanceHandle?.stateNode?.node
+
+// 4. a Host class wrapping the fiber
+export class Host {
+ constructor(readonly fiber: any) {}
+
+ getShadowNode(): ShadowNode | undefined {
+ return this.fiber.stateNode?.node
+ }
+
+ getParentHost(): Host | undefined {
+ const parent = hostAbove(this.fiber)
+
+ return parent ? new Host(parent) : undefined
+ }
+}
+
+export const getHostFor = (ref: any): Host | undefined => {
+ const handle = ref?.__internalInstanceHandle ?? firstHostBelow(ref?._reactInternals)
+
+ return handle ? new Host(handle) : undefined
+}
+
+// 5. same object without class syntax, and as a bare literal
+function HostFn(this: any, fiber: any) {
+ this.fiber = fiber
+}
+HostFn.prototype.getShadowNode = function (this: any) {
+ return this.fiber.stateNode?.node
+}
+export const getHostForFn = (ref: any): any => {
+ const handle = ref?.__internalInstanceHandle ?? firstHostBelow(ref?._reactInternals)
+
+ return handle ? new (HostFn as any)(handle) : undefined
+}
+export const getHostLiteral = (ref: any): any => {
+ const handle = ref?.__internalInstanceHandle ?? firstHostBelow(ref?._reactInternals)
+
+ return handle ? { fiber: handle } : undefined
+}
+
+// 6. the fiber itself as the handle (shipped)
+export const getHostHandle = (ref: any): any => ref?.__internalInstanceHandle ?? firstHostBelow(ref?._reactInternals)
+export const hostShadowNode = (host: any): ShadowNode | undefined => host.stateNode?.node
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..f4ed9fe
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,882 @@
+{
+ "lockfileVersion": 2,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "unifiber",
+ "devDependencies": {
+ "@babel/core": "7.29.7",
+ "@babel/runtime": "7.29.10",
+ "@react-native/babel-preset": "0.87.1",
+ "@types/node": "22.20.5",
+ "@types/react": "19.2.18",
+ "esbuild": "0.27.3",
+ "react": "19.2.3",
+ "react-freeze": "1.0.4",
+ "react-native": "0.87.1",
+ "react-native-0.86": "npm:react-native@0.86.3",
+ "react-native-reanimated": "4.7.0",
+ "react-native-reanimated-4.5": "npm:react-native-reanimated@4.5.1",
+ "react-native-unistyles": "3.5.3",
+ "scheduler": "0.27.0",
+ "tsx": "4.23.15",
+ "typescript": "6.0.3",
+ },
+ "peerDependencies": {
+ "react": ">=19.0.0",
+ "react-native": ">=0.81.0",
+ },
+ },
+ },
+ "packages": {
+ "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
+
+ "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
+
+ "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
+
+ "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="],
+
+ "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.5.3", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-/YY9soQuWa8TURtHXHT+GbklpbAA+dZErxFGHIr6qgDg96xuGWrTAs1+MfjgVJPF0YLQsOVsOYs8lhgsPbOinQ=="],
+
+ "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.8", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
+
+ "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
+
+ "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="],
+
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
+
+ "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-wrap-function": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og=="],
+
+ "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
+
+ "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw=="],
+
+ "@babel/helpers": ["@babel/helpers@7.29.10", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.8" } }, "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg=="],
+
+ "@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="],
+
+ "@babel/plugin-proposal-export-default-from": ["@babel/plugin-proposal-export-default-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ=="],
+
+ "@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="],
+
+ "@babel/plugin-syntax-export-default-from": ["@babel/plugin-syntax-export-default-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw=="],
+
+ "@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ=="],
+
+ "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
+
+ "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="],
+
+ "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="],
+
+ "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="],
+
+ "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ=="],
+
+ "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA=="],
+
+ "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w=="],
+
+ "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ=="],
+
+ "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA=="],
+
+ "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g=="],
+
+ "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg=="],
+
+ "@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-flow": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ=="],
+
+ "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ=="],
+
+ "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="],
+
+ "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ=="],
+
+ "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg=="],
+
+ "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng=="],
+
+ "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ=="],
+
+ "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug=="],
+
+ "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA=="],
+
+ "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q=="],
+
+ "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/types": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A=="],
+
+ "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
+
+ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
+
+ "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.29.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg=="],
+
+ "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q=="],
+
+ "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg=="],
+
+ "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA=="],
+
+ "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg=="],
+
+ "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA=="],
+
+ "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="],
+
+ "@babel/runtime": ["@babel/runtime@7.29.10", "", {}, "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA=="],
+
+ "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.10", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.9", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ=="],
+
+ "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
+
+ "@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="],
+
+ "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
+
+ "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@react-native/asset-utils": ["@react-native/asset-utils@0.87.1", "", {}, "sha512-FeFnbn9ENPs7IVBzZt1bBWfiRGvT4q+CsWwXGFyKVW/1ol3ylBRuTtXBGHIAmcNN4fUR60nSXsCN19pzNHkPgA=="],
+
+ "@react-native/assets-registry": ["@react-native/assets-registry@0.86.3", "", {}, "sha512-TDhgCZA4wjJg84d5A9swiOQYPIWSKEEVdg9IwMFZDupQzW/F3QoLUrfAJOcalgqTDA9/buTB8awhE3Whwg6u9Q=="],
+
+ "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.87.1", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@react-native/codegen": "0.87.1" } }, "sha512-DfnyLAHG7jH4pqeID7ib6AeD6gG8T+KvM3c/w/yEa5ONkfRcHvTC8JMIRdhUqs4ruA+8xbYcNVQzewYRCPm1Xw=="],
+
+ "@react-native/babel-preset": ["@react-native/babel-preset@0.87.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@react-native/babel-plugin-codegen": "0.87.1", "babel-plugin-syntax-hermes-parser": "0.36.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-EN1oo8IqsJgq++na/buq6hYsdZJY5A8+URohSA620eGz5a+322WOAcBuvcYZTeovYwR2NegvpPk5h+QVk8iUMw=="],
+
+ "@react-native/codegen": ["@react-native/codegen@0.87.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.29.0", "hermes-parser": "0.36.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "tinyglobby": "^0.2.15", "yargs": "^17.6.2" } }, "sha512-qbaqEdlUfj2vRgvWTpMoNgHnEqAhAYJLUrpGkb0WC9n0kdtqUvgigpz4bDktZwolM9BemXwgGFgyiAnbs3t0xw=="],
+
+ "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.87.1", "", { "dependencies": { "@react-native/asset-utils": "0.87.1", "@react-native/dev-middleware": "0.87.1", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.87.0", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "0.87.1" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-aGBae6v+ngy8fIpU1EOaVxn/8DOgmJNphEdn5Eb0wTchUCxr8bDjpTJYNdrptyn3qI/elk8r9agQ2OBaFvrRlw=="],
+
+ "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.87.1", "", {}, "sha512-RNm7soJB+8YSauLnsCCylA1eVfT6JWaDTPUEF01uu9YwDyZ7remKlZbXtmVbtscbNGcp+hbI4iZUdVOpQWsHuA=="],
+
+ "@react-native/debugger-shell": ["@react-native/debugger-shell@0.87.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "debug": "^4.4.0", "fb-dotslash": "0.5.8" } }, "sha512-eNbKjcnseJIjy5XCDwyhKOT1ngOg3Dv1fVxTDtnVFqdqjqsbfY4v9JuJG1RZJ7+mFoRRe05HE8GSQU8B7n5xwQ=="],
+
+ "@react-native/dev-middleware": ["@react-native/dev-middleware@0.87.1", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.87.1", "@react-native/debugger-shell": "0.87.1", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.3.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-KgvAGUaVl6/XrWFAPK8e0ojDRbsdBjr4bnwyn6PC3CwxPQc5iv+i7hMoUwYS8ORSkHKfjt+cV86/mBQ4lG8yfA=="],
+
+ "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.87.1", "", {}, "sha512-bZ5X2BNxaSlfp2KlDtUM910QqW9G1yTIeZXghuv4ag8SAQLzm5Mf9j5GjJfT6ax2Qo2aRT15Vi3Tro/yLGJstA=="],
+
+ "@react-native/js-polyfills": ["@react-native/js-polyfills@0.86.3", "", {}, "sha512-eYIJ0es967+tePBFQDnl/gidVFxLns3fnbiK6rxscQrGodvuUO6hwxpQnfNynJ8MjbbndImXihXjcnJdc7SzJg=="],
+
+ "@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.87.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@react-native/babel-preset": "0.87.1", "hermes-parser": "0.36.1", "nullthrows": "^1.1.1" } }, "sha512-9qPN7DIBA2ldpTVeEObRuIf0ihF9iphBR17rOyuFu8UnhbzHpoLDHCqnjnOm6P21TY8GU6JgaqmD4rhtUCOFeQ=="],
+
+ "@react-native/metro-config": ["@react-native/metro-config@0.87.1", "", { "dependencies": { "@react-native/js-polyfills": "0.87.1", "@react-native/metro-babel-transformer": "0.87.1", "metro-config": "^0.87.0", "metro-runtime": "^0.87.0" } }, "sha512-oHgFFZpWDElw/ZO7YmNsTproNtHL0Ty/PWAR7Lf7YjuT7HuY11RyihQ4Kh/YseepHi/9vMBe8QNFqJCB8eMNZg=="],
+
+ "@react-native/normalize-colors": ["@react-native/normalize-colors@0.87.1", "", {}, "sha512-8+AutemzX+a7cuKgTUyb7JUk3sFYgLyrSnlTLrL6LuW/LKDE+FYE8lJGWYhJPJcE51Ge1D8yRzxgQEdEFZtuIw=="],
+
+ "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.87.1", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "0.87.1" }, "optionalPeers": ["@types/react"] }, "sha512-qSZjeX3UJrDvyfjf7yc3E68rp1XnzE+5nu8ImklhkVC0+p/XiaHPb/KGkRqDdnQyWHN55BRYSsCMEwgVI6WRNQ=="],
+
+ "@sinclair/typebox": ["@sinclair/typebox@0.27.12", "", {}, "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g=="],
+
+ "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
+
+ "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
+
+ "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="],
+
+ "@types/node": ["@types/node@22.20.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA=="],
+
+ "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
+
+ "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="],
+
+ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
+
+ "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
+
+ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
+ "acorn": ["acorn@8.19.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw=="],
+
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
+
+ "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
+
+ "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.17", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w=="],
+
+ "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="],
+
+ "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.8", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg=="],
+
+ "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.36.1", "", { "dependencies": { "hermes-parser": "0.36.1" } }, "sha512-ycduwJbvdvIMmVvlAZqGggS+pm5Eu4Bk9pcV9Sm2Z4PJNRVsKkv0g7vHj+LeuC1gHTeF67sJXFOq61IlqCa2hA=="],
+
+ "babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="],
+
+ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.11.28", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA=="],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+ "browserslist": ["browserslist@4.29.3", "", { "dependencies": { "baseline-browser-mapping": "^2.11.26", "caniuse-lite": "^1.0.30001813", "electron-to-chromium": "^1.5.439", "node-releases": "^2.0.57", "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA=="],
+
+ "bser": ["bser@2.1.2", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-V2o8YS2EhrKMO9AumnkK0ynynfFlsajpYupwq7S46H5mhcanwxDkNTw3K67wPIcKTH1ffkA4vhMOtAc7gRmPZw=="],
+
+ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
+
+ "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001816", "", {}, "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw=="],
+
+ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+ "chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="],
+
+ "chromium-edge-launcher": ["chromium-edge-launcher@0.3.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4" } }, "sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA=="],
+
+ "ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
+
+ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
+
+ "connect": ["connect@3.7.0", "", { "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ=="],
+
+ "content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "core-js-compat": ["core-js-compat@3.50.0", "", { "dependencies": { "browserslist": "^4.28.7" } }, "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
+ "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
+
+ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.456", "", {}, "sha512-8hboqI6xesozteFb8aa7OGknvxUtTeRJLz0FoePysUEh/oWbddykIGdgpEJRbPRg/nIMfBf2DFos+H2MtvJl2A=="],
+
+ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
+ "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
+ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
+
+ "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
+
+ "fb-dotslash": ["fb-dotslash@0.5.8", "", { "bin": { "dotslash": "bin/dotslash" } }, "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA=="],
+
+ "fb-watchman": ["fb-watchman@2.0.4", "", { "dependencies": { "bser": "2.1.2" } }, "sha512-tGQtaoYisPDriJBN0mT5yZSyNETnJGA1gTYxaVRAE5KUO3LI9kPUc9WIu8JXcxaIci+faIKlzXqXyPgrmubtBQ=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+ "finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="],
+
+ "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="],
+
+ "flow-estree": ["flow-estree@0.331.0", "", {}, "sha512-FVLYkSL/ITb/QXBEQvNWPjPooRGswuUtGOwrH+puSlMDneNkxy640+FZsk/TfKL+b7Wbw/Fg9FTUv2bMQDpj2w=="],
+
+ "flow-parser": ["flow-parser@0.331.0", "", { "dependencies": { "flow-estree": "0.331.0" } }, "sha512-vEZcHIlnKeN8JSVtYFczj8sWJfHIJOkz+YG00O/oul/Bete+t1ZhTi+O6JRRLFYvGW+yJkZ0YgL16iStnbkUqQ=="],
+
+ "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
+
+ "hermes-compiler": ["hermes-compiler@250829098.0.17", "", {}, "sha512-qG1PXzTEtriF6oQLZF3vyHhSMxOdW5h2TqqLri0rdpstPustd2fSvRZQMVAPdlhgFwBfYnj3OUZtiO6LjYsEFw=="],
+
+ "hermes-estree": ["hermes-estree@0.36.1", "", {}, "sha512-guv1nQ6IJ7S83NRFPWc3SA7IBZrdNC9kapwOq6uXvF4wP+sDCgjzQbKPCoyYmoyZRzztF/n/c36l/rccCZSiCw=="],
+
+ "hermes-parser": ["hermes-parser@0.36.1", "", { "dependencies": { "hermes-estree": "0.36.1" } }, "sha512-GApNk4zLHi2UWoWZZkx7LNCOSzLSc5lB55pZ/PhK7ycFeg7u5LcF88p/WbpIi1XUDtE0MpHE3uRR3u3KB7TjSQ=="],
+
+ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="],
+
+ "is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="],
+
+ "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
+
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+ "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="],
+
+ "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
+
+ "jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="],
+
+ "jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "jsc-safe-url": ["jsc-safe-url@0.2.4", "", {}, "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
+
+ "lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
+
+ "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
+
+ "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="],
+
+ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+
+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="],
+
+ "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="],
+
+ "memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
+
+ "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
+
+ "metro": ["metro@0.87.1", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "flow-parser": "0.331.0", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.87.1", "metro-cache": "0.87.1", "metro-cache-key": "0.87.1", "metro-config": "0.87.1", "metro-core": "0.87.1", "metro-file-map": "0.87.1", "metro-resolver": "0.87.1", "metro-runtime": "0.87.1", "metro-source-map": "0.87.1", "metro-symbolicate": "0.87.1", "metro-transform-plugins": "0.87.1", "metro-transform-worker": "0.87.1", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-1oyLU9elM7hPAsKIqKooEekwxiuWr27+MZBhUROPhzcbXhMDxK64GPF/hCXwBZVp1F89ODavCrBmJtLx9WtBoQ=="],
+
+ "metro-babel-transformer": ["metro-babel-transformer@0.87.1", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "flow-parser": "0.331.0", "metro-cache-key": "0.87.1", "nullthrows": "^1.1.1" } }, "sha512-orvRIGpb0yK1yMbk5YP3dhVKjmoxUkRB9fdltGJxNppEqus/1tFpNuf9KnGFR3Qjg0izoUrJ4z6WgoBvdkXPrA=="],
+
+ "metro-cache": ["metro-cache@0.87.1", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.87.1" } }, "sha512-juNPaj0Xi5tkLp8imXqvbSnEIijwKG36m7OiFFVJMqEkiURwAjkMFVek3/xpix1+LVQqO7kjVp3PvuvIM61mKQ=="],
+
+ "metro-cache-key": ["metro-cache-key@0.87.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-scqVVPMA2c+RVO12I3wyMDp5xCkRPLsyrcG4Qc3xfU5JR0EW/0sz2tjty+5UjMUtG09uiM1KYQ8HjWg7OQXHtw=="],
+
+ "metro-config": ["metro-config@0.84.6", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.84.6", "metro-cache": "0.84.6", "metro-core": "0.84.6", "metro-runtime": "0.84.6", "yaml": "^2.6.1" } }, "sha512-cD2mLEofcuV26kxSV4hfV1Pbuh6igdUhYQriO8ZVeJ7HyZfq4/wqHV0gSR7fEO8NUp9odr60/Kzm5YakmghWwQ=="],
+
+ "metro-core": ["metro-core@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.84.6" } }, "sha512-ZqYskM8+f3PFMosBDJd8DLqRvT1ltRGhWL/ZfTpBvf4SoadO30VVrdhAoHFVGz3G1CJpWa3fgZH7nnnX/ybllA=="],
+
+ "metro-file-map": ["metro-file-map@0.87.1", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1" } }, "sha512-9sPuNCojC3pS4vj+ZPY7FXmHGpdJeckXuKXQrDAfJvZc60AF8owRFTkI8XdwlwrmPIB2w++KraK9FMEXvC4wJw=="],
+
+ "metro-minify-terser": ["metro-minify-terser@0.87.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-YK4k1oO1wV48hfE+Nbw2rJCnAgeabzrgT/xH5ha5n9gbK4p3HYFC898cERUiyTl9vRh58CNTiXRc88f7y6PfKg=="],
+
+ "metro-resolver": ["metro-resolver@0.87.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-FXH/brY69wT6sPxaSW8BhMmDGD/6irmBr/cL4rUi+VWwqR3pbvdcfUOfyjcHucgQ7pe5IW3xJY21eHKLVKNumQ=="],
+
+ "metro-runtime": ["metro-runtime@0.87.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-kjeuSvInsM6OzBjkCQsWxnkrKZFMKtnkCl38oXWRI9lDGs2L9hGs9Tak9sQYJ9h3gtDujneB7wZio+irgqJKDw=="],
+
+ "metro-source-map": ["metro-source-map@0.87.1", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.87.1", "nullthrows": "^1.1.1", "ob1": "0.87.1", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-Bqpm0PBGdy53pigIJb4HRF7QxD2pUOdgSmVh/4AWbP635kSCv+Y6ltVxUZgSnzS/QbSmzyCGijmRPlXMj2/qHw=="],
+
+ "metro-symbolicate": ["metro-symbolicate@0.87.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.87.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-rcGvwabrg0lbXGRGRKl14PY3My3+dkno2jZYhdJEuH2DmjTtTkV5bYwgto4JtV/Lbe+sXE6GO1UGg654430uow=="],
+
+ "metro-transform-plugins": ["metro-transform-plugins@0.87.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-qMiJ/x+VfqumFJJSefKS6nj8uojzC29b2/aK8vlMuFABIqGjDucICEOXd2Yt6k/XMFAgAcaJvShUOT0HLyYaPw=="],
+
+ "metro-transform-worker": ["metro-transform-worker@0.87.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.87.1", "metro-babel-transformer": "0.87.1", "metro-cache": "0.87.1", "metro-cache-key": "0.87.1", "metro-minify-terser": "0.87.1", "metro-source-map": "0.87.1", "metro-transform-plugins": "0.87.1", "nullthrows": "^1.1.1" } }, "sha512-VOzs3OV405FuOLYdAhqA2wsn1F9lvueDVxMDLvCKaQ44U+yknIqphJ9eMjjsGwLDZiJq9sW3DMLhjD27JxQgqQ=="],
+
+ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+
+ "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
+
+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+
+ "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="],
+
+ "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
+
+ "node-releases": ["node-releases@2.0.58", "", {}, "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg=="],
+
+ "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="],
+
+ "ob1": ["ob1@0.87.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-i8iA8uij0g1YQzS8uOJPSRCgwDjO9warIHUAu1Fqj877Wc3wlfxDYBioYWgKTBF2+URVJttyDWSEpmd99nlvtQ=="],
+
+ "on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
+
+ "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
+
+ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
+
+ "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
+
+ "promise": ["promise@8.3.0", "", { "dependencies": { "asap": "~2.0.6" } }, "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg=="],
+
+ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+ "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
+
+ "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="],
+
+ "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
+
+ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
+
+ "react-native": ["react-native@0.87.1", "", { "dependencies": { "@react-native/asset-utils": "0.87.1", "@react-native/codegen": "0.87.1", "@react-native/community-cli-plugin": "0.87.1", "@react-native/gradle-plugin": "0.87.1", "@react-native/normalize-colors": "0.87.1", "@react-native/virtualized-lists": "0.87.1", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-plugin-syntax-hermes-parser": "0.36.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "hermes-compiler": "250829098.0.17", "invariant": "^2.2.4", "memoize-one": "^5.0.0", "metro-runtime": "^0.87.0", "metro-source-map": "^0.87.0", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "tinyglobby": "^0.2.15", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.1", "react": "^19.2.3" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-DJKG6ANoD7BtrE4z9DewiSD7/RxCX73lK5Pu49aUr85P3333Dm2roTiP0rRjQNDZdVizHSOmstWfwF/o9EjCRA=="],
+
+ "react-native-0.86": ["react-native@0.86.3", "", { "dependencies": { "@react-native/assets-registry": "0.86.3", "@react-native/codegen": "0.86.3", "@react-native/community-cli-plugin": "0.86.3", "@react-native/gradle-plugin": "0.86.3", "@react-native/js-polyfills": "0.86.3", "@react-native/normalize-colors": "0.86.3", "@react-native/virtualized-lists": "0.86.3", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-plugin-syntax-hermes-parser": "0.36.0", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "hermes-compiler": "250829098.0.17", "invariant": "^2.2.4", "memoize-one": "^5.0.0", "metro-runtime": "^0.84.3", "metro-source-map": "^0.84.3", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "tinyglobby": "^0.2.15", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@react-native/jest-preset": "0.86.3", "@types/react": "^19.1.1", "react": "^19.2.3" }, "optionalPeers": ["@react-native/jest-preset", "@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-JR5s3bM9ezud+Mw24GlNXNfthqPIKwrQgPPJcam+L97t2sKjjEavhCzBn+fyqZZRcM5+XlhYxpTxVkK7e1n38Q=="],
+
+ "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="],
+
+ "react-native-nitro-modules": ["react-native-nitro-modules@0.37.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-KpW6EQVQ/bfegpCGxN9Be+ndvsfj56t4IESEvCASu9gWpJa/NDfHpIeIwCOvZQ3eXmLEj6YD4wCLqcC1FJPr2w=="],
+
+ "react-native-reanimated": ["react-native-reanimated@4.7.0", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", "semver": "^7.7.3" }, "peerDependencies": { "react": "*", "react-native": "0.86 - 0.88", "react-native-worklets": "0.13.x" } }, "sha512-8TyTrPisqz7WzmbhCz8F60Uw7NI2QeYWrY5Hk58/kurCA5L5KuHmhyQjxeq5k2YalpXUAKbdkn/mAMZWm/fU0g=="],
+
+ "react-native-reanimated-4.5": ["react-native-reanimated@4.5.1", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", "semver": "^7.7.3" }, "peerDependencies": { "react": "*", "react-native": "0.83 - 0.86", "react-native-worklets": "0.10.x" } }, "sha512-RnMvtDuR+68ig864gAvZCOdZehqhC5rFmMo0kn+ARfgVSTvFeF6IFLBVgMPUu0KwihaapEyW24WRi6nEyy1kSA=="],
+
+ "react-native-unistyles": ["react-native-unistyles@3.5.3", "", { "dependencies": { "@babel/types": "7.29.8" }, "peerDependencies": { "@react-native/normalize-colors": "*", "react": "*", "react-native": ">=0.81.0", "react-native-edge-to-edge": "*", "react-native-nitro-modules": "*", "react-native-reanimated": "*" }, "optionalPeers": ["react-native-edge-to-edge", "react-native-reanimated"] }, "sha512-m4hpfWJS86aR4B4hbuOZFqWYSnD2xtGXmof2JrUSpmsvFv+4SXUjYw8MFKpUiAb/dVUqg8I0pzDvHBUyY1MAUw=="],
+
+ "react-native-worklets": ["react-native-worklets@0.13.0", "", { "dependencies": { "@babel/generator": "^7.27.1", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.28.5", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "semver": "^7.7.4", "whatwg-fetch": "^3.6.20" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.86 - 0.88" } }, "sha512-1OrhAeAKaZF4XrBa4YbPQbKzNOpjh52HDlId8e6ete1OCHNdAlrQHzG4gznJ225EAj3dgQXHViJvK51M9B52jQ=="],
+
+ "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
+
+ "regenerate": ["regenerate@1.5.0", "", {}, "sha512-1s+CxXjJ71i7LNVy8nYwg47XV4y6lAnOEhsMtSKq3WpmPfhl9LuKlTqKIn/3yUpE8lBz+W+TT88Bxyhnr0U4yw=="],
+
+ "regenerate-unicode-properties": ["regenerate-unicode-properties@10.3.0", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-9ns8odR8e9q3otKeHj3DE80jZYKg6zVeZKE+zQmRKXFoVUcuiTGeO6iusDhPRoIQQPVPRvH+SfBhrTud4eczuw=="],
+
+ "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
+
+ "regexpu-core": ["regexpu-core@6.5.3", "", { "dependencies": { "regenerate": "^1.5.0", "regenerate-unicode-properties": "^10.3.0", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-E8VgUoSK+LMQrLboDxavm76MTBGrQhx9RiNuomRmYnZ10Uw12HrI8fkxOOncktb2hBpzIZ9SV/347euQHG5z+g=="],
+
+ "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="],
+
+ "regjsparser": ["regjsparser@0.13.4", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-EWV80YgFaVgFrjGCAsV7oybp4GOxbls5RzBg8OB0nKEVXsBJR1JlV8Di1JhCROg3MzTfe1RI8yitIm1eOs68iA=="],
+
+ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+ "resolve": ["resolve@1.22.13", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.17.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA=="],
+
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
+
+ "serialize-error": ["serialize-error@2.1.0", "", {}, "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw=="],
+
+ "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
+
+ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "shell-quote": ["shell-quote@1.12.0", "", {}, "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q=="],
+
+ "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
+
+ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
+
+ "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
+
+ "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="],
+
+ "statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
+
+ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
+
+ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+ "terser": ["terser@5.51.2", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q=="],
+
+ "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="],
+
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+
+ "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
+ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
+ "tsx": ["tsx@4.23.15", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw=="],
+
+ "type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
+
+ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
+
+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
+ "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="],
+
+ "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="],
+
+ "unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="],
+
+ "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="],
+
+ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.3.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A=="],
+
+ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
+
+ "vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="],
+
+ "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="],
+
+ "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="],
+
+ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="],
+
+ "yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
+
+ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
+ "@react-native/community-cli-plugin/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "@react-native/metro-config/@react-native/js-polyfills": ["@react-native/js-polyfills@0.87.1", "", {}, "sha512-L9yKwEJCq7e8RjhthMUht2CP/CPnBR134h9xHkJ0XOO15QOWBAOqSZltTZG/wiokidqePvK+exMsRinuFLsN9w=="],
+
+ "@react-native/metro-config/metro-config": ["metro-config@0.87.1", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.87.1", "metro-cache": "0.87.1", "metro-core": "0.87.1", "metro-runtime": "0.87.1" } }, "sha512-NmkDlc/qZAdo5gTkVTuO2AKE/khemRKRH5IA+SKfqUBJigI5GqZ5TOXhmEokhlkAqzjt8dyzKhzI6FHvXoXaTQ=="],
+
+ "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
+
+ "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
+
+ "finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
+
+ "http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+ "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
+
+ "jest-util/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+
+ "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
+
+ "metro/metro-config": ["metro-config@0.87.1", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.87.1", "metro-cache": "0.87.1", "metro-core": "0.87.1", "metro-runtime": "0.87.1" } }, "sha512-NmkDlc/qZAdo5gTkVTuO2AKE/khemRKRH5IA+SKfqUBJigI5GqZ5TOXhmEokhlkAqzjt8dyzKhzI6FHvXoXaTQ=="],
+
+ "metro/metro-core": ["metro-core@0.87.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.87.1" } }, "sha512-xizzky/4+c/Mkznege8OW05j7bSdM39VPoEps61Se4+hf32UQujmbvLvIE9+UnP6kNEPVomk0Fw8NsBK9iRQmw=="],
+
+ "metro-cache/metro-core": ["metro-core@0.87.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.87.1" } }, "sha512-xizzky/4+c/Mkznege8OW05j7bSdM39VPoEps61Se4+hf32UQujmbvLvIE9+UnP6kNEPVomk0Fw8NsBK9iRQmw=="],
+
+ "metro-config/metro": ["metro@0.84.6", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.35.0", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.84.6", "metro-cache": "0.84.6", "metro-cache-key": "0.84.6", "metro-config": "0.84.6", "metro-core": "0.84.6", "metro-file-map": "0.84.6", "metro-resolver": "0.84.6", "metro-runtime": "0.84.6", "metro-source-map": "0.84.6", "metro-symbolicate": "0.84.6", "metro-transform-plugins": "0.84.6", "metro-transform-worker": "0.84.6", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-ty/tx/imE5Ph2gvPU788Ckim6RAC2tyZxLz2hWRgC9U7/fGKV7qY/slADy0qHPEw9cpUgc4BfmV3oUsYwm7oeg=="],
+
+ "metro-config/metro-cache": ["metro-cache@0.84.6", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.84.6" } }, "sha512-KBJVpb02oKNO+jlWQ1Aax7cd11YP0MClDaMgx6C4MhyZ9g95J/dC1Bo6sCzXaU9L9h/w5cQfT5zDkGW5sOj99A=="],
+
+ "metro-config/metro-runtime": ["metro-runtime@0.84.6", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-47EYO/DOai0PA3GoAyDpyXOgHNMwbl/Z9dQlQineDLHkpD42xOwMrIx1crT0+n6F+aGoEyxZmeFwfMzaIqVqKQ=="],
+
+ "metro-core/metro-resolver": ["metro-resolver@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-PplpGc/OCLxgKeLNsLQO/VHsYDIvkcGGr3zNT7UanHCGRqhxKv245o+naV8cRI4/pcLqyc+9j+J0AJbT3Kp5Sg=="],
+
+ "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+
+ "react-native/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "react-native-0.86/@react-native/codegen": ["@react-native/codegen@0.86.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.29.0", "hermes-parser": "0.36.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "tinyglobby": "^0.2.15", "yargs": "^17.6.2" } }, "sha512-Ux4jHi0fh+bdtVEcL0gaPLbY56V+SvFUDl/8sRAE1jdb4k+o7fT/4Nc29yz4X+qfjstkSqObQTMBGhdzxH9JvA=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.86.3", "", { "dependencies": { "@react-native/dev-middleware": "0.86.3", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.84.3", "metro-config": "^0.84.3", "metro-core": "^0.84.3", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "0.86.3" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-qSDL9LQc5mZSZPNczT95WU9YQuPzxBklgON9vLhhqfI0yWIwKInqFx88dQ/uiEXBtf0yossthaQIqA4Ml6bF6g=="],
+
+ "react-native-0.86/@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.86.3", "", {}, "sha512-lxmx0GqLEWRIpZfpYFXlYVIs3ENQwaW6Vmp6oi29l2GoQJ1wZfFZRdMimDWlGEk8LKfHar3QH3iaPMkTcK9lEQ=="],
+
+ "react-native-0.86/@react-native/normalize-colors": ["@react-native/normalize-colors@0.86.3", "", {}, "sha512-Cv3CDkprb67GrzuaS9BGbBJC/6G4lIw3nyKOHRKTqTTum4bn37y5+R0Z04L8mcbQN85eEohNrRwb7IOM4j6uvg=="],
+
+ "react-native-0.86/@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.86.3", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "0.86.3" }, "optionalPeers": ["@types/react"] }, "sha512-1j44NEyNn05Ut40vHAmoSWbsIcybFkMAOBTwQt1PrESyfSS+qBoyU1LGIogNva0VIa0rQyEC5PzbA7R4/7Nhyw=="],
+
+ "react-native-0.86/babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.36.0", "", { "dependencies": { "hermes-parser": "0.36.0" } }, "sha512-LhD0xdoedDw7ansQgXbB2DADLZIK/LRXuWNBPuVzMc5S2WK5GyT89tCM+cQzxFGO0mGyLK6D5TrVOJJzAoDy8Q=="],
+
+ "react-native-0.86/metro-runtime": ["metro-runtime@0.84.6", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-47EYO/DOai0PA3GoAyDpyXOgHNMwbl/Z9dQlQineDLHkpD42xOwMrIx1crT0+n6F+aGoEyxZmeFwfMzaIqVqKQ=="],
+
+ "react-native-0.86/metro-source-map": ["metro-source-map@0.84.6", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.84.6", "nullthrows": "^1.1.1", "ob1": "0.84.6", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-4p/EplRbgNxhdqA6kAj1AmIq9qqYXYSAjgxuZ73rYA+p037xDDrxK8CyDKNfbn1k6aMuVt8LV0fpt7NHUTKiZw=="],
+
+ "react-native-0.86/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "react-native-reanimated/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "react-native-reanimated-4.5/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "react-native-worklets/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
+
+ "send/on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
+ "send/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+ "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+
+ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+
+ "tsx/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
+
+ "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "@react-native/metro-config/metro-config/metro-core": ["metro-core@0.87.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.87.1" } }, "sha512-xizzky/4+c/Mkznege8OW05j7bSdM39VPoEps61Se4+hf32UQujmbvLvIE9+UnP6kNEPVomk0Fw8NsBK9iRQmw=="],
+
+ "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
+
+ "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
+
+ "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
+
+ "metro-config/metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="],
+
+ "metro-config/metro/metro-babel-transformer": ["metro-babel-transformer@0.84.6", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", "metro-cache-key": "0.84.6", "nullthrows": "^1.1.1" } }, "sha512-B1ozl6KxFHbQjXZ2U8WiTPWXylqiD3V4EXyV5r0fqETOrqIrJlBvbVTD82UxIN+B3Vpe4l8G1tb15ocd2RyOQA=="],
+
+ "metro-config/metro/metro-cache-key": ["metro-cache-key@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-6tyXt1BZ/3U183XV48oif7Nm65TE+pTo0Vs31u4FxMgqiYQR/SiSr3RoXn3pmy4pd280ZmLmct0x3jZpu0pV4A=="],
+
+ "metro-config/metro/metro-file-map": ["metro-file-map@0.84.6", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-ov9VywWBsHPt54Zc7/DxWvqjTwxuZvhfeyHg8ZAMGKtf2PcH1wS3EnoBH7In53Rspk/eXVWAO5+avxunPn081w=="],
+
+ "metro-config/metro/metro-resolver": ["metro-resolver@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-PplpGc/OCLxgKeLNsLQO/VHsYDIvkcGGr3zNT7UanHCGRqhxKv245o+naV8cRI4/pcLqyc+9j+J0AJbT3Kp5Sg=="],
+
+ "metro-config/metro/metro-source-map": ["metro-source-map@0.84.6", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.84.6", "nullthrows": "^1.1.1", "ob1": "0.84.6", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-4p/EplRbgNxhdqA6kAj1AmIq9qqYXYSAjgxuZ73rYA+p037xDDrxK8CyDKNfbn1k6aMuVt8LV0fpt7NHUTKiZw=="],
+
+ "metro-config/metro/metro-symbolicate": ["metro-symbolicate@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.84.6", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wJFyyF5ysbVyoYVWGL6GwVMiNnGllwDU2gF096376fl0fc8IFUlADSyNlswj31K9og2sOilXe/FdyWFPczPUYg=="],
+
+ "metro-config/metro/metro-transform-plugins": ["metro-transform-plugins@0.84.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-pROPMFaj25Y9+3LlLqpBRz/7B/2YPnfEQz/z3juYxkh3FGQ+3c/Qh4E+khPSTdwTw0M6LitLYHMqaHMbQmuBJw=="],
+
+ "metro-config/metro/metro-transform-worker": ["metro-transform-worker@0.84.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.84.6", "metro-babel-transformer": "0.84.6", "metro-cache": "0.84.6", "metro-cache-key": "0.84.6", "metro-minify-terser": "0.84.6", "metro-source-map": "0.84.6", "metro-transform-plugins": "0.84.6", "nullthrows": "^1.1.1" } }, "sha512-xLTVrOENaHR9DiuhnHS+zcle0rKsCy2CpXSIx7xD+zhJs+qVRNUyV7Z1zIt/0P/d0cSGVx846R+dIjHEudhcww=="],
+
+ "react-native-0.86/@react-native/codegen/hermes-parser": ["hermes-parser@0.36.0", "", { "dependencies": { "hermes-estree": "0.36.0" } }, "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.86.3", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.86.3", "@react-native/debugger-shell": "0.86.3", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.3.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-LiEPTqTg/63bYUnrPyHLfjTDCNhA/+CUqI1+DsA9tYyewtSbULd5awsva6SgE10I+2iMhgKXS3ymkhU/kSCrGA=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro": ["metro@0.84.6", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.35.0", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.84.6", "metro-cache": "0.84.6", "metro-cache-key": "0.84.6", "metro-config": "0.84.6", "metro-core": "0.84.6", "metro-file-map": "0.84.6", "metro-resolver": "0.84.6", "metro-runtime": "0.84.6", "metro-source-map": "0.84.6", "metro-symbolicate": "0.84.6", "metro-transform-plugins": "0.84.6", "metro-transform-worker": "0.84.6", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-ty/tx/imE5Ph2gvPU788Ckim6RAC2tyZxLz2hWRgC9U7/fGKV7qY/slADy0qHPEw9cpUgc4BfmV3oUsYwm7oeg=="],
+
+ "react-native-0.86/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.36.0", "", { "dependencies": { "hermes-estree": "0.36.0" } }, "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w=="],
+
+ "react-native-0.86/metro-source-map/metro-symbolicate": ["metro-symbolicate@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.84.6", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wJFyyF5ysbVyoYVWGL6GwVMiNnGllwDU2gF096376fl0fc8IFUlADSyNlswj31K9og2sOilXe/FdyWFPczPUYg=="],
+
+ "react-native-0.86/metro-source-map/ob1": ["ob1@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-+s+6zjd0X68hfAcn92NsEPXnSYOF3O7exw/Fj4UfRTB+UL+Tdjdkzu5/4WquXlzqgvNm8FWuuW6/0Yd8S2rYxw=="],
+
+ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
+
+ "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
+
+ "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
+
+ "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
+
+ "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
+
+ "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
+
+ "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
+
+ "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
+
+ "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
+
+ "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
+
+ "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
+
+ "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
+
+ "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
+
+ "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
+
+ "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
+
+ "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
+
+ "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
+
+ "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
+
+ "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
+
+ "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
+
+ "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
+
+ "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
+
+ "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
+
+ "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
+
+ "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
+
+ "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
+
+ "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
+
+ "metro-config/metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="],
+
+ "metro-config/metro/metro-source-map/ob1": ["ob1@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-+s+6zjd0X68hfAcn92NsEPXnSYOF3O7exw/Fj4UfRTB+UL+Tdjdkzu5/4WquXlzqgvNm8FWuuW6/0Yd8S2rYxw=="],
+
+ "metro-config/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-SqhB/Kxrw0XfyW0k8mEscqX/CQopm6Fp3EpdSIPzvKfGgQp9bTMahg9PAdVsXUmIWwErbmFw1VqB+VFBOKksUw=="],
+
+ "react-native-0.86/@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.36.0", "", {}, "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.86.3", "", {}, "sha512-TQmeofQ0PcuylhhlleOeuzHYZfbrgm3gayXzowqUEzgRisTm1D40/J3ggqs7XkQi5HP5ZA3n8dHmKL9vIzPcsw=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-shell": ["@react-native/debugger-shell@0.86.3", "", { "dependencies": { "cross-spawn": "^7.0.6", "debug": "^4.4.0", "fb-dotslash": "0.5.8" } }, "sha512-O4ds+J7xZfxkbih9T+cAGegBdvKSPKYJm/lDgC9CpEjFMkmzWTpVLU3Qsv9sqZuo58z+sGhIcJfPsCFFWHpqbQ=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/metro-babel-transformer": ["metro-babel-transformer@0.84.6", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", "metro-cache-key": "0.84.6", "nullthrows": "^1.1.1" } }, "sha512-B1ozl6KxFHbQjXZ2U8WiTPWXylqiD3V4EXyV5r0fqETOrqIrJlBvbVTD82UxIN+B3Vpe4l8G1tb15ocd2RyOQA=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/metro-cache": ["metro-cache@0.84.6", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.84.6" } }, "sha512-KBJVpb02oKNO+jlWQ1Aax7cd11YP0MClDaMgx6C4MhyZ9g95J/dC1Bo6sCzXaU9L9h/w5cQfT5zDkGW5sOj99A=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/metro-cache-key": ["metro-cache-key@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-6tyXt1BZ/3U183XV48oif7Nm65TE+pTo0Vs31u4FxMgqiYQR/SiSr3RoXn3pmy4pd280ZmLmct0x3jZpu0pV4A=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/metro-file-map": ["metro-file-map@0.84.6", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-ov9VywWBsHPt54Zc7/DxWvqjTwxuZvhfeyHg8ZAMGKtf2PcH1wS3EnoBH7In53Rspk/eXVWAO5+avxunPn081w=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/metro-resolver": ["metro-resolver@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-PplpGc/OCLxgKeLNsLQO/VHsYDIvkcGGr3zNT7UanHCGRqhxKv245o+naV8cRI4/pcLqyc+9j+J0AJbT3Kp5Sg=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/metro-symbolicate": ["metro-symbolicate@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.84.6", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wJFyyF5ysbVyoYVWGL6GwVMiNnGllwDU2gF096376fl0fc8IFUlADSyNlswj31K9og2sOilXe/FdyWFPczPUYg=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/metro-transform-plugins": ["metro-transform-plugins@0.84.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-pROPMFaj25Y9+3LlLqpBRz/7B/2YPnfEQz/z3juYxkh3FGQ+3c/Qh4E+khPSTdwTw0M6LitLYHMqaHMbQmuBJw=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/metro-transform-worker": ["metro-transform-worker@0.84.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.84.6", "metro-babel-transformer": "0.84.6", "metro-cache": "0.84.6", "metro-cache-key": "0.84.6", "metro-minify-terser": "0.84.6", "metro-source-map": "0.84.6", "metro-transform-plugins": "0.84.6", "nullthrows": "^1.1.1" } }, "sha512-xLTVrOENaHR9DiuhnHS+zcle0rKsCy2CpXSIx7xD+zhJs+qVRNUyV7Z1zIt/0P/d0cSGVx846R+dIjHEudhcww=="],
+
+ "react-native-0.86/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.36.0", "", {}, "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="],
+
+ "react-native-0.86/@react-native/community-cli-plugin/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-SqhB/Kxrw0XfyW0k8mEscqX/CQopm6Fp3EpdSIPzvKfGgQp9bTMahg9PAdVsXUmIWwErbmFw1VqB+VFBOKksUw=="],
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ebe602a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "unifiber",
+ "version": "0.1.0",
+ "description": "Typed, fail-safe access to React Native fiber internals for styling libraries (Unistyles, Uniwind), plus adapters for 3rd-party owners of host views (Reanimated).",
+ "license": "MIT",
+ "private": true,
+ "type": "module",
+ "main": "src/index.ts",
+ "react-native": "src/index.ts",
+ "types": "src/index.ts",
+ "sideEffects": false,
+ "files": [
+ "src",
+ "LICENSE",
+ "README.md"
+ ],
+ "scripts": {
+ "typecheck": "tsc --noEmit",
+ "test": "node --import tsx --test 'test/**/*.test.ts'",
+ "test:prod": "UNIFIBER_MODE=production npm test",
+ "test:rn086": "UNIFIBER_RN=0.86 npm test",
+ "test:all": "npm test && npm run test:prod && npm run test:rn086 && UNIFIBER_MODE=production UNIFIBER_RN=0.86 npm test",
+ "coverage": "node --import tsx --test --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-lines=95 --test-coverage-branches=95 --test-coverage-functions=95 'test/**/*.test.ts'",
+ "check": "npm run typecheck && npm run test:all && npm run coverage",
+ "bench:hermes": "node bench/run-hermes.mjs",
+ "bench:metro": "node bench/metro.mjs"
+ },
+ "peerDependencies": {
+ "react": ">=19.0.0",
+ "react-native": ">=0.81.0"
+ },
+ "devDependencies": {
+ "@babel/core": "7.29.7",
+ "@babel/runtime": "7.29.10",
+ "@react-native/babel-preset": "0.87.1",
+ "@types/node": "22.20.5",
+ "@types/react": "19.2.18",
+ "esbuild": "0.27.3",
+ "react": "19.2.3",
+ "react-freeze": "1.0.4",
+ "react-native": "0.87.1",
+ "react-native-0.86": "npm:react-native@0.86.3",
+ "react-native-reanimated": "4.7.0",
+ "react-native-reanimated-4.5": "npm:react-native-reanimated@4.5.1",
+ "react-native-unistyles": "3.5.3",
+ "scheduler": "0.27.0",
+ "tsx": "4.23.15",
+ "typescript": "6.0.3"
+ },
+ "engines": {
+ "node": ">=22.18.0"
+ }
+}
diff --git a/src/adapters/reanimated.ts b/src/adapters/reanimated.ts
new file mode 100644
index 0000000..b09e655
--- /dev/null
+++ b/src/adapters/reanimated.ts
@@ -0,0 +1,170 @@
+// Keeps Reanimated's CSS style copy (`_cssStyle`) in sync with native-only style updates.
+// Private fields are pinned by test/reanimated-contract.test.ts for 4.5 and 4.7.
+import type { Host } from '../host'
+import type { OwnerAdapter, StyleSource } from './registry'
+import * as fiber from '../fiber'
+
+// Bound once: Metro's inline requires
+const { contextReaderFor } = fiber
+const CLASS_COMPONENT = fiber.Tag.ClassComponent
+
+type CSSManagerLike = { update(style: Record, props?: Record): void }
+
+type AnimatedComponentLike = {
+ props: Record
+ _cssStyle: Record
+ _CSSManager?: CSSManagerLike
+ _updateStyles(props: Record): void
+ shouldComponentUpdate(nextProps: Record, ...rest: Array): boolean
+ [STATE]?: AttachedState
+}
+
+type AttachedState = {
+ source: StyleSource
+ original: (props: Record) => void
+ originalShouldUpdate: AnimatedComponentLike['shouldComponentUpdate']
+ rendering: boolean
+ styleProp: unknown
+ epoch: number
+ contextKeys: Array
+ fresh: Record | undefined
+}
+
+// Symbol.for: two copies of unifiber in one bundle never wrap a component twice
+const STATE = Symbol.for('unifiber.reanimated.css')
+
+const hasCSSConfig = (cssStyle: Record) => {
+ for (const key in cssStyle) {
+ if (key.startsWith('transition') || key.startsWith('animation')) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// Deepest case: ScrollView with refreshControl on Android, 6-7 fibers
+const MAX_OWNER_DEPTH = 12
+
+// The class whose `_componentRef` is this host's public instance. Lists don't match: their ref isn't a host.
+export const animatedComponentOf = (host: Host | null | undefined): AnimatedComponentLike | null => {
+ const fiber = host as any
+
+ if (fiber?.memoizedProps?.collapsable !== false) {
+ return null
+ }
+
+ const publicInstance = fiber.stateNode?.canonical?.publicInstance
+
+ if (!publicInstance) {
+ return null
+ }
+
+ let current = fiber.return
+
+ for (let depth = 0; current && depth < MAX_OWNER_DEPTH; depth++) {
+ if (current.tag === CLASS_COMPONENT && current.stateNode?._componentRef === publicInstance) {
+ return isAnimatedComponent(current.stateNode) ? current.stateNode : null
+ }
+
+ current = current.return
+ }
+
+ return null
+}
+
+const isAnimatedComponent = (owner: any): owner is AnimatedComponentLike =>
+ typeof owner?._updateStyles === 'function' &&
+ typeof owner.shouldComponentUpdate === 'function' &&
+ owner._cssStyle !== null &&
+ typeof owner._cssStyle === 'object'
+
+function updateStylesThroughSource(this: AnimatedComponentLike, props: Record) {
+ const state = this[STATE]!
+
+ state.original.call(this, props)
+
+ if (!hasCSSConfig(this._cssStyle)) {
+ return
+ }
+
+ const epoch = state.source.epoch()
+ const read = contextReaderFor(this, state.rendering)
+ const contexts = state.source.contexts ?? []
+ let contextChanged = false
+
+ for (let i = 0; i < contexts.length; i++) {
+ const value = read(contexts[i])
+ const key = state.source.contextKey ? state.source.contextKey(contexts[i], value) : value
+
+ if (state.contextKeys[i] !== key) {
+ state.contextKeys[i] = key
+ contextChanged = true
+ }
+ }
+
+ if (contextChanged || state.styleProp !== props.style || state.epoch !== epoch) {
+ state.fresh = state.source.resolve(props, read)
+ state.styleProp = props.style
+ state.epoch = epoch
+ }
+
+ const fresh = state.fresh
+
+ if (!fresh) {
+ return
+ }
+
+ // only the keys Reanimated kept: animated styles were split out of _cssStyle
+ for (const key in this._cssStyle) {
+ if (key in fresh) {
+ this._cssStyle[key] = fresh[key]
+ }
+ }
+}
+
+// Marks the render phase, where context must be read from React's stack
+function shouldUpdateMarkingRender(this: AnimatedComponentLike, ...args: [Record, ...Array]) {
+ const state = this[STATE]!
+
+ state.rendering = true
+
+ try {
+ return state.originalShouldUpdate.apply(this, args)
+ } finally {
+ state.rendering = false
+ }
+}
+
+export const reanimatedCSSAdapter: OwnerAdapter = {
+ name: 'react-native-reanimated/css',
+ gate: (host: Host) => (host as unknown as fiber.RawFiber).memoizedProps?.collapsable === false,
+ claim: animatedComponentOf,
+ attach: (owner, source) => {
+ if (owner[STATE]) {
+ return
+ }
+
+ owner[STATE] = {
+ source,
+ original: owner._updateStyles,
+ originalShouldUpdate: owner.shouldComponentUpdate,
+ rendering: false,
+ styleProp: undefined,
+ epoch: -1,
+ contextKeys: [],
+ fresh: undefined,
+ }
+ owner._updateStyles = updateStylesThroughSource
+ owner.shouldComponentUpdate = shouldUpdateMarkingRender
+ },
+ refresh: (owner) => {
+ // not mounted yet
+ if (typeof owner._CSSManager?.update !== 'function') {
+ return
+ }
+
+ owner._updateStyles(owner.props)
+ owner._CSSManager.update(owner._cssStyle, owner.props)
+ },
+}
diff --git a/src/adapters/registry.ts b/src/adapters/registry.ts
new file mode 100644
index 0000000..23fd95f
--- /dev/null
+++ b/src/adapters/registry.ts
@@ -0,0 +1,105 @@
+import type { Context } from 'react'
+import type { ContextReader } from '../fiber'
+import type { Host } from '../host'
+import * as host from '../host'
+
+// Bound once: Metro's inline requires
+const { isHidden } = host
+
+export interface StyleSource {
+ readonly name: string
+ // bumps on every runtime change (theme, breakpoint, ...)
+ epoch(): number
+ readonly contexts?: ReadonlyArray>
+ // cache key for a context value; default: the value itself. Stable scopes renamed in place need `scope?.name`
+ contextKey?(context: Context, value: unknown): unknown
+ // undefined: none of these styles belong to this source
+ resolve(props: Record, read: ContextReader): Record | undefined
+}
+
+export interface OwnerAdapter {
+ readonly name: string
+ // runs on every link: keep it to a property read or two
+ gate(host: Host): boolean
+ claim(host: Host): Owner | null
+ attach(owner: Owner, source: StyleSource): void
+ refresh(owner: Owner, source: StyleSource): void
+}
+
+export type OwnerRegistry = {
+ link(host: Host | null | undefined): void
+ refresh(hosts: ReadonlyArray): void
+}
+
+// An adapter that throws is disabled for the session and reported once
+export const createOwnerRegistry = (
+ source: StyleSource,
+ adapters: ReadonlyArray>,
+ onAdapterError?: (reason: string) => void,
+): OwnerRegistry => {
+ const disabledAdapters = new Set()
+ const disable = (adapter: OwnerAdapter, error: unknown) => {
+ disabledAdapters.add(adapter)
+ onAdapterError?.(`adapter ${adapter.name} failed: ${String(error)}`)
+ }
+ // Not inline closures: a closure in link() makes Hermes allocate its environment on every call
+ const attachOwner = (adapter: OwnerAdapter, host: Host) => {
+ try {
+ const owner = adapter.claim(host)
+
+ if (owner) {
+ adapter.attach(owner, source)
+ }
+ } catch (error) {
+ disable(adapter, error)
+ }
+ }
+ const refreshOwner = (adapter: OwnerAdapter, host: Host, seen: Set) => {
+ try {
+ const owner = adapter.claim(host)
+
+ if (owner && !seen.has(owner)) {
+ seen.add(owner)
+ adapter.attach(owner, source)
+ adapter.refresh(owner, source)
+ }
+ } catch (error) {
+ disable(adapter, error)
+ }
+ }
+
+ return {
+ link(host) {
+ if (!host) {
+ return
+ }
+
+ for (let i = 0; i < adapters.length; i++) {
+ const adapter = adapters[i]
+
+ if (adapter.gate(host) && !disabledAdapters.has(adapter)) {
+ attachOwner(adapter, host)
+ }
+ }
+ },
+ refresh(hosts) {
+ const seen = new Set()
+
+ for (let h = 0; h < hosts.length; h++) {
+ const host = hosts[h]
+
+ if (!host || isHidden(host)) {
+ continue
+ }
+
+ for (let i = 0; i < adapters.length; i++) {
+ const adapter = adapters[i]
+
+ if (adapter.gate(host) && !disabledAdapters.has(adapter)) {
+ refreshOwner(adapter, host, seen)
+ }
+ }
+ }
+ },
+ }
+}
diff --git a/src/diagnostics.ts b/src/diagnostics.ts
new file mode 100644
index 0000000..8e7ddaa
--- /dev/null
+++ b/src/diagnostics.ts
@@ -0,0 +1,55 @@
+import type { RawFiber } from './fiber'
+import type { Host } from './host'
+import * as host from './host'
+
+// Bound once: Metro's inline requires
+const { committedHalf, fiberOf } = host
+
+const styleDiffers = (a: RawFiber, b: RawFiber) => a.memoizedProps?.style !== b.memoizedProps?.style
+
+const flattenInto = (style: unknown, out: Record) => {
+ if (Array.isArray(style)) {
+ for (let i = 0; i < style.length; i++) {
+ flattenInto(style[i], out)
+ }
+ } else if (style && typeof style === 'object') {
+ Object.assign(out, style)
+ }
+
+ return out
+}
+
+export type Drift = { key: string; react: unknown; expected: unknown }
+
+// Keys that differ from React's committed props: React re-sends them on the view's next render
+export const verify = (host: Host, expected: Record): Array => {
+ const baseline = committedHalf(fiberOf(host), styleDiffers) ?? fiberOf(host)
+ const react = flattenInto(baseline.memoizedProps?.style, {})
+ const drift: Array = []
+
+ for (const key in expected) {
+ if (react[key] !== expected[key]) {
+ drift.push({ key, react: react[key], expected: expected[key] })
+ }
+ }
+
+ return drift
+}
+
+// Development builds only
+export const getOwners = (host: Host, max = 8): Array => {
+ const owners: Array = []
+
+ for (let owner = fiberOf(host)._debugOwner; owner && owners.length < max; owner = owner._debugOwner ?? owner.owner) {
+ const name = owner.name ?? owner.type?.displayName ?? owner.type?.name ?? owner.type?.render?.name
+
+ if (name) {
+ owners.push(name)
+ }
+ }
+
+ return owners
+}
+
+// Development builds only
+export const getCreationStack = (host: Host): string | undefined => fiberOf(host)._debugStack?.stack
diff --git a/src/fiber.ts b/src/fiber.ts
new file mode 100644
index 0000000..499b41b
--- /dev/null
+++ b/src/fiber.ts
@@ -0,0 +1,262 @@
+// The only module that reads React's fiber fields. A missing field gives undefined / null for that call, never a throw.
+import type { Context } from 'react'
+
+// ReactWorkTags, React 19
+export const Tag = {
+ ClassComponent: 1,
+ HostRoot: 3,
+ HostComponent: 5,
+ ContextProvider: 10,
+ Suspense: 13,
+ Offscreen: 22,
+ Activity: 31,
+} as const
+
+declare const brand: unique symbol
+export type FiberNode = { readonly [brand]: 'FiberNode' }
+export type ShadowNode = { readonly __hostObjectShadowNodeWrapper?: unknown }
+
+export type RawFiber = {
+ tag: number
+ type: any
+ return: RawFiber | null
+ child: RawFiber | null
+ sibling: RawFiber | null
+ alternate: RawFiber | null
+ stateNode: any
+ memoizedProps: any
+ pendingProps: any
+ memoizedState: any
+ _debugOwner?: any
+ _debugStack?: { stack?: string } | null
+}
+
+export const raw = (fiber: FiberNode | null | undefined) => fiber as unknown as RawFiber | null | undefined
+
+export const hostFiberOf = (instance: unknown): FiberNode | null =>
+ ((instance as any)?.__internalInstanceHandle ?? null) as FiberNode | null
+
+// RN composites that keep their host behind a ref of their own, most common first
+const compositeHostRefs: Array<(handle: any) => unknown> = [
+ (handle) => handle?.getScrollResponder?.()?.getNativeScrollRef?.(), // FlatList, SectionList, VirtualizedList
+ (handle) => handle?.getNativeScrollRef?.(), // ScrollView
+ (handle) => handle?._viewRef, // ImageBackground
+ (handle) => handle?.viewRef?.current, // KeyboardAvoidingView
+ (handle) => handle?._nativeRef, // RefreshControl
+]
+
+export const resolveHostFiber = (handle: unknown): FiberNode | null => {
+ const direct = hostFiberOf(handle)
+
+ if (direct || !handle) {
+ return direct
+ }
+
+ for (let i = 0; i < compositeHostRefs.length; i++) {
+ const fiber = hostFiberOf(compositeHostRefs[i](handle))
+
+ if (fiber) {
+ return fiber
+ }
+ }
+
+ return null
+}
+
+export const fiberOfInstance = (instance: unknown): FiberNode | null =>
+ ((instance as any)?._reactInternals ?? null) as FiberNode | null
+
+export const firstHostBelow = (fiber: FiberNode | null): FiberNode | null => {
+ let current = raw(fiber)
+
+ while (current && current.tag !== Tag.HostComponent) {
+ current = current.child
+ }
+
+ return (current ?? null) as FiberNode | null
+}
+
+export const hostAncestor = (fiber: FiberNode | null, levels = 1): FiberNode | null => {
+ let current = raw(fiber)?.return
+
+ while (current) {
+ if (current.tag === Tag.HostComponent && --levels === 0) {
+ return current as unknown as FiberNode
+ }
+
+ current = current.return
+ }
+
+ return null
+}
+
+export const shadowNodesOf = (fiber: FiberNode | null, out: Array) => {
+ const current = raw(fiber)
+ const node = current?.stateNode?.node
+ const alternateNode = current?.alternate?.stateNode?.node
+
+ if (node) {
+ out.push(node)
+ }
+
+ if (alternateNode && alternateNode !== node) {
+ out.push(alternateNode)
+ }
+}
+
+const childOfParent = (parent: RawFiber, a: RawFiber, b: RawFiber): RawFiber | null => {
+ for (let child = parent.child; child; child = child.sibling) {
+ if (child === a || child === b) {
+ return child
+ }
+ }
+
+ return null
+}
+
+// React 19.2 findCurrentFiberUsingSlowPath, returning null where React throws
+export const committedOf = (fiber: FiberNode | null): FiberNode | null => {
+ const self = raw(fiber)
+
+ if (!self) {
+ return null
+ }
+
+ const alternate = self.alternate
+
+ if (!alternate) {
+ let root = self
+
+ while (root.return) {
+ root = root.return
+ }
+
+ return root.tag === Tag.HostRoot ? fiber : null
+ }
+
+ let a: RawFiber = self
+ let b: RawFiber = alternate
+
+ while (true) {
+ const parentA = a.return
+
+ if (!parentA) {
+ break
+ }
+
+ const parentB = parentA.alternate
+
+ if (!parentB) {
+ const next = parentA.return
+
+ if (next) {
+ a = next
+ continue
+ }
+
+ break
+ }
+
+ if (parentA.child === parentB.child) {
+ const found = childOfParent(parentA, a, b)
+
+ return found === a ? fiber : found === b ? (alternate as unknown as FiberNode) : null
+ }
+
+ if (a.return !== b.return) {
+ a = parentA
+ b = parentB
+ } else {
+ const inA = childOfParent(parentA, a, b)
+ const inB = inA ? null : childOfParent(parentB, a, b)
+
+ if (!inA && !inB) {
+ return null
+ }
+
+ const aUnderA = inA ? inA === a : inB !== a
+
+ a = aUnderA ? parentA : parentB
+ b = aUnderA ? parentB : parentA
+ }
+
+ if (a.alternate !== b) {
+ return null
+ }
+ }
+
+ if (a.tag !== Tag.HostRoot) {
+ return null
+ }
+
+ return a.stateNode?.current === a ? fiber : (alternate as unknown as FiberNode)
+}
+
+// a provider's other half can hold its previous value
+const committedProviderValue = (provider: RawFiber): unknown => {
+ const alternate = provider.alternate
+
+ if (!alternate || alternate.memoizedProps?.value === provider.memoizedProps?.value) {
+ return provider.memoizedProps?.value
+ }
+
+ return (raw(committedOf(provider as unknown as FiberNode)) ?? provider).memoizedProps?.value
+}
+
+export const readContext = (fiber: FiberNode | null, context: Context, fallback?: T): T | undefined => {
+ for (let current = raw(fiber)?.return; current; current = current.return) {
+ if (current.tag === Tag.ContextProvider && (current.type === context || current.type?._context === context)) {
+ return committedProviderValue(current) as T
+ }
+ }
+
+ return fallback
+}
+
+// Fabric is a secondary renderer: its context stack is `_currentValue2`. Only valid during render.
+export const readContextDuringRender = (context: Context): T | undefined =>
+ (context as any)?._currentValue2
+
+export type ContextReader = (context: Context) => T | undefined
+
+export const contextReaderFor = (instance: unknown, rendering: boolean): ContextReader =>
+ rendering ? readContextDuringRender : (context) => readCommittedContext(instance, context)
+
+const NO_PROVIDER = Symbol('no provider')
+
+const readCommittedContext = (instance: unknown, context: Context): T | undefined => {
+ const value = readContext(committedOf(fiberOfInstance(instance)), context, NO_PROVIDER as T)
+
+ return value === NO_PROVIDER ? readContextDuringRender(context) : value
+}
+
+// pendingProps too, so React still bails out. Replaced, not mutated: props are frozen in dev.
+export const patchRenderedProps = (
+ fiber: FiberNode | null,
+ patch: (props: Record) => Record | undefined,
+): boolean => {
+ const self = raw(fiber)
+
+ if (!self) {
+ return false
+ }
+
+ const patchedSelf = replaceProps(self, patch)
+ const patchedAlternate = self.alternate ? replaceProps(self.alternate, patch) : false
+
+ return patchedSelf || patchedAlternate
+}
+
+const replaceProps = (target: RawFiber, patch: (props: Record) => Record | undefined) => {
+ const props = target.memoizedProps
+ const next = props ? patch(props) : undefined
+
+ if (!next) {
+ return false
+ }
+
+ target.memoizedProps = next
+ target.pendingProps = next
+
+ return true
+}
diff --git a/src/host.ts b/src/host.ts
new file mode 100644
index 0000000..4f1f283
--- /dev/null
+++ b/src/host.ts
@@ -0,0 +1,214 @@
+// A Host is the view's creation fiber behind an opaque type: no allocation, and stable identity (=== / WeakMap keys).
+import type { FiberNode, RawFiber, ShadowNode } from './fiber'
+import * as fiber from './fiber'
+
+// Bound once: Metro's inline requires would turn each imported name used in a function into a require per call
+const {
+ committedOf,
+ fiberOfInstance,
+ firstHostBelow,
+ hostAncestor,
+ patchRenderedProps: patchFiberProps,
+ raw,
+ resolveHostFiber,
+ shadowNodesOf,
+} = fiber
+const { HostComponent: HOST_COMPONENT, Offscreen: OFFSCREEN, Suspense: SUSPENSE, Activity: ACTIVITY } = fiber.Tag
+
+declare const brand: unique symbol
+export type Host = { readonly [brand]: 'Host' }
+
+export const fiberOf = (host: Host): RawFiber => host as unknown as RawFiber
+
+export const toHost = (fiber: RawFiber | FiberNode | null | undefined): Host | undefined =>
+ ((fiber as RawFiber | null | undefined)?.stateNode?.canonical?.internalInstanceHandle ?? undefined) as Host | undefined
+
+const resolveRefSlowly = (ref: unknown): RawFiber | undefined =>
+ raw(resolveHostFiber(ref)) ?? (toHost(firstHostBelow(fiberOfInstance(ref))) as unknown as RawFiber | undefined)
+
+// The fast path is inlined: legacy Hermes doesn't inline calls
+export const getHostFor = (ref: unknown): Host | undefined => {
+ const fiber = (ref as { __internalInstanceHandle?: RawFiber } | null | undefined)?.__internalInstanceHandle
+
+ if (fiber !== undefined) {
+ return fiber as unknown as Host
+ }
+
+ return resolveRefSlowly(ref) as unknown as Host | undefined
+}
+
+export const getShadowNodeFor = (ref: unknown): ShadowNode | undefined => {
+ const fiber = (ref as { __internalInstanceHandle?: RawFiber } | null | undefined)?.__internalInstanceHandle
+
+ if (fiber !== undefined) {
+ return fiber.stateNode?.node
+ }
+
+ return resolveRefSlowly(ref)?.stateNode?.node
+}
+
+export const getHostForInstanceHandle = (handle: unknown): Host | undefined => toHost(handle as RawFiber | null | undefined)
+
+// Not the first host below the scroll view: on iOS a refreshControl comes first
+export const getContentContainerFor = (ref: any): Host | undefined => {
+ const scroll = ref?.getInnerViewRef ? ref : (ref?.getNativeScrollRef?.() ?? ref?.getScrollResponder?.())
+
+ return getHostFor(scroll?.getInnerViewRef?.())
+}
+
+export const getShadowNode = (host: Host): ShadowNode | undefined => fiberOf(host).stateNode?.node
+
+// Two nodes only while a render is in flight; after a commit both halves share one
+export const getShadowNodes = (host: Host, out: Array = []): Array => {
+ shadowNodesOf(host as unknown as FiberNode, out)
+
+ return out
+}
+
+export const getNativeTag = (host: Host): number | undefined => fiberOf(host).stateNode?.canonical?.nativeTag
+
+export const getParentHost = (host: Host): Host | undefined => toHost(hostAncestor(host as unknown as FiberNode))
+
+export const patchRenderedProps = (host: Host, patch: (props: Record) => Record | undefined): boolean =>
+ patchFiberProps(host as unknown as FiberNode, patch)
+
+export const HiddenBy = {
+ None: 0,
+ Suspense: 1,
+ Freeze: 2,
+ Activity: 4,
+} as const
+
+// `return` can point into either half of a pair; ask which half is committed only when they disagree
+export const committedHalf = (fiber: RawFiber, differs: (a: RawFiber, b: RawFiber) => boolean): RawFiber | null => {
+ const alternate = fiber.alternate
+
+ if (!alternate || !differs(fiber, alternate)) {
+ return fiber
+ }
+
+ return raw(committedOf(fiber as unknown as FiberNode)) ?? null
+}
+
+const hiddenStateDiffers = (a: RawFiber, b: RawFiber) => (a.memoizedState === null) !== (b.memoizedState === null)
+const freezeDiffers = (a: RawFiber, b: RawFiber) => a.memoizedProps?.freeze !== b.memoizedProps?.freeze
+
+// react-freeze: Freeze > Suspense > Suspender. The Suspender keeps stale props because it throws, so read Freeze's.
+const isFrozenBoundary = (suspense: RawFiber) =>
+ committedHalf(suspense.return as RawFiber, freezeDiffers)?.memoizedProps?.freeze === true
+
+// Every hidden subtree sits under an Offscreen fiber with memoizedState set
+export const getHiddenReasons = (host: Host): number => {
+ let reasons = 0
+
+ for (let current = fiberOf(host).return; current; current = current.return) {
+ if (current.tag !== OFFSCREEN || committedHalf(current, hiddenStateDiffers)?.memoizedState == null) {
+ continue
+ }
+
+ const boundary = current.return
+
+ if (boundary?.tag === ACTIVITY) {
+ reasons |= HiddenBy.Activity
+ } else if (boundary?.tag === SUSPENSE) {
+ reasons |= isFrozenBoundary(boundary) ? HiddenBy.Freeze : HiddenBy.Suspense
+ }
+ }
+
+ return reasons
+}
+
+export const isHidden = (host: Host) => getHiddenReasons(host) !== HiddenBy.None
+export const isSuspended = (host: Host) => (getHiddenReasons(host) & HiddenBy.Suspense) !== 0
+export const isFrozen = (host: Host) => (getHiddenReasons(host) & HiddenBy.Freeze) !== 0
+export const isInHiddenActivity = (host: Host) => (getHiddenReasons(host) & HiddenBy.Activity) !== 0
+
+const isHiddenOffscreen = (offscreen: RawFiber, boundaries: Map): boolean => {
+ let hidden = boundaries.get(offscreen)
+
+ if (hidden === undefined) {
+ hidden = committedHalf(offscreen, hiddenStateDiffers)?.memoizedState != null
+ boundaries.set(offscreen, hidden)
+
+ if (offscreen.alternate) {
+ boundaries.set(offscreen.alternate, hidden)
+ }
+ }
+
+ return hidden
+}
+
+// Hot loops below: indexed loops and writes, explicit null checks, no helper calls (legacy Hermes)
+export const collectVisibleAncestors = (hosts: ReadonlyArray, visit: (host: Host) => void) => {
+ const visibleByTag: Array = [] // 1 visible, 2 hidden
+ const boundaries = new Map()
+ const pending: Array = []
+
+ for (let h = 0; h < hosts.length; h++) {
+ let visible = true
+ let count = 0
+
+ for (let current = hosts[h] as unknown as RawFiber | null | undefined; current != null; current = current.return) {
+ if (current.tag === HOST_COMPONENT) {
+ const stateNode = current.stateNode
+
+ if (stateNode == null || stateNode.canonical == null) {
+ break
+ }
+
+ const answer = visibleByTag[stateNode.canonical.nativeTag]
+
+ if (answer !== undefined) {
+ visible = answer === 1
+ break
+ }
+
+ pending[count++] = current
+ } else if (current.tag === OFFSCREEN && isHiddenOffscreen(current, boundaries)) {
+ visible = false
+ break
+ }
+ }
+
+ for (let i = 0; i < count; i++) {
+ const canonical = pending[i].stateNode.canonical
+
+ visibleByTag[canonical.nativeTag] = visible ? 1 : 2
+
+ if (visible && canonical.internalInstanceHandle != null) {
+ visit(canonical.internalInstanceHandle as Host)
+ }
+ }
+ }
+}
+
+// Hidden hosts included
+export const collectAncestors = (hosts: ReadonlyArray, visit: (host: Host) => void) => {
+ const visitedByTag: Array = []
+
+ for (let h = 0; h < hosts.length; h++) {
+ for (let current = hosts[h] as unknown as RawFiber | null | undefined; current != null; current = current.return) {
+ if (current.tag !== HOST_COMPONENT) {
+ continue
+ }
+
+ const stateNode = current.stateNode
+
+ if (stateNode == null || stateNode.canonical == null) {
+ break
+ }
+
+ const canonical = stateNode.canonical
+
+ if (visitedByTag[canonical.nativeTag] === true) {
+ break
+ }
+
+ visitedByTag[canonical.nativeTag] = true
+
+ if (canonical.internalInstanceHandle != null) {
+ visit(canonical.internalInstanceHandle as Host)
+ }
+ }
+ }
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..c14f070
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,43 @@
+import { createOwnerRegistry } from './adapters/registry'
+import { animatedComponentOf, reanimatedCSSAdapter } from './adapters/reanimated'
+import { getCreationStack, getOwners, verify } from './diagnostics'
+import * as HostApi from './host'
+
+export type { Host } from './host'
+export type { Drift } from './diagnostics'
+export type { ShadowNode } from './fiber'
+export type { OwnerAdapter, OwnerRegistry, StyleSource } from './adapters/registry'
+
+// Object literals, not classes: static methods on a Babel-transpiled class are slower on Hermes
+export const Fiber = {
+ getHostFor: HostApi.getHostFor,
+ getShadowNodeFor: HostApi.getShadowNodeFor,
+ getHostForInstanceHandle: HostApi.getHostForInstanceHandle,
+ getContentContainerFor: HostApi.getContentContainerFor,
+
+ getShadowNode: HostApi.getShadowNode,
+ getShadowNodes: HostApi.getShadowNodes,
+ getNativeTag: HostApi.getNativeTag,
+ getParentHost: HostApi.getParentHost,
+
+ isHidden: HostApi.isHidden,
+ isSuspended: HostApi.isSuspended,
+ isFrozen: HostApi.isFrozen,
+ isInHiddenActivity: HostApi.isInHiddenActivity,
+
+ collectVisibleAncestors: HostApi.collectVisibleAncestors,
+ collectAncestors: HostApi.collectAncestors,
+
+ patchRenderedProps: HostApi.patchRenderedProps,
+
+ verify,
+ getOwners,
+ getCreationStack,
+
+ createOwnerRegistry,
+}
+
+export const Reanimated = {
+ animatedComponentOf,
+ cssAdapter: reanimatedCSSAdapter,
+}
diff --git a/test/collect.test.ts b/test/collect.test.ts
new file mode 100644
index 0000000..59a830a
--- /dev/null
+++ b/test/collect.test.ts
@@ -0,0 +1,169 @@
+// collectVisibleAncestors / collectAncestors vs per-host oracles through Suspense, Activity and react-freeze sequences.
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { Fiber, type Host } from '../src/index'
+
+const require = createRequire(import.meta.url)
+const mode = process.env.UNIFIBER_MODE ?? 'development'
+const { React, render, act, View } = require('./harness/fabric.cjs')({ mode })
+const h = React.createElement
+const rerender = (element: any, tag: number) => (mode === 'development' ? act(() => render(element, tag)) : render(element, tag))
+const { Freeze } = require(require('./harness/paths.cjs').REACT_FREEZE)
+
+const pending = { then() {} }
+const Gate = ({ suspend, children }: any) => {
+ if (suspend) throw pending
+ return children
+}
+
+type State = { suspend: boolean; freeze: boolean; activity: 'visible' | 'hidden'; n: number }
+const LEAVES = 4
+
+const app = (s: State, ref: (key: string) => any) =>
+ h(View, { testID: `root-${s.n}` },
+ h(View, { testID: 'visible' }, ...Array.from({ length: LEAVES }, (_, i) => h(View, { key: i, ref: ref(`v${i}`) }))),
+ h(React.Activity, { mode: s.activity },
+ h(Freeze, { freeze: s.freeze },
+ h(React.Suspense, { fallback: h(View, { testID: 'fallback', ref: ref('fallback') }) },
+ h(Gate, { suspend: s.suspend },
+ h(View, { testID: `screen-${s.n}` },
+ h(View, null, ...Array.from({ length: LEAVES }, (_, i) => h(View, { key: i, ref: ref(`s${i}`) })))))))))
+
+const tagsOf = (collect: (visit: (host: Host) => void) => void) => {
+ const tags: Array = []
+ collect((host) => tags.push(Fiber.getNativeTag(host)!))
+ return tags.sort((a, b) => a - b)
+}
+
+const oracle = (hosts: Array) =>
+ tagsOf((visit) => {
+ const seen = new Set()
+ for (const host of hosts) {
+ if (Fiber.getNativeTag(host) === undefined || Fiber.isHidden(host)) continue // unmounted, or hidden
+ for (let current: Host | undefined = host; current; current = Fiber.getParentHost(current)) {
+ const tag = Fiber.getNativeTag(current)!
+ if (seen.has(tag)) break
+ seen.add(tag)
+ visit(current)
+ }
+ }
+ })
+
+// Unistyles ShadowRegistry/index.ts:99-124
+const unistylesToday = (fibers: Array) => {
+ const tags = new Set()
+ for (const fiber of fibers) {
+ if (!fiber.stateNode) continue // unmounted: the registry would have unlinked it
+ const chain: Array = []
+ let hidden = false
+ for (let current = fiber; current; current = current.return) {
+ if ((current.tag === 13 || current.tag === 22) && current.memoizedState !== null) {
+ hidden = true
+ break
+ }
+ if (current.tag === 5) chain.push(current.stateNode?.canonical?.nativeTag)
+ }
+ if (!hidden) for (const tag of chain) tags.add(tag)
+ }
+ return [...tags].sort((a, b) => a - b)
+}
+
+const shuffle = (items: Array, seed: number) => {
+ const out = items.slice()
+ for (let i = out.length - 1; i > 0; i--) {
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff
+ const j = seed % (i + 1)
+ ;[out[i], out[j]] = [out[j], out[i]]
+ }
+ return out
+}
+
+// Uniwind fiberBaseline.ts:216-236
+const uniwindCollectHostNodes = (fiber: any, nodes: Set, visited: Set) => {
+ for (let current = fiber; current && !visited.has(current); current = current.return) {
+ visited.add(current)
+ if (current.alternate) visited.add(current.alternate)
+ if (current.tag !== 5) continue
+ for (const target of [current, current.alternate]) {
+ const node = target?.stateNode?.node
+ if (node) nodes.add(node)
+ }
+ }
+}
+
+const allAncestorsOracle = (hosts: Array) =>
+ tagsOf((visit) => {
+ const seen = new Set()
+ for (const host of hosts) {
+ if (Fiber.getNativeTag(host) === undefined) continue // unmounted
+ for (let current: Host | undefined = host; current; current = Fiber.getParentHost(current)) {
+ const tag = Fiber.getNativeTag(current)!
+ if (seen.has(tag)) break
+ seen.add(tag)
+ visit(current)
+ }
+ }
+ })
+
+const run = (steps: Array>) => {
+ const instances = new Map()
+ const ref = (key: string) => (r: any) => r && instances.set(key, r)
+ let state: State = { suspend: false, freeze: false, activity: 'visible', n: 0 }
+ const tag = render(app(state, ref))
+ const hosts = [...instances.values()].map((r) => Fiber.getHostFor(r)!)
+ const failures: Array = []
+ const today: Array = []
+
+ for (const [i, step] of steps.entries()) {
+ state = { ...state, ...step, n: state.n + 1 }
+ rerender(app(state, ref), tag)
+ for (const r of instances.values()) {
+ const host = Fiber.getHostFor(r)!
+ if (!hosts.includes(host)) hosts.push(host)
+ }
+ const expected = oracle(hosts)
+
+ const allExpected = allAncestorsOracle(hosts)
+ for (const seed of [1, 2, 3]) {
+ const all = tagsOf((visit) => Fiber.collectAncestors(shuffle(hosts, seed), visit))
+ if (JSON.stringify(all) !== JSON.stringify(allExpected)) failures.push(`step ${i} collectAncestors seed ${seed}: ${all} vs ${allExpected}`)
+ }
+ const uniwindNodes = new Set()
+ const visitedFibers = new Set()
+ for (const host of hosts) if (Fiber.getNativeTag(host) !== undefined) uniwindCollectHostNodes(host, uniwindNodes, visitedFibers)
+ const ourNodes = new Set()
+ Fiber.collectAncestors(hosts, (x) => Fiber.getShadowNodes(x).forEach((n) => ourNodes.add(n)))
+ const missing = [...uniwindNodes].filter((n) => !ourNodes.has(n)).length
+ const extra = [...ourNodes].filter((n) => !uniwindNodes.has(n)).length
+ if (missing || extra) failures.push(`step ${i} collectAncestors vs Uniwind collectHostNodes: missing ${missing}, extra ${extra}`)
+ for (const seed of [1, 2, 3]) {
+ const got = tagsOf((visit) => Fiber.collectVisibleAncestors(shuffle(hosts, seed), visit))
+ if (JSON.stringify(got) !== JSON.stringify(expected)) failures.push(`step ${i} ${JSON.stringify(step)} seed ${seed}: ${got} vs ${expected}`)
+ }
+ const todayTags = unistylesToday(hosts)
+ if (JSON.stringify(todayTags) !== JSON.stringify(expected)) {
+ const fallbackTag = instances.has('fallback') ? Fiber.getNativeTag(Fiber.getHostFor(instances.get('fallback'))!) : undefined
+ const missing = expected.filter((t) => !todayTags.includes(t)).map((t) => (t === fallbackTag ? `${t}(fallback)` : t))
+ const extra = todayTags.filter((t) => !expected.includes(t))
+ today.push(`step ${i} ${JSON.stringify(step)} missing [${missing}] extra [${extra}]`)
+ }
+ }
+
+ return { failures, today }
+}
+
+const SEQUENCES: Record>> = {
+ suspense: [{ suspend: true }, {}, { suspend: false }, {}, { suspend: true }, { suspend: false }],
+ activity: [{ activity: 'hidden' }, {}, { activity: 'visible' }, {}, { activity: 'hidden' }, { activity: 'visible' }],
+ freeze: [{ freeze: true }, {}, { freeze: false }, {}, { freeze: true }, { freeze: false }],
+ nested: [{ freeze: true }, { activity: 'hidden' }, { freeze: false }, { activity: 'visible' }, { suspend: true }, { freeze: true }, { suspend: false }, { freeze: false }],
+}
+
+for (const [name, steps] of Object.entries(SEQUENCES)) {
+ test(`collectVisibleAncestors / collectAncestors = oracles (+ Uniwind collectHostNodes): ${name}`, () => {
+ const { failures, today } = run(steps)
+ if (today.length) console.log(`# ${name}: today's walk differs at ${today.length}/${steps.length} steps\n# ${today.join('\n# ')}`)
+ assert.deepEqual(failures, [])
+ })
+}
diff --git a/test/context.test.ts b/test/context.test.ts
new file mode 100644
index 0000000..2ac05b8
--- /dev/null
+++ b/test/context.test.ts
@@ -0,0 +1,41 @@
+// A provider's other half keeps its old value, and a bailed-out subtree's `return` pointers can reach it.
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import * as U from '../src/fiber'
+
+const require = createRequire(import.meta.url)
+const mode = process.env.UNIFIBER_MODE ?? 'development'
+const { React, render, act, View } = require('./harness/fabric.cjs')({ mode })
+const h = React.createElement
+const rerender = (element: any, tag: number) => (mode === 'development' ? act(() => render(element, tag)) : render(element, tag))
+
+test('readContext / contextReaderFor: never the provider value of the stale half', () => {
+ const Ctx = React.createContext('default')
+ let reader: any = null
+ let host: any = null
+ class Reader extends React.Component {
+ render() {
+ return h(View, { ref: (r: any) => r && (host = r) })
+ }
+ }
+ // `k` re-renders the memoized subtree so its halves swap
+ const Content = React.memo(({ k }: any) => h(View, { testID: `content-${k}` }, h(Reader, { k, ref: (r: any) => r && (reader = r) })))
+ const app = (value: string, n: number, k = 0) => h(View, { testID: `root-${n}` }, h(Ctx, { value }, h(Content, { k })))
+ const tag = render(app('a', 0))
+ const creation = host.__internalInstanceHandle
+ const stale: Array = []
+
+ const steps: Array<[string, number]> = [['b', 0], ['c', 0], ['c', 1], ['d', 1], ['e', 1], ['e', 2], ['f', 2], ['f', 3], ['g', 3], ['g', 3]]
+ for (const [n, [value, k]] of steps.entries()) {
+ rerender(app(value, n + 1, k), tag)
+ const reads = {
+ 'reader (committed)': U.contextReaderFor(reader, false)(Ctx),
+ 'host creation fiber': U.readContext(creation, Ctx),
+ 'host alternate': creation.alternate ? U.readContext(creation.alternate, Ctx) : value,
+ }
+ for (const [how, got] of Object.entries(reads)) if (got !== value) stale.push(`${value}: ${how} read ${got}`)
+ }
+
+ assert.deepEqual(stale, [])
+})
diff --git a/test/diagnostics.test.ts b/test/diagnostics.test.ts
new file mode 100644
index 0000000..966ac9a
--- /dev/null
+++ b/test/diagnostics.test.ts
@@ -0,0 +1,111 @@
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { Fiber } from '../src/index'
+
+const require = createRequire(import.meta.url)
+const mode = process.env.UNIFIBER_MODE ?? 'development'
+const { React, render, act, View } = require('./harness/fabric.cjs')({ mode })
+const h = React.createElement
+const rerender = (element: any, tag: number) => (mode === 'development' ? act(() => render(element, tag)) : render(element, tag))
+
+// RN 0.87 ScrollView shape (ScrollView.js:1281)
+class ScrollView extends React.Component {
+ _inner: any = null
+ _native: any = null
+ setInner = (r: any) => {
+ this._inner = r
+ }
+ setNative = (r: any) => {
+ if (r) {
+ this._native = Object.assign(r, {
+ getScrollResponder: () => this._native,
+ getNativeScrollRef: () => this._native,
+ getInnerViewRef: () => this._inner,
+ })
+ }
+ this.props.forwardedRef?.(this._native)
+ }
+ render() {
+ // iOS: refreshControl is the scroll host's first child (ScrollView.js:1925-1933)
+ return h('RCTScrollView', { ref: this.setNative },
+ this.props.refreshControl ? h('RCTRefreshControl') : null,
+ h('RCTScrollContentView', { ref: this.setInner, testID: 'content' }, this.props.children))
+ }
+}
+class FlatList extends React.Component {
+ _scroll: any = null
+ getScrollResponder() {
+ return this._scroll
+ }
+ getNativeScrollRef() {
+ return this._scroll
+ }
+ render() {
+ return h(ScrollView, { refreshControl: this.props.refreshControl, forwardedRef: (r: any) => (this._scroll = r) })
+ }
+}
+
+test('getContentContainerFor: ScrollView and FlatList refs, with and without refreshControl', () => {
+ for (const refreshControl of [false, true]) {
+ let scroll: any = null
+ let list: any = null
+ render(h(View, null,
+ h(ScrollView, { refreshControl, forwardedRef: (r: any) => r && (scroll = r) }),
+ h(FlatList, { refreshControl, ref: (r: any) => r && (list = r) })))
+
+ for (const ref of [scroll, list]) {
+ const content = Fiber.getContentContainerFor(ref)!
+ assert.ok(content, `found for ${ref.constructor.name}, refreshControl ${refreshControl}`)
+ assert.equal((content as any).type, 'RCTScrollContentView')
+ assert.equal(Fiber.getParentHost(content), Fiber.getHostFor(ref.getNativeScrollRef?.() ?? ref))
+ }
+ }
+})
+
+test('verify: a native-only update React would revert, until the baseline is patched', () => {
+ let instance: any = null
+ const style = { backgroundColor: 'white', padding: 4 }
+ const app = (n: number) => h(View, { testID: `root-${n}` }, h(View, { ref: (r: any) => r && (instance = r), style: [style, null] }))
+ const tag = render(app(0))
+ const host = Fiber.getHostFor(instance)!
+
+ assert.deepEqual(Fiber.verify(host, { backgroundColor: 'white' }), [])
+ assert.deepEqual(Fiber.verify(host, { backgroundColor: 'black' }), [{ key: 'backgroundColor', react: 'white', expected: 'black' }])
+
+ Fiber.patchRenderedProps(host, (props) => ({ ...props, style: [{ ...style, backgroundColor: 'black' }] }))
+ assert.deepEqual(Fiber.verify(host, { backgroundColor: 'black', padding: 4 }), [])
+ for (let n = 1; n <= 3; n++) {
+ rerender(app(n), tag)
+ assert.deepEqual(Fiber.verify(host, { backgroundColor: 'black' }), [{ key: 'backgroundColor', react: 'white', expected: 'black' }])
+ }
+})
+
+test('getOwners / getCreationStack: dev only', () => {
+ let instance: any = null
+ function ProfileCard() {
+ return h(View, { ref: (r: any) => r && (instance = r) })
+ }
+ function ProfileScreen() {
+ return h(ProfileCard)
+ }
+ render(h(ProfileScreen))
+ const host = Fiber.getHostFor(instance)!
+
+ if (mode === 'development') {
+ assert.deepEqual(Fiber.getOwners(host), ['View', 'ProfileCard', 'ProfileScreen'])
+ assert.match(Fiber.getCreationStack(host)!, /createElement/)
+ } else {
+ assert.deepEqual(Fiber.getOwners(host), [])
+ assert.equal(Fiber.getCreationStack(host), undefined)
+ }
+})
+
+test('verify: falsy and non-object entries in a style array are skipped, like RN flattenStyle', () => {
+ let instance: any = null
+ render(h(View, { ref: (r: any) => r && (instance = r), style: [false, null, 0, 'stray', [undefined, { opacity: 0.5 }]] }))
+ const host = Fiber.getHostFor(instance)!
+
+ assert.deepEqual(Fiber.verify(host, { opacity: 0.5 }), [])
+ assert.deepEqual(Fiber.verify(host, { opacity: 1 }), [{ key: 'opacity', react: 0.5, expected: 1 }])
+})
diff --git a/test/edges.test.ts b/test/edges.test.ts
new file mode 100644
index 0000000..7f4f20c
--- /dev/null
+++ b/test/edges.test.ts
@@ -0,0 +1,257 @@
+// Rare branches: committedOf's slow-path cases on hand-built fiber pairs, detached fibers, adapter and registry guards.
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import * as U from '../src/fiber'
+import { Fiber, Reanimated, type Host } from '../src/index'
+import { createOwnerRegistry, type OwnerAdapter, type StyleSource } from '../src/adapters/registry'
+
+const require = createRequire(import.meta.url)
+const mode = process.env.UNIFIBER_MODE ?? 'development'
+const { React, render, act, View } = require('./harness/fabric.cjs')({ mode })
+const h = React.createElement
+const rerender = (element: any, tag: number) => (mode === 'development' ? act(() => render(element, tag)) : render(element, tag))
+
+type F = { tag: number; return: F | null; alternate: F | null; child: F | null; sibling: F | null; stateNode: any }
+const fiber = (tag = 5): F => ({ tag, return: null, alternate: null, child: null, sibling: null, stateNode: null })
+const pair = (tag = 5): [F, F] => {
+ const a = fiber(tag)
+ const b = fiber(tag)
+ a.alternate = b
+ b.alternate = a
+ return [a, b]
+}
+const root = (): F => {
+ const r = fiber(U.Tag.HostRoot)
+ r.stateNode = { current: r }
+ return r
+}
+const committedOf = (f: F) => U.committedOf(f as any) as unknown as F | null
+
+test('committedOf: a parent that never re-rendered (no alternate) is skipped, like React', () => {
+ const r = root()
+ const parent = fiber(0)
+ const [a, b] = pair()
+ parent.return = r
+ r.child = parent
+ a.return = parent
+ b.return = parent
+ parent.child = a
+
+ assert.equal(committedOf(a), a)
+ assert.equal(committedOf(b), b)
+})
+
+test('committedOf: detached (no HostRoot at the top) or inconsistent pairs answer null, where React throws', () => {
+ // walks off the top without meeting a HostRoot
+ const [a, b] = pair()
+ const orphanParent = fiber(0)
+ a.return = orphanParent
+ b.return = orphanParent
+ assert.equal(committedOf(a), null)
+
+ // parents that aren't each other's alternate
+ const r = root()
+ const [pa, pb] = pair(0)
+ const stranger = fiber(0)
+ pa.return = r
+ pb.return = r
+ const [x, y] = pair()
+ x.return = pa
+ y.return = stranger
+ stranger.alternate = fiber(0)
+ pa.child = x
+ stranger.child = y
+ assert.equal(committedOf(x), null)
+
+ // parents share a child list that holds neither fiber
+ const [sa, sb] = pair(0)
+ const shared = fiber()
+ sa.child = shared
+ sb.child = shared
+ const [c, d] = pair()
+ c.return = sa
+ d.return = sb
+ assert.equal(committedOf(c), null)
+
+ // same return pointer, but neither parent lists either child
+ const [qa, qb] = pair(0)
+ const [e, f] = pair()
+ e.return = qa
+ f.return = qa
+ qa.child = fiber()
+ qb.child = fiber()
+ assert.equal(committedOf(e), null)
+})
+
+test('committedOf on real fibers: unmounted views answer null, mounted ones the committed half', () => {
+ let ref: any = null
+ const app = (show: boolean, n: number) => h(View, { testID: `r${n}` }, show ? h(View, { ref: (r: any) => r && (ref = r), style: { opacity: n } }) : null)
+ const tag = render(app(true, 1))
+ rerender(app(true, 2), tag)
+ const creation = ref.__internalInstanceHandle
+
+ assert.equal((U.committedOf(creation) as any).memoizedProps.style.opacity, 2)
+ rerender(app(false, 3), tag)
+ assert.equal(U.committedOf(creation), null)
+ assert.equal(Fiber.getNativeTag(creation as Host), undefined)
+ assert.equal(Fiber.isHidden(creation as Host), false)
+})
+
+test('getShadowNodes: two revisions only while a render is in flight; one shared node once committed', () => {
+ let ref: any = null
+ const during: Array = []
+ // renders after the first view completed: its work-in-progress half holds a new node
+ const Probe = ({ n }: any) => {
+ if (ref && n > 0) during.push(Fiber.getShadowNodes(Fiber.getHostFor(ref)!).length)
+ return null
+ }
+ const app = (n: number) => h(View, null, h(View, { ref: (r: any) => r && (ref = r), style: { opacity: n } }), h(Probe, { n }))
+ const tag = render(app(0))
+
+ for (let n = 1; n <= 3; n++) {
+ rerender(app(n), tag)
+ assert.equal(Fiber.getShadowNodes(Fiber.getHostFor(ref)!).length, 1)
+ }
+ assert.deepEqual(during, [2, 2, 2], 'mid-render: the committed node and the one React is about to commit')
+})
+
+test('Reanimated.animatedComponentOf: a collapsable={false} view no ref ever attached to is not claimed', () => {
+ let parent: any = null
+ render(h(View, { ref: (r: any) => r && (parent = r) }, h(View, { collapsable: false })))
+ const child = U.firstHostBelow((parent.__internalInstanceHandle as any).child)
+
+ assert.equal((child as any).memoizedProps.collapsable, false)
+ assert.equal(Reanimated.animatedComponentOf(child as unknown as Host), null)
+})
+
+test('cssAdapter guards: not mounted yet (no CSS manager) and styles that aren\'t the source\'s', () => {
+ const calls: Array = []
+ const owner: any = {
+ _cssStyle: { transitionProperty: ['backgroundColor'], backgroundColor: 'white' },
+ _updateStyles(props: any) {
+ calls.push('original')
+ this._cssStyle = { ...props.style }
+ },
+ shouldComponentUpdate: () => true,
+ props: { style: { transitionProperty: ['backgroundColor'], backgroundColor: 'white' } },
+ _reactInternals: null,
+ }
+ const source: StyleSource = { name: 's', epoch: () => 0, resolve: () => undefined }
+
+ Reanimated.cssAdapter.attach(owner, source)
+ Reanimated.cssAdapter.attach(owner, source) // idempotent
+ assert.doesNotThrow(() => Reanimated.cssAdapter.refresh(owner, source))
+ owner._updateStyles(owner.props)
+ assert.equal(owner._cssStyle.backgroundColor, 'white')
+ assert.deepEqual(calls, ['original'])
+})
+
+test('registry guards: null hosts, hidden fibers, an adapter switched off on the refresh path', () => {
+ const seen: Array = []
+ const adapter: OwnerAdapter = {
+ name: 'probe',
+ gate: () => true,
+ claim: () => {
+ seen.push('claim')
+ throw new Error('field moved')
+ },
+ attach: () => {},
+ refresh: () => {},
+ }
+ const errors: Array = []
+ const registry = createOwnerRegistry({ name: 's', epoch: () => 0, resolve: () => undefined }, [adapter], (reason) => errors.push(reason))
+ let ref: any = null
+ render(h(React.Suspense, { fallback: null }, h(View, { ref: (r: any) => r && (ref = r) })))
+ const fiberOfView = Fiber.getHostFor(ref)
+
+ registry.link(null)
+ registry.refresh([null])
+ registry.refresh([fiberOfView]) // throws inside claim: switched off, reported once
+ registry.refresh([fiberOfView]) // gate passes, but the adapter is off
+ registry.link(fiberOfView)
+
+ assert.deepEqual(seen, ['claim'])
+ assert.equal(errors.length, 1)
+ assert.match(errors[0], /probe failed: Error: field moved/)
+})
+
+test('committedOf: an asymmetric pair under a shared parent answers null (React throws)', () => {
+ const [qa, qb] = pair(0)
+ qb.alternate = fiber(0) // qa -> qb, but qb no longer points back
+ const [x, y] = pair()
+ x.return = qa
+ y.return = qa
+ qa.child = fiber()
+ qb.child = x
+ assert.equal(committedOf(x), null)
+})
+
+test('readContext: a detached provider whose halves disagree reads the half the walk reached', () => {
+ const Ctx = React.createContext('default')
+ const [pa, pb] = pair(U.Tag.ContextProvider) as Array
+ pa.type = Ctx
+ pb.type = Ctx
+ pa.memoizedProps = { value: 'reached' }
+ pb.memoizedProps = { value: 'other' }
+ const leaf: any = fiber()
+ leaf.return = pa
+ assert.equal(U.readContext(leaf, Ctx), 'reached')
+})
+
+test('refs that resolve to nothing: plain objects, composites with no host, a scroll ref exposing only getNativeScrollRef', () => {
+ let inner: any = null
+ let empty: any = null
+ class Empty extends React.Component {
+ render() {
+ return null
+ }
+ }
+ render(h(View, null, h(View, { ref: (r: any) => r && (inner = r) }), h(Empty, { ref: (r: any) => r && (empty = r) })))
+
+ assert.equal(Fiber.getHostFor({}), undefined)
+ assert.equal(Fiber.getHostFor(null), undefined)
+ assert.equal(Fiber.getHostFor(empty), undefined, 'a class that renders no host')
+ assert.equal(Fiber.getContentContainerFor({}), undefined)
+ // scroll components exposing only getNativeScrollRef
+ assert.equal(Fiber.getHostFor({ getNativeScrollRef: () => inner }), Fiber.getHostFor(inner))
+})
+
+test('collectVisibleAncestors skips missing hosts (collected / unmounted handles from C++)', () => {
+ let ref: any = null
+ render(h(View, null, h(View, { ref: (r: any) => r && (ref = r) })))
+ const visited: Array = []
+ Fiber.collectVisibleAncestors([undefined, null, Fiber.getHostForInstanceHandle(null), Fiber.getHostFor(ref)], (x) => visited.push(x))
+ assert.equal(visited.length, 2, 'the view and its parent')
+})
+
+test('verify on an unmounted view reads the half it holds', () => {
+ let ref: any = null
+ const app = (show: boolean, n: number) => h(View, { testID: `r${n}` }, show ? h(View, { ref: (r: any) => r && (ref = r), style: { opacity: n } }) : null)
+ const tag = render(app(true, 1))
+ rerender(app(true, 2), tag)
+ const host = Fiber.getHostFor(ref)!
+ rerender(app(false, 3), tag)
+ assert.deepEqual(Fiber.verify(host, {}), [])
+ assert.equal(Fiber.verify(host, { opacity: 99 }).length, 1)
+})
+
+test('getOwners: names through forwardRef, memo and displayName (dev)', () => {
+ let ref: any = null
+ const Inner = React.forwardRef(function InnerRender(props: any, forwarded: any) {
+ return h(View, { ...props, ref: forwarded })
+ })
+ const Named = (props: any) => h(Inner, props)
+ Named.displayName = 'NamedCard'
+ const Memo = React.memo(function MemoCard(props: any) {
+ return h(Named, props)
+ })
+ render(h(Memo, { ref: (r: any) => r && (ref = r) }))
+ const owners = Fiber.getOwners(Fiber.getHostFor(ref)!)
+
+ if (mode === 'development') {
+ assert.deepEqual(owners.slice(0, 4), ['View', 'InnerRender', 'NamedCard', 'MemoCard'])
+ } else {
+ assert.deepEqual(owners, [])
+ }
+})
diff --git a/test/fiber-shapes.test.ts b/test/fiber-shapes.test.ts
new file mode 100644
index 0000000..50fa3cc
--- /dev/null
+++ b/test/fiber-shapes.test.ts
@@ -0,0 +1,212 @@
+// The fiber shapes unifiber reads, pinned against the renderer react-native ships.
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import * as U from '../src/fiber'
+import { Fiber, type Host } from '../src/index'
+
+const require = createRequire(import.meta.url)
+const mode = process.env.UNIFIBER_MODE ?? 'development'
+const { React, render, act, View, ops, rnVersion, reactVersion } = require('./harness/fabric.cjs')({ mode })
+const h = React.createElement
+const rerender = (element: any, tag: number) => (mode === 'development' ? act(() => render(element, tag)) : render(element, tag))
+
+console.log(`# React ${reactVersion}, RN ${rnVersion}, ${mode}`)
+
+const capture = () => {
+ const box: { current: any } = { current: null }
+ const ref = (r: any) => {
+ box.current = r
+ }
+ return [box, ref] as const
+}
+
+test('host ref -> fiber -> shadow node', () => {
+ const [box, ref] = capture()
+ render(h(View, { ref, style: { flex: 1 } }))
+ const fiber = U.hostFiberOf(box.current)
+
+ assert.ok(fiber)
+ assert.equal((fiber as any).tag, U.Tag.HostComponent)
+ assert.equal(typeof Fiber.getShadowNode(fiber as unknown as Host), 'object')
+ assert.equal(typeof Fiber.getNativeTag(fiber as unknown as Host), 'number')
+ assert.equal(box.current.__nativeTag, Fiber.getNativeTag(fiber as unknown as Host))
+ assert.deepEqual(Fiber.getShadowNodes(fiber as unknown as Host), [Fiber.getShadowNode(fiber as unknown as Host)], 'one revision before any re-render')
+})
+
+test('fibers are sealed in development: no expandos, side tables only', () => {
+ const [box, ref] = capture()
+ render(h(View, { ref }))
+ const fiber = box.current.__internalInstanceHandle
+
+ assert.equal(Object.isExtensible(fiber), mode !== 'development')
+})
+
+test('__internalInstanceHandle stays the creation fiber while the committed one alternates', () => {
+ const [box, ref] = capture()
+ const tag = render(h(View, { ref, style: { opacity: 1 } }))
+ const handle = box.current.__internalInstanceHandle
+ const seen = new Set()
+
+ for (let i = 2; i < 6; i++) {
+ rerender(h(View, { ref, style: { opacity: i / 10 } }), tag)
+ assert.equal(box.current.__internalInstanceHandle, handle)
+
+ const committed = U.committedOf(handle) as any
+
+ seen.add(committed)
+ assert.equal(committed.memoizedProps.style.opacity, i / 10, 'committedOf returns the fiber with the latest props')
+ assert.equal(handle.stateNode.canonical, handle.alternate.stateNode.canonical)
+ assert.equal(handle.stateNode.canonical.currentProps, committed.memoizedProps)
+ const nodes = Fiber.getShadowNodes(handle)
+ assert.ok(nodes.length >= 1 && nodes.length <= 2 && nodes.includes(committed.stateNode.node))
+ }
+
+ assert.equal(seen.size, 2, 'the committed fiber flips between the pair')
+})
+
+test('class component instance -> fiber via _reactInternals', () => {
+ let instance: any
+ class Owner extends React.Component {
+ constructor(props: any) {
+ super(props)
+ instance = this
+ }
+ render() {
+ return h(View, { collapsable: false })
+ }
+ }
+ render(h(Owner))
+ const fiber = U.fiberOfInstance(instance) as any
+
+ assert.equal(fiber.tag, U.Tag.ClassComponent)
+ assert.equal(fiber.stateNode, instance)
+})
+
+test('host -> parent host crosses function components; props of either half', () => {
+ let owner: any
+ const [box, ref] = capture()
+ const RNView = (props: any) => h(View, props)
+ const UnistylesView = (props: any) => h(RNView, props)
+ class AnimatedComponent extends React.Component {
+ constructor(props: any) {
+ super(props)
+ owner = this
+ }
+ render() {
+ return h(UnistylesView, { ...this.props, collapsable: false })
+ }
+ }
+ render(h(View, null, h(AnimatedComponent, { ref: undefined, testID: 'a' }, h(View, { ref }))))
+ const outer = U.hostFiberOf(box.current)
+
+ const animatedHost = U.hostAncestor(outer)
+
+ assert.equal((animatedHost as any)?.memoizedProps?.collapsable, false)
+ assert.equal(Fiber.getParentHost(outer as unknown as Host), animatedHost)
+ assert.ok(U.hostAncestor(outer, 2), 'two hosts up: the outer View')
+ assert.equal(U.hostAncestor(outer, 3), null, 'nothing above the root view')
+ assert.ok(owner)
+})
+
+test('context: and are the same provider fiber in React 19', () => {
+ const Ctx = React.createContext(null)
+ const [a, refA] = capture()
+ const [b, refB] = capture()
+ render(h(View, null, h(Ctx, { value: { name: 'dark' } }, h(View, { ref: refA })), h(Ctx.Provider, { value: { name: 'light' } }, h(View, { ref: refB }))))
+
+ assert.equal(Ctx.Provider, Ctx)
+ assert.deepEqual(U.readContext(U.hostFiberOf(a.current), Ctx), { name: 'dark' })
+ assert.deepEqual(U.readContext(U.hostFiberOf(b.current), Ctx), { name: 'light' })
+ assert.equal(U.readContext(U.hostFiberOf(a.current), React.createContext(null)), undefined)
+})
+
+test('Suspense fallback hides the primary subtree (Suspense 13 + Offscreen 22 memoizedState)', () => {
+ const [box, ref] = capture()
+ const pending = { then() {} }
+ const Gate = ({ suspend }: any) => {
+ if (suspend) throw pending
+ return h(View, { ref })
+ }
+ const tag = render(h(React.Suspense, { fallback: null }, h(Gate, { suspend: false })))
+ const fiber = U.hostFiberOf(box.current)
+
+ assert.equal(Fiber.isHidden(fiber as unknown as Host), false)
+ rerender(h(React.Suspense, { fallback: null }, h(Gate, { suspend: true })), tag)
+ assert.equal(Fiber.isHidden(fiber as unknown as Host), true, 'react-freeze and freezeOnBlur go through here')
+})
+
+test('Activity hidden: Activity 31 -> Offscreen 22 with memoizedState', () => {
+ const [box, ref] = capture()
+ const tag = render(h(React.Activity, { mode: 'visible' }, h(View, { ref })))
+ const fiber = U.hostFiberOf(box.current)
+ const parent = (fiber as any).return.return // RCTView <- View (function) <- Offscreen
+
+ assert.equal(parent.tag, U.Tag.Offscreen)
+ assert.equal(parent.return.tag, U.Tag.Activity)
+ assert.equal(Fiber.isHidden(fiber as unknown as Host), false)
+
+ rerender(h(React.Activity, { mode: 'hidden' }, h(View, { ref })), tag)
+ assert.equal(Fiber.isInHiddenActivity(fiber as unknown as Host), true)
+})
+
+test('batched refresh: visible host chains, deduped, hidden skipped', () => {
+ const refs: Array = []
+ const ref = (i: number) => (r: any) => {
+ refs[i] = r
+ }
+ const pending = { then() {} }
+ const Frozen = () => {
+ throw pending
+ }
+ render(
+ h(View, { testID: 'root' },
+ h(View, { ref: ref(0) }),
+ h(View, { ref: ref(1) }),
+ h(React.Suspense, { fallback: null }, h(View, { ref: ref(2) }), h(Frozen))),
+ )
+ const visited: Array = []
+
+ Fiber.collectVisibleAncestors(refs.map((r) => Fiber.getHostFor(r)), (host) => visited.push((host as any).memoizedProps?.testID ?? 'leaf'))
+
+ assert.deepEqual(visited, ['leaf', 'root', 'leaf'])
+})
+
+test('Uniwind baseline: React diffs against memoizedProps, patching both keeps the bailout', () => {
+ const [box, ref] = capture()
+ const stale = { backgroundColor: 'white' }
+ const fresh = { backgroundColor: 'black' }
+ const Leaf = ({ style }: any) => h(View, { ref, style })
+ const tag = render(h(Leaf, { style: stale }))
+
+ rerender(h(Leaf, { style: stale }), tag)
+
+ U.patchRenderedProps(U.hostFiberOf(box.current), (props) => ({ ...props, style: fresh }))
+ ops.length = 0
+ rerender(h(Leaf, { style: fresh }), tag)
+
+ assert.deepEqual(ops.filter((o: any) => o.payload?.style), [], 'no clone: React saw no diff against the patched baseline')
+
+ rerender(h(Leaf, { style: stale }), tag)
+ assert.deepEqual(ops.at(-1).payload.style, stale)
+})
+
+test('render-phase context: Fabric is a secondary renderer, the stack is _currentValue2', () => {
+ const Ctx = React.createContext('default')
+ const seen: Array = []
+ class Reader extends React.Component {
+ shouldComponentUpdate() {
+ seen.push(U.readContextDuringRender(Ctx) as string)
+ return true
+ }
+ render() {
+ return h(View)
+ }
+ }
+ const app = (value: string) => h(Ctx, { value }, h(Reader, { n: value }))
+ const tag = render(app('a'))
+ for (const value of ['b', 'c', 'd']) rerender(app(value), tag)
+
+ assert.deepEqual(seen, ['b', 'c', 'd'])
+ assert.equal(U.readContextDuringRender(Ctx), 'default', 'outside render: default value')
+})
diff --git a/test/harness/asset-stub.cjs b/test/harness/asset-stub.cjs
new file mode 100644
index 0000000..11809d2
--- /dev/null
+++ b/test/harness/asset-stub.cjs
@@ -0,0 +1 @@
+module.exports = { __packager_asset: true, uri: 'stub.png', width: 1, height: 1 }
diff --git a/test/harness/fabric.cjs b/test/harness/fabric.cjs
new file mode 100644
index 0000000..0ea2cb3
--- /dev/null
+++ b/test/harness/fabric.cjs
@@ -0,0 +1,58 @@
+// React Native's Fabric renderer in Node, against a fake native side. require('./fabric.cjs')({ mode })
+const path = require('path')
+const Module = require('module')
+
+const { RN, REACT, SCHEDULER } = require('./paths.cjs')
+
+module.exports = ({ mode = 'development' } = {}) => {
+ process.env.NODE_ENV = mode
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true
+ globalThis.__DEV__ = mode === 'development'
+
+ const { privateInterface, ops, committed } = require('./stubs.cjs').install()
+
+ const originalLoad = Module._load
+ Module._load = function (request, parent, isMain) {
+ if (request === 'react') return originalLoad.call(this, REACT, parent, isMain)
+ if (request === 'scheduler')
+ return originalLoad.call(this, mode === 'development' ? SCHEDULER : path.join(SCHEDULER, 'unstable_mock.js'), parent, isMain)
+ // RN <= 0.88 paths, then 0.89+ entry points
+ if (request === 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') return privateInterface
+ if (request === 'react-native/react-private-interface') return privateInterface
+ if (request === 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore') return {}
+ if (request === 'react-native/setup-env') return {}
+ return originalLoad.call(this, request, parent, isMain)
+ }
+
+ const React = require(REACT)
+ const file = mode === 'development' ? 'ReactFabric-dev.js' : 'ReactFabric-prod.js'
+ const productionQueue = require('./flush.cjs')()
+ const ReactFabric = productionQueue.load(() => require(path.join(RN, 'Libraries', 'Renderer', 'implementations', file)))
+
+ let nextRootTag = 1
+ const render = (element, rootTag = nextRootTag++) => {
+ if (mode === 'development') {
+ React.act(() => {
+ ReactFabric.render(element, rootTag, null, true)
+ })
+ } else {
+ ReactFabric.render(element, rootTag, null, true)
+ productionQueue.flush()
+ }
+ return rootTag
+ }
+
+ const View = (props) => React.createElement('RCTView', props)
+
+ return {
+ React,
+ ReactFabric,
+ render,
+ act: React.act,
+ committed,
+ ops,
+ View,
+ rnVersion: require(path.join(RN, 'package.json')).version,
+ reactVersion: React.version,
+ }
+}
diff --git a/test/harness/flush.cjs b/test/harness/flush.cjs
new file mode 100644
index 0000000..eb86ea3
--- /dev/null
+++ b/test/harness/flush.cjs
@@ -0,0 +1,26 @@
+// Production has no act(). React 19.3+ schedules root work in a microtask, so the renderer gets a queue we can drain.
+const path = require('path')
+const { SCHEDULER } = require('./paths.cjs')
+
+module.exports = () => {
+ const microtasks = []
+ const scheduler = require(path.join(SCHEDULER, 'unstable_mock.js'))
+
+ return {
+ load: (requireRenderer) => {
+ const realQueueMicrotask = globalThis.queueMicrotask
+ globalThis.queueMicrotask = (task) => microtasks.push(task)
+ try {
+ return requireRenderer()
+ } finally {
+ globalThis.queueMicrotask = realQueueMicrotask
+ }
+ },
+ flush: () => {
+ do {
+ while (microtasks.length > 0) microtasks.shift()()
+ scheduler.unstable_flushAll()
+ } while (microtasks.length > 0)
+ },
+ }
+}
diff --git a/test/harness/initialize-core-stub.cjs b/test/harness/initialize-core-stub.cjs
new file mode 100644
index 0000000..f4101d4
--- /dev/null
+++ b/test/harness/initialize-core-stub.cjs
@@ -0,0 +1,2 @@
+// InitializeCore: device setup the renderer doesn't need in Node
+module.exports = {}
diff --git a/test/harness/paths.cjs b/test/harness/paths.cjs
new file mode 100644
index 0000000..3e84428
--- /dev/null
+++ b/test/harness/paths.cjs
@@ -0,0 +1,32 @@
+// UNIFIBER_RN=0.86 selects react-native 0.86; UNISTYLES_DIR points at a Unistyles package dir instead of the npm one
+const fs = require('fs')
+const path = require('path')
+
+const ROOT = path.resolve(__dirname, '..', '..')
+const nodeModules = path.join(ROOT, 'node_modules')
+const dir = (name) => fs.realpathSync(path.join(nodeModules, name))
+
+const RN_PACKAGES = { '0.87': 'react-native', '0.86': 'react-native-0.86' }
+const rnKey = process.env.UNIFIBER_RN ?? '0.87'
+
+if (!RN_PACKAGES[rnKey]) {
+ throw new Error(`UNIFIBER_RN=${rnKey}: supported ${Object.keys(RN_PACKAGES).join(', ')}`)
+}
+
+const RN = dir(RN_PACKAGES[rnKey])
+
+module.exports = {
+ ROOT,
+ RN,
+ RN_VERSION: require(path.join(RN, 'package.json')).version,
+ REACT: dir('react'),
+ SCHEDULER: dir('scheduler'),
+ BABEL_PRESET: dir('@react-native/babel-preset'),
+ BABEL_CORE: dir('@babel/core'),
+ BABEL_RUNTIME: dir('@babel/runtime'),
+ ESBUILD: dir('esbuild'),
+ REACT_FREEZE: dir('react-freeze'),
+ REANIMATED: { '4.7': dir('react-native-reanimated'), '4.5': dir('react-native-reanimated-4.5') },
+ // realpath: /tmp is /private/tmp on macOS
+ UNISTYLES: process.env.UNISTYLES_DIR ? fs.realpathSync(path.resolve(process.env.UNISTYLES_DIR)) : dir('react-native-unistyles'),
+}
diff --git a/test/harness/rn.cjs b/test/harness/rn.cjs
new file mode 100644
index 0000000..aeff856
--- /dev/null
+++ b/test/harness/rn.cjs
@@ -0,0 +1,162 @@
+// React Native's own components in Node, built with RN's Babel preset and Metro's platform resolution.
+// require('./rn.cjs')({ platform, mode }), one platform per process.
+const fs = require('fs')
+const os = require('os')
+const path = require('path')
+const crypto = require('crypto')
+const Module = require('module')
+
+const { RN, RN_VERSION, REACT, SCHEDULER, BABEL_PRESET: PRESET_DIR, BABEL_CORE: BABEL } = require('./paths.cjs')
+
+module.exports = ({ platform = 'ios', mode = 'development' } = {}) => {
+ process.env.NODE_ENV = mode
+ // import cycles in RN probe `then` on half-initialized exports: noise
+ const emitWarning = process.emitWarning
+ process.emitWarning = (warning, ...args) =>
+ String(warning).includes('inside circular dependency') ? undefined : emitWarning.call(process, warning, ...args)
+ globalThis.__DEV__ = mode === 'development'
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true
+ globalThis.RN$Bridgeless = true
+ globalThis.window = globalThis
+
+ const { ops } = require('./stubs.cjs').install()
+ installNativeModules(platform)
+
+ const babel = require(BABEL)
+ const preset = require(PRESET_DIR)
+ const cacheDir = path.join(os.tmpdir(), 'unifiber-rn-cache', `${RN_VERSION}-${platform}-${mode}`)
+ fs.mkdirSync(cacheDir, { recursive: true })
+
+ const isRNSource = (file) =>
+ (file.startsWith(RN + path.sep) || file.includes(`${path.sep}@react-native${path.sep}`)) &&
+ !file.includes(`${path.sep}Renderer${path.sep}implementations${path.sep}`) &&
+ !file.includes(`${path.sep}node_modules${path.sep}@babel${path.sep}`)
+
+ const originalJs = Module._extensions['.js']
+ Module._extensions['.js'] = function (module, filename) {
+ if (!isRNSource(filename)) {
+ return originalJs(module, filename)
+ }
+ const source = fs.readFileSync(filename, 'utf8')
+ const key = crypto.createHash('sha1').update(filename).update(source).digest('hex')
+ const cached = path.join(cacheDir, `${key}.js`)
+ let code
+ if (fs.existsSync(cached)) {
+ code = fs.readFileSync(cached, 'utf8')
+ } else {
+ code = babel.transformSync(source, {
+ filename,
+ babelrc: false,
+ configFile: false,
+ compact: false,
+ presets: [[preset, { dev: mode === 'development', disableImportExportTransform: false, unstable_transformProfile: 'hermes-stable' }]],
+ plugins: [[require(path.join(PRESET_DIR, '..', '..', '@babel', 'plugin-transform-modules-commonjs')), { lazy: false }]].filter(() => false),
+ }).code
+ // atomic: parallel test processes share this cache
+ const temp = `${cached}.${process.pid}.tmp`
+ fs.writeFileSync(temp, code)
+ fs.renameSync(temp, cached)
+ }
+ module._compile(code, filename)
+ }
+
+ const originalResolve = Module._resolveFilename
+ const exts = [`.${platform}.js`, '.native.js', '.js', `.${platform}.ts`, '.ts', '.json']
+ Module._resolveFilename = function (request, parent, isMain, options) {
+ if (request === 'react' || request.startsWith('react/')) {
+ return originalResolve.call(this, request.replace(/^react/, REACT), parent, isMain, options)
+ }
+ if (request === 'scheduler') {
+ return mode === 'development' ? path.join(SCHEDULER, 'index.js') : path.join(SCHEDULER, 'unstable_mock.js')
+ }
+ if (/\.(png|jpg|gif)$/.test(request)) {
+ return path.join(__dirname, 'asset-stub.cjs')
+ }
+ // RN <= 0.88 path, then 0.89+
+ if (request === 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' || request === 'react-native/setup-env') {
+ return path.join(__dirname, 'initialize-core-stub.cjs')
+ }
+ const base = request === 'react-native' ? path.join(RN, 'index') : request.startsWith('react-native/') ? path.join(RN, request.slice('react-native/'.length)) : null
+ if (base || request.startsWith('.')) {
+ const from = base ?? path.resolve(path.dirname(parent?.filename ?? __filename), request)
+ for (const ext of exts) {
+ if (fs.existsSync(from + ext)) return from + ext
+ }
+ for (const ext of exts) {
+ if (fs.existsSync(path.join(from, 'index' + ext))) return path.join(from, 'index' + ext)
+ }
+ }
+ return originalResolve.call(this, request, parent, isMain, options)
+ }
+
+ const React = require(REACT)
+ const RNIndex = require(path.join(RN, 'index'))
+ const productionQueue = require('./flush.cjs')()
+ const ReactFabric = productionQueue.load(() => require(path.join(RN, 'Libraries', 'Renderer', 'shims', 'ReactFabric')))
+ const ReactFabricImpl = ReactFabric.default ?? ReactFabric
+
+ let nextRootTag = 1
+ const render = (element, rootTag = nextRootTag++) => {
+ if (mode === 'development') {
+ React.act(() => {
+ ReactFabricImpl.render(element, rootTag, null, true)
+ })
+ } else {
+ ReactFabricImpl.render(element, rootTag, null, true)
+ productionQueue.flush()
+ }
+ return rootTag
+ }
+
+ return { React, RN: RNIndex, render, act: React.act, ops, platform, rnVersion: require(path.join(RN, 'package.json')).version }
+}
+
+// every native module exists: methods are no-ops, constants are what RN reads at import
+function installNativeModules(platform) {
+ const window = { width: 390, height: 844, scale: 3, fontScale: 1 }
+ const constants = {
+ PlatformConstants: {
+ isTesting: true,
+ isDisableAnimations: true,
+ reactNativeVersion: { major: 0, minor: 87, patch: 1, prerelease: null },
+ forceTouchAvailable: false,
+ osVersion: platform === 'ios' ? '27.0' : 36,
+ systemName: platform === 'ios' ? 'iOS' : 'Android',
+ interfaceIdiom: 'phone',
+ Version: 36,
+ Release: '16',
+ Serial: 'unknown',
+ Fingerprint: 'unifiber',
+ Model: 'unifiber',
+ uiMode: 'normal',
+ Brand: 'unifiber',
+ Manufacturer: 'unifiber',
+ },
+ DeviceInfo: { Dimensions: { window, screen: window, windowPhysicalPixels: window, screenPhysicalPixels: window } },
+ I18nManager: { isRTL: false, doLeftAndRightSwapInRTL: true, localeIdentifier: 'en_US' },
+ StatusBarManager: { HEIGHT: 47, DEFAULT_BACKGROUND_COLOR: 0 },
+ SourceCode: { scriptURL: 'http://localhost:8081/index.bundle' },
+ Appearance: {},
+ }
+ const stub = (name) =>
+ new Proxy(
+ {},
+ {
+ get(target, key) {
+ if (key === 'getConstants') return () => constants[name] ?? {}
+ if (key === 'then') return undefined
+ if (typeof key === 'symbol') return undefined
+ if (constants[name] && key in constants[name]) return constants[name][key]
+ if (key === 'getColorScheme') return () => 'light'
+ if (key === 'isReduceMotionEnabled' || key === 'isScreenReaderEnabled') return (cb) => cb?.(false)
+ return target[key] ?? (target[key] = () => undefined)
+ },
+ },
+ )
+ const modules = new Map()
+ const get = (name) => (modules.has(name) ? modules.get(name) : (modules.set(name, stub(name)), modules.get(name)))
+ globalThis.__turboModuleProxy = get
+ globalThis.nativeModuleProxy = new Proxy({}, { get: (_, name) => (typeof name === 'string' ? get(name) : undefined) })
+ globalThis.nativeFabricUIManager.getRoot ??= () => null
+ globalThis.__nativeComponentRegistry__hasComponent = () => true
+}
diff --git a/test/harness/stubs.cjs b/test/harness/stubs.cjs
new file mode 100644
index 0000000..8bb125b
--- /dev/null
+++ b/test/harness/stubs.cjs
@@ -0,0 +1,100 @@
+// Fake nativeFabricUIManager and private interface for the Fabric renderer. No Node APIs: also bundled for Hermes.
+const install = () => {
+ let nodeCount = 0
+ const ops = [] // clone log, so a test can see what React diffed
+ const committed = new Map() // rootTag -> children of the last completeRoot
+ const node = (family, props, children) => ({ id: ++nodeCount, family, props, children, promoted: false })
+ const merge = (props, payload) => (payload ? { ...props, ...payload } : props)
+
+ // unknown methods are no-ops, so newer renderers (eg. RN 0.89's View Transitions) still run
+ const lenient = (target) =>
+ new Proxy(target, {
+ get: (object, key) => (key in object || typeof key === 'symbol' || key === 'then' ? object[key] : () => undefined),
+ })
+
+ globalThis.nativeFabricUIManager = {
+ createNode: (tag, viewName, rootTag, props, instanceHandle) =>
+ node({ tag, viewName, rootTag, instanceHandle }, props, []),
+ cloneNodeWithNewChildren: (n) => node(n.family, n.props, []),
+ cloneNodeWithNewProps: (n, payload) => (ops.push({ op: 'props', tag: n.family.tag, payload }), node(n.family, merge(n.props, payload), n.children)),
+ cloneNodeWithNewChildrenAndProps: (n, payload) => (ops.push({ op: 'childrenAndProps', tag: n.family.tag, payload }), node(n.family, merge(n.props, payload), [])),
+ createChildSet: () => [],
+ appendChild: (parent, child) => parent.children.push(child),
+ appendChildToSet: (set, child) => set.push(child),
+ completeRoot: (rootTag, set) => {
+ const promote = (n) => {
+ n.promoted = true
+ n.children.forEach(promote)
+ }
+ set.forEach(promote)
+ committed.set(rootTag, set)
+ },
+ registerEventHandler: () => {},
+ unstable_DiscreteEventPriority: 2,
+ unstable_ContinuousEventPriority: 8,
+ unstable_IdleEventPriority: 536870912,
+ unstable_getCurrentEventPriority: () => 32,
+ measure: () => {},
+ setIsJSResponder: () => {},
+ dispatchCommand: () => {},
+ sendAccessibilityEvent: () => {},
+ findNodeAtPoint: () => {},
+ }
+
+ const viewConfig = (name) => ({ uiViewClassName: name, validAttributes: { style: true, collapsable: true, testID: true, nativeID: true } })
+ const flattenStyle = (style) => (Array.isArray(style) ? Object.assign({}, ...style.flat(Infinity).map(flattenStyle)) : style ?? undefined)
+ const payload = (props) => {
+ const out = {}
+ for (const key in props) {
+ if (key !== 'children' && typeof props[key] !== 'function') {
+ out[key] = key === 'style' ? flattenStyle(props[key]) : props[key]
+ }
+ }
+ return out
+ }
+ class ReactNativeElement {
+ constructor(tag, config, internalInstanceHandle) {
+ this.__nativeTag = tag
+ this.__viewConfig = config
+ this.__internalInstanceHandle = internalInstanceHandle
+ }
+ }
+ const privateInterface = {
+ ReactNativeViewConfigRegistry: { get: viewConfig, register: () => {} },
+ createAttributePayload: payload,
+ diffAttributePayloads: (prev, next) => {
+ const a = payload(prev)
+ const b = payload(next)
+ const diff = {}
+ let changed = false
+ for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
+ if (JSON.stringify(a[key]) !== JSON.stringify(b[key])) {
+ diff[key] = b[key] ?? null
+ changed = true
+ }
+ }
+ return changed ? diff : null
+ },
+ createPublicInstance: (tag, config, handle) => new ReactNativeElement(tag, config, handle),
+ createPublicRootInstance: (rootTag) => ({ rootTag }),
+ createPublicTextInstance: (handle) => ({ __internalInstanceHandle: handle }),
+ getNativeTagFromPublicInstance: (i) => i.__nativeTag,
+ getNodeFromPublicInstance: (i) => i.__internalInstanceHandle?.stateNode?.node,
+ getInternalInstanceHandleFromPublicInstance: (i) => i.__internalInstanceHandle,
+ deepFreezeAndThrowOnMutationInDev: () => {},
+ legacySendAccessibilityEvent: () => {},
+ RawEventEmitter: { emit: () => {} },
+ ReactFiberErrorDialog: { showErrorDialog: () => true },
+ UIManager: {},
+ }
+
+ globalThis.nativeFabricUIManager = lenient(globalThis.nativeFabricUIManager)
+ // RN 0.89+ reads the UI manager from the private interface
+ privateInterface.fabricUIManager = globalThis.nativeFabricUIManager
+ const lenientInterface = lenient(privateInterface)
+
+ globalThis.__unifiberPrivateInterface = lenientInterface
+ return { privateInterface: lenientInterface, ops, committed }
+}
+
+module.exports = { install }
diff --git a/test/harness/unistyles.cjs b/test/harness/unistyles.cjs
new file mode 100644
index 0000000..d7346f0
--- /dev/null
+++ b/test/harness/unistyles.cjs
@@ -0,0 +1,149 @@
+// Unistyles' real wrappers and registry JS, after rn.cjs. Stubbed: Nitro (records native calls), StyleSheet's listener.
+const fs = require('fs')
+const os = require('os')
+const path = require('path')
+const crypto = require('crypto')
+const Module = require('module')
+
+const { UNISTYLES, BABEL_PRESET: PRESET_DIR, BABEL_CORE: BABEL, BABEL_RUNTIME } = require('./paths.cjs')
+const SRC = path.join(UNISTYLES, 'src')
+
+module.exports = ({ platform = 'ios', mode = 'development' } = {}) => {
+ const babel = require(BABEL)
+ const preset = require(PRESET_DIR)
+ const cacheDir = path.join(os.tmpdir(), 'unifiber-unistyles-cache', crypto.createHash('sha1').update(SRC).digest('hex').slice(0, 8), `${platform}-${mode}`)
+ fs.mkdirSync(cacheDir, { recursive: true })
+
+ const calls = []
+ let scopedTheme
+ let committedHandles = []
+ const registry = {
+ link: (node, styles) => calls.push({ op: 'link', node, styles }),
+ unlink: (node) => calls.push({ op: 'unlink', node }),
+ suspend: (node) => calls.push({ op: 'suspend', node }),
+ flush: () => {},
+ setScopedTheme: (name) => (scopedTheme = name),
+ getScopedTheme: () => scopedTheme,
+ verify: () => ({}),
+ takeCommittedInstanceHandles: () => {
+ const handles = committedHandles
+ committedHandles = []
+ return handles
+ },
+ refreshReactNodes: (nodes) => calls.push({ op: 'refreshReactNodes', nodes }),
+ }
+ const changeListeners = []
+ const stubs = {
+ nitro: { NitroModules: { createHybridObject: (name) => (name === 'UnistylesShadowRegistry' ? registry : {}) } },
+ styleSheet: { StyleSheet: { addChangeListener: (fn) => (changeListeners.push(fn), () => {}) } },
+ }
+ globalThis.__unifiberUnistylesStubs = stubs
+
+ const stubFile = (name, code) => {
+ const file = path.join(cacheDir, `${name}.cjs`)
+ fs.writeFileSync(file, code)
+ return file
+ }
+ const NITRO = stubFile('nitro', 'module.exports = globalThis.__unifiberUnistylesStubs.nitro\n')
+ const STYLESHEET = stubFile('stylesheet', 'module.exports = globalThis.__unifiberUnistylesStubs.styleSheet\n')
+ const SPECS = stubFile('specs', `module.exports = { UnistylesShadowRegistry: require(${JSON.stringify(path.join(SRC, 'specs', 'ShadowRegistry', 'index.ts'))}).UnistylesShadowRegistry }\n`)
+ const CORE = stubFile(
+ 'core',
+ `const core = ${JSON.stringify(path.join(SRC, 'core'))}
+module.exports = {
+ createUnistylesElement: require(core + '/createUnistylesElement').createUnistylesElement,
+ createUnistylesActivityIndicator: require(core + '/createUnistylesActivityIndicator').createUnistylesActivityIndicator,
+ createUnistylesImageBackground: require(core + '/createUnistylesImageBackground').createUnistylesImageBackground,
+ createUnistylesTouchableHighlight: require(core + '/createUnistylesTouchableHighlight').createUnistylesTouchableHighlight,
+ passForwardedRef: require(core + '/passForwardRef').passForwardedRef,
+}
+`,
+ )
+
+ const inUnistyles = (file) => typeof file === 'string' && file.startsWith(SRC + path.sep)
+ const exts = [`.${platform}.tsx`, '.native.tsx', '.tsx', `.${platform}.ts`, '.native.ts', '.ts']
+
+ const originalResolve = Module._resolveFilename
+ Module._resolveFilename = function (request, parent, isMain, options) {
+ if (request === 'react-native-nitro-modules') return NITRO
+ if (request.startsWith('@babel/runtime/')) return require.resolve(path.join(BABEL_RUNTIME, request.slice('@babel/runtime/'.length)))
+ const fromUnistyles = inUnistyles(parent?.filename) && request.startsWith('.')
+ if (fromUnistyles || (path.isAbsolute(request) && request.startsWith(SRC + path.sep) && !path.extname(request))) {
+ const target = fromUnistyles ? path.resolve(path.dirname(parent.filename), request) : request
+ if (target === path.join(SRC, 'specs')) return SPECS
+ if (target === path.join(SRC, 'core')) return CORE
+ if (target === path.join(SRC, 'specs', 'StyleSheet')) return STYLESHEET
+ for (const ext of exts) {
+ if (fs.existsSync(target + ext)) return target + ext
+ }
+ for (const ext of exts) {
+ if (fs.existsSync(path.join(target, 'index' + ext))) return path.join(target, 'index' + ext)
+ }
+ }
+ return originalResolve.call(this, request, parent, isMain, options)
+ }
+
+ const transform = (originalLoader) =>
+ function (module, filename) {
+ if (!inUnistyles(filename)) {
+ return originalLoader.call(this, module, filename)
+ }
+ const source = fs.readFileSync(filename, 'utf8')
+ const key = crypto.createHash('sha1').update(filename).update(source).digest('hex')
+ const cached = path.join(cacheDir, `${key}.js`)
+ let code
+ if (fs.existsSync(cached)) {
+ code = fs.readFileSync(cached, 'utf8')
+ } else {
+ code = babel.transformSync(source, {
+ filename,
+ babelrc: false,
+ configFile: false,
+ presets: [[preset, { dev: mode === 'development' }]],
+ }).code
+ // atomic: parallel test processes share this cache
+ const temp = `${cached}.${process.pid}.tmp`
+ fs.writeFileSync(temp, code)
+ fs.renameSync(temp, cached)
+ }
+ module._compile(code, filename)
+ }
+ Module._extensions['.tsx'] = transform(Module._extensions['.js'])
+ Module._extensions['.ts'] = transform(Module._extensions['.ts'] ?? Module._extensions['.js'])
+
+ const native = (name) => require(path.join(SRC, 'components', 'native', name))
+ const components = {
+ View: native('View').View,
+ Text: native('Text').Text,
+ Image: native('Image').Image,
+ ImageBackground: native('ImageBackground').ImageBackground,
+ TextInput: native('TextInput').TextInput,
+ Switch: native('Switch').Switch,
+ ScrollView: native('ScrollView').ScrollView,
+ FlatList: native('FlatList').FlatList,
+ SectionList: native('SectionList').SectionList,
+ VirtualizedList: native('VirtualizedList').VirtualizedList,
+ KeyboardAvoidingView: native('KeyboardAvoidingView').KeyboardAvoidingView,
+ RefreshControl: native('RefreshControl').RefreshControl,
+ ActivityIndicator: native('ActivityIndicator').ActivityIndicator,
+ Pressable: native('Pressable').Pressable,
+ TouchableOpacity: native('TouchableOpacity').TouchableOpacity,
+ TouchableHighlight: native('TouchableHighlight').TouchableHighlight,
+ SafeAreaView: native('SafeAreaView').SafeAreaView,
+ NativeView: native('NativeView').NativeView,
+ NativeText: native('NativeText').NativeText,
+ Animated: native('Animated').Animated,
+ }
+ const ScopedThemeContext = require(path.join(SRC, 'components', 'ScopedThemeContext')).ScopedThemeContext
+
+ return {
+ components,
+ ScopedThemeContext,
+ calls,
+ // what C++ would hand back after a native commit (UnistylesRegistry::takeCommittedInstanceHandles)
+ setCommittedHandles: (handles) => (committedHandles = handles),
+ // StyleSheet's change listener: the real refreshReactNodes
+ runChangeListeners: () => changeListeners.forEach((listener) => listener()),
+ source: UNISTYLES,
+ }
+}
diff --git a/test/hidden.test.ts b/test/hidden.test.ts
new file mode 100644
index 0000000..407f676
--- /dev/null
+++ b/test/hidden.test.ts
@@ -0,0 +1,164 @@
+// Hidden predicates through state sequences, on a Host kept from link time and on the other fiber half.
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { Fiber } from '../src/index'
+import { HiddenBy, getHiddenReasons, type Host } from '../src/host'
+
+const hostOfFiber = (fiber: unknown) => fiber as Host
+
+const require = createRequire(import.meta.url)
+const mode = process.env.UNIFIBER_MODE ?? 'development'
+const { React, render, act, View } = require('./harness/fabric.cjs')({ mode })
+const h = React.createElement
+const rerender = (element: any, tag: number) => (mode === 'development' ? act(() => render(element, tag)) : render(element, tag))
+const { Freeze } = require(require('./harness/paths.cjs').REACT_FREEZE)
+
+
+const pending = { then() {} }
+const Gate = ({ suspend, children }: any) => {
+ if (suspend) throw pending
+ return children
+}
+
+type State = { suspend: boolean; freeze: boolean; activity: 'visible' | 'hidden'; n: number }
+const reasonsOf = (s: State, innerSuspended: boolean) =>
+ (innerSuspended ? HiddenBy.Suspense : 0) | (s.freeze ? HiddenBy.Freeze : 0) | (s.activity === 'hidden' ? HiddenBy.Activity : 0)
+
+// `n` re-renders the parent so fiber halves keep swapping
+const app = (s: State, ref: any) =>
+ h(View, { testID: `root-${s.n}` },
+ h(React.Activity, { mode: s.activity },
+ h(Freeze, { freeze: s.freeze },
+ h(React.Suspense, { fallback: null },
+ h(Gate, { suspend: s.suspend }, h(View, { ref, testID: 'leaf' }))))))
+
+const run = (steps: Array>) => {
+ let instance: any = null
+ const ref = (r: any) => {
+ if (r) instance = r
+ }
+ let state: State = { suspend: false, freeze: false, activity: 'visible', n: 0 }
+ const tag = render(app(state, ref))
+ const host = Fiber.getHostFor(instance)!
+ const creation = host as any
+ const failures: Array = []
+ // while frozen, the Suspense inside Freeze never re-renders: it keeps its last committed state
+ let innerSuspended = false
+
+ for (const [i, step] of steps.entries()) {
+ state = { ...state, ...step, n: state.n + 1 }
+ rerender(app(state, ref), tag)
+ innerSuspended = state.freeze ? innerSuspended : state.suspend
+ const expected = reasonsOf(state, innerSuspended)
+ const halves = [['kept Host', host], ['alternate', creation.alternate && hostOfFiber(creation.alternate)]] as const
+
+ for (const [name, h] of halves) {
+ if (!h) continue
+ const got = getHiddenReasons(h)
+ if (got !== expected) failures.push(`step ${i} ${JSON.stringify(step)} ${name}: got ${got}, expected ${expected}`)
+ }
+ }
+
+ return failures
+}
+
+test('Suspense: fallback on / off, parent re-rendering in between', () => {
+ assert.deepEqual(run([{ suspend: true }, {}, { suspend: false }, {}, { suspend: true }, { suspend: false }]), [])
+})
+
+test('Activity: hidden / visible, parent re-rendering in between', () => {
+ assert.deepEqual(run([{ activity: 'hidden' }, {}, { activity: 'visible' }, {}, { activity: 'hidden' }, { activity: 'visible' }]), [])
+})
+
+test('react-freeze: frozen / thawed, reported as Freeze not Suspense', () => {
+ assert.deepEqual(run([{ freeze: true }, {}, { freeze: false }, {}, { freeze: true }, { freeze: false }]), [])
+})
+
+test('nested: frozen screen inside a hidden Activity, then each released', () => {
+ assert.deepEqual(run([{ freeze: true }, { activity: 'hidden' }, { freeze: false }, { activity: 'visible' }, { suspend: true }, { freeze: true }, { suspend: false }, { freeze: false }]), [])
+})
+
+test('predicates', () => {
+ let instance: any = null
+ const ref = (r: any) => {
+ if (r) instance = r
+ }
+ const tag = render(app({ suspend: false, freeze: false, activity: 'visible', n: 0 }, ref))
+ const host = Fiber.getHostFor(instance)!
+
+ assert.deepEqual([Fiber.isHidden(host), Fiber.isSuspended(host), Fiber.isFrozen(host), Fiber.isInHiddenActivity(host)], [false, false, false, false])
+ rerender(app({ suspend: false, freeze: true, activity: 'visible', n: 1 }, ref), tag)
+ assert.deepEqual([Fiber.isHidden(host), Fiber.isSuspended(host), Fiber.isFrozen(host), Fiber.isInHiddenActivity(host)], [true, false, true, false])
+})
+
+// Unistyles ShadowRegistry/index.ts:31-44, Uniwind utils/shadowNode.ts:7-19
+const unistylesToday = (fiber: any) => {
+ for (let c = fiber?.return; c; c = c.return) if ((c.tag === 13 || c.tag === 22) && c.memoizedState !== null) return true
+ return false
+}
+const uniwindToday = (fiber: any) => {
+ for (let c = fiber?.return; c; c = c.return) if (c.tag === 13 && c.memoizedState !== null) return true
+ return false
+}
+
+test('today: after a screen is frozen and thawed, the creation fiber still reads as hidden', () => {
+ let instance: any = null
+ const ref = (r: any) => {
+ if (r) instance = r
+ }
+ const seen: Array = []
+ let state: State = { suspend: false, freeze: false, activity: 'visible', n: 0 }
+ const tag = render(app(state, ref))
+ const creation = instance.__internalInstanceHandle
+ const host = Fiber.getHostFor(instance)!
+
+ for (const freeze of [true, false, true, false]) {
+ state = { ...state, freeze, n: state.n + 1 }
+ rerender(app(state, ref), tag)
+ seen.push(`${freeze ? 'frozen' : 'thawed'}: unistyles ${unistylesToday(creation)}, uniwind ${uniwindToday(creation)}, host ${Fiber.isHidden(host)}`)
+ }
+ console.log('# ' + seen.join('\n# '))
+ assert.equal(Fiber.isHidden(host), false)
+})
+
+test('identity: the same view is the same Host, from a ref or from a walk, after any number of re-renders', () => {
+ let leaf: any = null
+ let root: any = null
+ const app = (n: number) =>
+ h(View, { ref: (r: any) => r && (root = r), testID: `root-${n}` }, h(View, { ref: (r: any) => r && (leaf = r), testID: `leaf-${n}` }))
+ const tag = render(app(0))
+ const host = Fiber.getHostFor(leaf)!
+ const parent = Fiber.getHostFor(root)!
+ const seen = new WeakMap([[host, 'leaf']])
+
+ for (let n = 1; n <= 4; n++) {
+ rerender(app(n), tag)
+ const walked = (host as any).alternate
+ assert.equal(Fiber.getParentHost(hostOfFiber(walked)), parent)
+ assert.equal(Fiber.getParentHost(host), parent)
+ assert.equal(Fiber.getHostFor(leaf), host)
+ assert.equal(seen.get(Fiber.getHostFor(leaf)!), 'leaf')
+ }
+})
+
+test('getHostFor: a class that renders its host (Button on Android), and handles from C++, give the same Host', () => {
+ class Touchable extends React.Component {
+ render() {
+ return h(View, { testID: `button-${this.props.n}`, ref: this.props.hostRef })
+ }
+ }
+ let button: any = null
+ let host: any = null
+ const app = (n: number) => h(View, null, h(Touchable, { n, ref: (r: any) => r && (button = r), hostRef: (r: any) => r && (host = r) }))
+ const tag = render(app(0))
+
+ for (let n = 1; n <= 3; n++) {
+ rerender(app(n), tag)
+ const expected = Fiber.getHostFor(host)!
+ assert.equal(Fiber.getHostFor(button), expected)
+ assert.equal(Fiber.getHostForInstanceHandle((expected as any).alternate), expected)
+ assert.equal(Fiber.getNativeTag(expected), host.__nativeTag)
+ assert.equal(Fiber.getShadowNodeFor(button), Fiber.getShadowNode(expected))
+ }
+})
diff --git a/test/malformed.test.ts b/test/malformed.test.ts
new file mode 100644
index 0000000..9bc4f6d
--- /dev/null
+++ b/test/malformed.test.ts
@@ -0,0 +1,62 @@
+// A fiber with missing fields gives undefined / null / false for that call, never a throw, and nothing else is affected.
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import * as U from '../src/fiber'
+import { Fiber, Reanimated, type Host } from '../src/index'
+
+const require = createRequire(import.meta.url)
+const { React, render, View } = require('./harness/fabric.cjs')({ mode: process.env.UNIFIBER_MODE ?? 'development' })
+const h = React.createElement
+
+const malformed = [
+ {},
+ { tag: 5 },
+ { tag: 5, stateNode: null },
+ { tag: 5, stateNode: {} },
+ { tag: 5, stateNode: { node: {} } },
+ { tag: 5, stateNode: { node: {}, canonical: {} }, return: { tag: 22, memoizedState: {}, return: null } },
+]
+
+test('malformed fibers: every query answers without throwing', () => {
+ for (const fiber of malformed) {
+ const ref = { __internalInstanceHandle: fiber }
+ const host = fiber as unknown as Host
+ const visited: Array = []
+
+ assert.equal(Fiber.getHostFor(ref), fiber, 'a ref is resolved without inspecting the fiber')
+ assert.doesNotThrow(() => Fiber.getShadowNodeFor(ref))
+ assert.equal(Fiber.getNativeTag(host), undefined)
+ assert.equal(Fiber.getParentHost(host), undefined)
+ assert.equal(Fiber.getHostForInstanceHandle(fiber), undefined)
+ assert.doesNotThrow(() => Fiber.getShadowNodes(host))
+ assert.doesNotThrow(() => Fiber.isHidden(host))
+ assert.doesNotThrow(() => Fiber.collectVisibleAncestors([host], (x) => visited.push(x)))
+ assert.doesNotThrow(() => Fiber.collectAncestors([host], (x) => visited.push(x)))
+ assert.deepEqual(visited, [], 'no canonical: nothing to refresh')
+ assert.doesNotThrow(() => Fiber.verify(host, { opacity: 1 }))
+ assert.deepEqual(Fiber.getOwners(host), [])
+ assert.equal(Fiber.patchRenderedProps(host, (props) => props), false)
+ assert.equal(Reanimated.animatedComponentOf(host), null)
+ assert.equal(U.committedOf(fiber as any), null)
+ assert.equal(U.readContext(fiber as any, React.createContext('x'), 'fallback'), 'fallback')
+ }
+
+ assert.equal(Fiber.getHostFor(null), undefined)
+ assert.equal(Fiber.getHostFor({}), undefined)
+ assert.equal(Fiber.getShadowNodeFor(undefined), undefined)
+ assert.equal(U.readContextDuringRender(null as any), undefined)
+})
+
+test('a malformed fiber switches nothing off: real views keep working afterwards', () => {
+ Fiber.getHostFor({ __internalInstanceHandle: { tag: 5, stateNode: {} } })
+
+ let ref: any = null
+ render(h(View, null, h(View, { ref: (r: any) => r && (ref = r) })))
+ const host = Fiber.getHostFor(ref)!
+
+ assert.equal(typeof Fiber.getShadowNode(host), 'object')
+ assert.equal(typeof Fiber.getNativeTag(host), 'number')
+ assert.ok(Fiber.getParentHost(host))
+ assert.equal(Fiber.isHidden(host), false)
+})
diff --git a/test/metro-inline-requires.test.ts b/test/metro-inline-requires.test.ts
new file mode 100644
index 0000000..db45760
--- /dev/null
+++ b/test/metro-inline-requires.test.ts
@@ -0,0 +1,62 @@
+// With Metro's inlineRequires (RN's default), an import used inside a function costs a require() per call.
+// Transforms src/ like metro-transform-worker and fails on any require() left inside a function.
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import fs from 'node:fs'
+import path from 'node:path'
+
+const require = createRequire(import.meta.url)
+const { ROOT, BABEL_CORE, BABEL_PRESET } = require('./harness/paths.cjs')
+const babel = require(BABEL_CORE)
+const preset = require(BABEL_PRESET)
+const { inlineRequiresPlugin } = require('metro-transform-plugins')
+const generateImportNames = require('metro/private/ModuleGraph/worker/generateImportNames').default
+
+const sources = (dir: string): Array =>
+ fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+ const file = path.join(dir, entry.name)
+ return entry.isDirectory() ? sources(file) : file.endsWith('.ts') ? [file] : []
+ })
+
+const requiresInsideFunctions = (file: string) => {
+ const transformed = babel.transformSync(fs.readFileSync(file, 'utf8'), {
+ filename: file,
+ babelrc: false,
+ configFile: false,
+ ast: true,
+ code: false,
+ presets: [[preset, { dev: false }]],
+ })
+ const { importDefault, importAll } = generateImportNames(transformed.ast)
+ const { ast } = babel.transformFromAstSync(transformed.ast, '', {
+ filename: file,
+ babelrc: false,
+ configFile: false,
+ ast: true,
+ code: false,
+ plugins: [[inlineRequiresPlugin, { inlineableCalls: [importDefault, importAll] }]],
+ })
+ const found: Array = []
+
+ babel.traverse(ast, {
+ CallExpression(callPath: any) {
+ if (!callPath.get('callee').isIdentifier({ name: 'require' })) {
+ return
+ }
+ const fn = callPath.getFunctionParent()
+ if (fn) {
+ const name = fn.node.id?.name ?? fn.parentPath?.node?.id?.name ?? fn.parentPath?.node?.key?.name ?? '(anonymous)'
+ found.push(`${path.relative(ROOT, file)}: ${name}() requires '${callPath.node.arguments[0]?.value}' on every call`)
+ }
+ },
+ })
+
+ return found
+}
+
+test('no src function body pays an inline require (Metro inlineRequires: true, RN default)', () => {
+ const violations = sources(path.join(ROOT, 'src')).flatMap(requiresInsideFunctions)
+
+ assert.deepEqual(violations, [])
+})
diff --git a/test/reanimated-adapter.test.ts b/test/reanimated-adapter.test.ts
new file mode 100644
index 0000000..5da2e4b
--- /dev/null
+++ b/test/reanimated-adapter.test.ts
@@ -0,0 +1,282 @@
+// Reanimated CSS adapter on the real renderer, with a stand-in copying Reanimated 4.7's AnimatedComponent lifecycle.
+import { test, beforeEach } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { Fiber, Reanimated, type Host } from '../src/index'
+import { reanimatedCSSAdapter } from '../src/adapters/reanimated'
+import { createOwnerRegistry, type StyleSource } from '../src/adapters/registry'
+
+const require = createRequire(import.meta.url)
+const { React, render, View } = require('./harness/fabric.cjs')({ mode: process.env.UNIFIBER_MODE ?? 'development' })
+const h = React.createElement
+
+
+class FakeCSSManager {
+ previous: Record = {}
+ started: Array<[string, unknown, unknown]> = []
+ update(style: Record, _props?: unknown) {
+ const properties = style.transitionProperty as Array | undefined
+ for (const key of properties ?? []) {
+ if (this.previous[key] !== undefined && this.previous[key] !== style[key]) {
+ this.started.push([key, this.previous[key], style[key]])
+ }
+ }
+ this.previous = { ...style }
+ }
+}
+const managers: Array = []
+
+class AnimatedComponentBase extends React.Component {
+ _CSSManager?: FakeCSSManager
+ _cssStyle: Record = {}
+ _componentRef: any = null
+ // 4.7 css/component/AnimatedComponent.native.tsx: forwards, then keeps the ref it animates
+ _setComponentRef = (ref: any) => {
+ const forwarded = this.props.forwardedRef
+ if (typeof forwarded === 'function') forwarded(ref)
+ if (ref && ref !== this._componentRef) this._componentRef = ref
+ }
+ _updateStyles(props: any) {
+ this._cssStyle = Object.assign({}, ...[props.style].flat(Infinity).filter(Boolean))
+ }
+ componentDidMount() {
+ this._updateStyles(this.props)
+ this._CSSManager ??= new FakeCSSManager()
+ managers.push(this._CSSManager)
+ this._CSSManager.update(this._cssStyle, this.props)
+ }
+ shouldComponentUpdate(nextProps: any) {
+ this._updateStyles(nextProps)
+ this._CSSManager?.update(this._cssStyle, nextProps)
+ return true
+ }
+ render() {
+ const Child = this.props.child ?? UnistylesView
+ return h(Child, { ...this.props, collapsable: false, ref: this._setComponentRef })
+ }
+}
+// createAnimatedComponent: overrides _updateStyles to split shared-value styles out
+class AnimatedView extends AnimatedComponentBase {
+ _updateStyles(props: any) {
+ const flat = Object.assign({}, ...[props.style].flat(Infinity).filter(Boolean))
+ for (const key in flat) {
+ if (key.startsWith('unistyles_')) delete flat[key]
+ }
+ this._cssStyle = flat
+ }
+}
+
+// Unistyles stand-in: snapshot styles, a hidden getter, scoped theme through context
+const themes: Record = { light: { bg: 'white' }, dark: { bg: 'black' }, red: { bg: 'red' } }
+let globalTheme = 'light'
+let epoch = 0
+let resolveCalls = 0
+const ScopedThemeContext = React.createContext(null)
+const makeStyle = (extra: Record = {}) => ({
+ backgroundColor: themes[globalTheme].bg, // snapshot, like HostUnistyle._cache
+ ...extra,
+ unistyles_abc: { uni__getStyles: (scope?: string) => ({ backgroundColor: themes[scope ?? globalTheme].bg, ...extra }) },
+})
+const source: StyleSource = {
+ name: 'unistyles',
+ epoch: () => (process.env.NO_CACHE ? ++epoch : epoch),
+ contexts: [ScopedThemeContext],
+ // stable scopes are renamed in place: key on the name, not the object
+ contextKey: (_context, value) => (process.env.NO_CONTEXT_KEY ? value : (value as { name?: string } | null)?.name),
+ resolve: (props, read) => {
+ resolveCalls++
+ const scope = read<{ name: string } | null>(ScopedThemeContext)?.name
+ const out: Record = {}
+ let ours = false
+ for (const style of [props.style].flat(Infinity)) {
+ if (!style) continue
+ Object.assign(out, style)
+ for (const key in style) {
+ if (key.startsWith('unistyles_')) {
+ ours = true
+ Object.assign(out, style[key].uni__getStyles(scope))
+ }
+ }
+ }
+ return ours ? out : undefined
+ },
+}
+const registry = createOwnerRegistry(source, process.env.NO_ADAPTER ? [] : [reanimatedCSSAdapter])
+const linked: Array = []
+const unistylesRef = (outer: any) => (r: any) => {
+ if (typeof outer === 'function') outer(r)
+ const fiber = Fiber.getHostFor(r)
+ if (fiber) {
+ linked.push(fiber)
+ registry.link(fiber)
+ }
+}
+function UnistylesView({ ref, ...props }: any) {
+ return h(View, { ...props, ref: unistylesRef(ref) })
+}
+const setTheme = (name: string) => {
+ globalTheme = name
+ epoch++
+ // native side updated the shadow tree; refresh only what that commit touched
+ registry.refresh(linked)
+}
+
+beforeEach(() => {
+ managers.length = 0
+ linked.length = 0
+ globalTheme = 'light'
+ resolveCalls = 0
+})
+
+const transition = { transitionProperty: ['backgroundColor'], transitionDuration: 300 }
+
+test('theme change without a React render starts the transition to the new value', () => {
+ render(h(AnimatedView, { style: makeStyle(transition) }))
+ setTheme('dark')
+
+ assert.deepEqual(managers[0].started, [['backgroundColor', 'white', 'black']])
+})
+
+test('a later React render with the stale snapshot does not transition back', () => {
+ const style = makeStyle(transition) // memoized: still says white after the flip
+ const tag = render(h(AnimatedView, { style }))
+ setTheme('dark')
+ render(h(AnimatedView, { style }), tag)
+
+ assert.deepEqual(managers[0].started, [['backgroundColor', 'white', 'black']])
+ assert.equal(managers[0].previous.backgroundColor, 'black')
+})
+
+test('second bug: a scoped theme reaches Reanimated at mount, not the global one', () => {
+ render(h(ScopedThemeContext, { value: { name: 'red' } }, h(AnimatedView, { style: makeStyle(transition) })))
+
+ assert.equal(managers[0].previous.backgroundColor, 'red')
+ setTheme('dark')
+ assert.deepEqual(managers[0].started, [], 'scope still wins after a global flip')
+})
+
+test('per-change cache: re-renders with the same style and epoch resolve once', () => {
+ const style = makeStyle(transition)
+ const tag = render(h(AnimatedView, { style }))
+ const afterMount = resolveCalls
+
+ for (let i = 0; i < 5; i++) render(h(AnimatedView, { style }), tag)
+ assert.equal(resolveCalls, afterMount)
+
+ setTheme('dark')
+ assert.equal(resolveCalls, afterMount + 1)
+})
+
+test('Animated views without CSS config are left to Reanimated', () => {
+ render(h(AnimatedView, { style: makeStyle() }))
+ setTheme('dark')
+
+ assert.equal(resolveCalls, 0)
+ assert.deepEqual(managers[0].started, [])
+})
+
+test('plain views never get past the gate', () => {
+ let claims = 0
+ const counting = createOwnerRegistry(source, [{ ...reanimatedCSSAdapter, claim: (f) => (claims++, reanimatedCSSAdapter.claim(f)) }])
+ const refs: Array = []
+ render(h(View, null, ...Array.from({ length: 50 }, (_, i) => h(View, { key: i, ref: (r: any) => refs.push(r) }))))
+ refs.forEach((r) => counting.link(Fiber.getHostFor(r)))
+
+ assert.equal(claims, 0)
+})
+
+test('collapsable={false} on someone else\'s class: claimed as nothing', () => {
+ class Other extends React.Component {
+ render() {
+ return h(View, { collapsable: false, ref: (r: any) => registry.link(Fiber.getHostFor(r)) })
+ }
+ }
+ assert.doesNotThrow(() => render(h(Other)))
+})
+
+test('an adapter that throws is switched off, the app keeps rendering', () => {
+ const reasons: Array = []
+ const broken = createOwnerRegistry(source, [{ ...reanimatedCSSAdapter, name: 'broken', claim: () => { throw new Error('field moved') } }], (reason) => reasons.push(reason))
+ const refs: Array = []
+ render(h(AnimatedView, { style: makeStyle(transition), ref: undefined }))
+ render(h(View, { collapsable: false, ref: (r: any) => refs.push(r) }))
+
+ broken.link(Fiber.getHostFor(refs[0]))
+ broken.link(Fiber.getHostFor(refs[0]))
+ assert.equal(reasons.length, 1)
+ assert.match(reasons[0], /broken failed/)
+})
+
+test('edge: a mounted ScopedTheme changing its name, read from _reactInternals during render', () => {
+ const seen: Array = []
+ const style = makeStyle(transition)
+ const app = (name: string) => h(ScopedThemeContext, { value: { name } }, h(AnimatedView, { style }))
+ const tag = render(app('red'))
+ for (const name of ['dark', 'red', 'dark', 'light']) {
+ render(app(name), tag)
+ seen.push(managers[0].previous.backgroundColor as string)
+ }
+ // expected: black, red, black, white
+ console.log('# scope flips seen by Reanimated:', seen.join(', '))
+ assert.deepEqual(seen, ['black', 'red', 'black', 'white'])
+})
+
+// Animated.ScrollView: RN's ScrollView class sits between AnimatedComponent and the host, so "nearest class" is wrong
+class ScrollViewLike extends React.Component {
+ render() {
+ const { forwardedRef, ...rest } = this.props
+ return h('RCTScrollView', { ...rest, ref: unistylesRef(forwardedRef) })
+ }
+}
+const ScrollViewChild = ({ ref, ...props }: any) => h(ScrollViewLike, { ...props, forwardedRef: ref })
+// Android + refreshControl (ScrollView.js:1925-1956): the scroll host is a child of RefreshControl's native host
+class RefreshControlLike extends React.Component {
+ render() {
+ return h('AndroidSwipeRefreshLayout', { style: this.props.style }, this.props.children)
+ }
+}
+class AndroidRefreshScrollViewLike extends React.Component {
+ render() {
+ const { forwardedRef, ...rest } = this.props
+ return h(RefreshControlLike, null, h('RCTScrollView', { ...rest, ref: unistylesRef(forwardedRef) }))
+ }
+}
+const UnistylesWrapper = (Inner: any) => ({ ref, ...props }: any) => h(Inner, { ...props, forwardedRef: ref })
+const AndroidRefreshScrollChild = UnistylesWrapper(({ forwardedRef, ...props }: any) => h(AndroidRefreshScrollViewLike, { ...props, forwardedRef }))
+
+test('reanimated check: the AnimatedComponent whose _componentRef is this host', async () => {
+ const { animatedComponentOf } = Reanimated
+ const hosts: Array = []
+ const keep = (r: any) => r && hosts.push(Fiber.getHostFor(r))
+ class Other extends React.Component {
+ _componentRef: any = null // same field name, but it doesn't hold this host
+ render() {
+ return h(View, { collapsable: false, ref: keep })
+ }
+ }
+ render(h(View, null,
+ h(AnimatedView, { style: makeStyle(transition), forwardedRef: keep }),
+ h(AnimatedView, { style: makeStyle(transition), child: ScrollViewChild, forwardedRef: keep }),
+ h(View, { ref: keep }),
+ h(Other),
+ h(AnimatedView, { style: makeStyle(transition), child: AndroidRefreshScrollChild, forwardedRef: keep })))
+
+ const [animated, scroll, plain, other, refreshScroll] = hosts
+ assert.ok(animatedComponentOf(refreshScroll) instanceof AnimatedView, 'Android refreshControl: walks past the AndroidSwipeRefreshLayout host')
+ assert.ok(animatedComponentOf(animated) instanceof AnimatedView)
+ assert.ok(animatedComponentOf(scroll) instanceof AnimatedView, 'skips the ScrollView class in between')
+ assert.equal(animatedComponentOf(plain), null)
+ assert.equal(animatedComponentOf(other), null)
+})
+
+// A stable scope is renamed in place, so the adapter must key its cache on the name, not the object
+test('stable scope: a rename without a React render still reaches Reanimated CSS transitions', () => {
+ const scope = { name: 'red' }
+ render(h(ScopedThemeContext, { value: scope }, h(AnimatedView, { style: makeStyle(transition) })))
+ assert.equal(managers[0].previous.backgroundColor, 'red')
+
+ scope.name = 'dark'
+ registry.refresh(linked)
+
+ assert.deepEqual(managers[0].started, [['backgroundColor', 'red', 'black']])
+})
diff --git a/test/reanimated-contract.test.ts b/test/reanimated-contract.test.ts
new file mode 100644
index 0000000..b9915f1
--- /dev/null
+++ b/test/reanimated-contract.test.ts
@@ -0,0 +1,35 @@
+// The private Reanimated fields the adapter reads, checked in each supported version's source.
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import path from 'node:path'
+import { createRequire } from 'node:module'
+
+const require = createRequire(import.meta.url)
+const { REANIMATED } = require('./harness/paths.cjs') as { REANIMATED: Record }
+
+const read = (root: string, ...candidates: Array) => {
+ const file = candidates.map((c) => path.join(root, c)).find((f) => fs.existsSync(f))
+ assert.ok(file, `none of ${candidates.join(', ')}`)
+ return fs.readFileSync(file, 'utf8')
+}
+
+for (const [line, root] of Object.entries(REANIMATED)) {
+ const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version
+
+ test(`react-native-reanimated ${version} (${line}.x): fields the Reanimated check and CSS adapter rely on`, () => {
+ const src = path.join(root, 'src')
+ const css = read(src, 'css/component/AnimatedComponent.native.tsx', 'css/component/AnimatedComponent.tsx')
+ const animated = read(src, 'createAnimatedComponent/AnimatedComponent.native.tsx', 'createAnimatedComponent/AnimatedComponent.tsx')
+ const factory = read(src, 'createAnimatedComponent/createAnimatedComponent.tsx')
+
+ assert.match(css, /default:\s*\{\s*collapsable:\s*false\s*\}/, 'renders its child with collapsable: false')
+ assert.match(css, /_componentRef\s*=\s*this\._resolveComponentRef\(ref\)/, 'keeps the animated ref in _componentRef')
+ assert.match(css, /ref=\{this\._setComponentRef/, 'the ref goes on the child it renders')
+ assert.match(css, /_CSSManager\??:\s*CSSManager/)
+ assert.match(css, /_cssStyle:\s*CSSStyle/)
+ assert.match(animated, /_updateStyles\(props/)
+ assert.match(css, /this\._CSSManager\??\.update\(this\._cssStyle/, "CSS base class pushes _cssStyle to the manager")
+ assert.match(factory, /static displayName = `AnimatedComponent\(/)
+ })
+}
diff --git a/test/rn-android.test.ts b/test/rn-android.test.ts
new file mode 100644
index 0000000..8e840b3
--- /dev/null
+++ b/test/rn-android.test.ts
@@ -0,0 +1,4 @@
+// React Native's own components, android. See rn-suite.ts.
+import { runSuite } from './rn-suite'
+
+runSuite('android')
diff --git a/test/rn-ios.test.ts b/test/rn-ios.test.ts
new file mode 100644
index 0000000..fff41e8
--- /dev/null
+++ b/test/rn-ios.test.ts
@@ -0,0 +1,4 @@
+// React Native's own components, ios. See rn-suite.ts.
+import { runSuite } from './rn-suite'
+
+runSuite('ios')
diff --git a/test/rn-suite.ts b/test/rn-suite.ts
new file mode 100644
index 0000000..f089458
--- /dev/null
+++ b/test/rn-suite.ts
@@ -0,0 +1,281 @@
+// React Native's own components, compared with Unistyles' and Uniwind's lookups (copied, file:line). One platform per process.
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { Fiber, type Host } from '../src/index'
+
+export const runSuite = (platform: 'ios' | 'android') => {
+ const require = createRequire(import.meta.url)
+ const mode = process.env.UNIFIBER_MODE ?? 'development'
+ const { React, RN, render, act } = require('./harness/rn.cjs')({ platform, mode })
+ const h = React.createElement
+ const rerender = (element: any, tag: number) => (mode === 'development' ? act(() => render(element, tag)) : render(element, tag))
+ const isAndroid = platform === 'android'
+
+ // packages/unistyles/src/specs/ShadowRegistry/index.ts:57-67 (findShadowNodeForHandle)
+ const unistylesShadowNode = (handle: any) =>
+ handle?.__internalInstanceHandle?.stateNode?.node ??
+ handle?.getScrollResponder?.()?.getNativeScrollRef?.()?.__internalInstanceHandle?.stateNode?.node ??
+ handle?.getNativeScrollRef?.()?.__internalInstanceHandle?.stateNode?.node ??
+ handle?._viewRef?.__internalInstanceHandle?.stateNode?.node ??
+ handle?.viewRef?.current?.__internalInstanceHandle?.stateNode?.node ??
+ handle?._nativeRef?.__internalInstanceHandle?.stateNode?.node
+ // packages/unistyles/src/core/createUnistylesActivityIndicator.tsx:12-17 (getWrapperHandle)
+ const unistylesActivityIndicatorWrapper = (ref: any) => {
+ const spinner = ref?.__internalInstanceHandle
+ return { __internalInstanceHandle: isAndroid ? spinner?.return?.return : spinner?.return }
+ }
+
+ // Uniwind packages/uniwind/src/components/native/*
+ const uniwindShadowNode: Record unknown> = {
+ host: (ref) => ref?.__internalInstanceHandle?.stateNode?.node, // View.tsx:87-88 and 13 more
+ ImageBackground: (ref) => ref?._viewRef?.__internalInstanceHandle?.stateNode?.node, // ImageBackground.tsx:73-74
+ KeyboardAvoidingView: (ref) => ref?.viewRef?.current?.__internalInstanceHandle?.stateNode?.node, // KeyboardAvoidingView.tsx:57-58
+ RefreshControl: (ref) => ref?._nativeRef?.__internalInstanceHandle?.stateNode?.node, // RefreshControl.tsx:62-63
+ ScrollView: (ref) => ref?.getNativeScrollRef?.()?.__internalInstanceHandle?.stateNode?.node, // ScrollView.tsx:96-98
+ list: (ref) => ref?.getScrollResponder?.()?.getNativeScrollRef?.()?.__internalInstanceHandle?.stateNode?.node, // FlatList.tsx:86-87, SectionList, VirtualizedList
+ // Button.tsx:49-52
+ Button: (ref) => (isAndroid ? ref?._reactInternals?.child?.child : ref?.__internalInstanceHandle)?.stateNode?.node,
+ }
+
+ type Case = { name: string; element: (ref: any) => any; uniwind?: keyof typeof uniwindShadowNode; unistyles?: boolean }
+ const CASES: Array = [
+ { name: 'View', element: (ref) => h(RN.View, { ref, style: { flex: 1 } }), uniwind: 'host', unistyles: true },
+ { name: 'Text', element: (ref) => h(RN.Text, { ref }, 'text'), uniwind: 'host', unistyles: true },
+ { name: 'Image', element: (ref) => h(RN.Image, { ref, source: { uri: 'a' } }), uniwind: 'host', unistyles: true },
+ { name: 'ImageBackground', element: (ref) => h(RN.ImageBackground, { ref, source: { uri: 'a' } }), uniwind: 'ImageBackground', unistyles: true },
+ { name: 'TextInput', element: (ref) => h(RN.TextInput, { ref }), uniwind: 'host', unistyles: true },
+ { name: 'Switch', element: (ref) => h(RN.Switch, { ref }), uniwind: 'host', unistyles: true },
+ { name: 'ScrollView', element: (ref) => h(RN.ScrollView, { ref }, h(RN.View)), uniwind: 'ScrollView', unistyles: true },
+ {
+ name: 'ScrollView + refreshControl',
+ element: (ref) => h(RN.ScrollView, { ref, refreshControl: h(RN.RefreshControl, { refreshing: false }) }, h(RN.View)),
+ uniwind: 'ScrollView',
+ unistyles: true,
+ },
+ { name: 'horizontal ScrollView', element: (ref) => h(RN.ScrollView, { ref, horizontal: true }, h(RN.View)), uniwind: 'ScrollView', unistyles: true },
+ {
+ name: 'FlatList',
+ element: (ref) => h(RN.FlatList, { ref, data: ['a', 'b'], keyExtractor: (i: string) => i, renderItem: () => h(RN.View), ListHeaderComponent: h(RN.View) }),
+ uniwind: 'list',
+ unistyles: true,
+ },
+ {
+ name: 'SectionList',
+ element: (ref) => h(RN.SectionList, { ref, sections: [{ key: 's', data: ['a'] }], keyExtractor: (i: string) => i, renderItem: () => h(RN.View) }),
+ uniwind: 'list',
+ unistyles: true,
+ },
+ {
+ name: 'VirtualizedList',
+ element: (ref) =>
+ h(RN.VirtualizedList, { ref, data: ['a'], keyExtractor: (i: string) => i, getItem: (d: any, i: number) => d[i], getItemCount: (d: any) => d.length, renderItem: () => h(RN.View) }),
+ uniwind: 'list',
+ unistyles: true,
+ },
+ { name: 'KeyboardAvoidingView', element: (ref) => h(RN.KeyboardAvoidingView, { ref }), uniwind: 'KeyboardAvoidingView', unistyles: true },
+ { name: 'RefreshControl', element: (ref) => h(RN.RefreshControl, { ref, refreshing: false }), uniwind: 'RefreshControl', unistyles: true },
+ { name: 'ActivityIndicator', element: (ref) => h(RN.ActivityIndicator, { ref }), uniwind: 'host' },
+ { name: 'Button', element: (ref) => h(RN.Button, { ref, title: 'b', onPress() {} }), uniwind: 'Button' },
+ { name: 'Pressable', element: (ref) => h(RN.Pressable, { ref }), uniwind: 'host', unistyles: true },
+ { name: 'TouchableOpacity', element: (ref) => h(RN.TouchableOpacity, { ref }), uniwind: 'host', unistyles: true },
+ { name: 'TouchableHighlight', element: (ref) => h(RN.TouchableHighlight, { ref }, h(RN.View)), uniwind: 'host', unistyles: true },
+ { name: 'TouchableWithoutFeedback (child view)', element: (ref) => h(RN.TouchableWithoutFeedback, null, h(RN.View, { ref })), uniwind: 'host' },
+ { name: 'SafeAreaView', element: (ref) => h(RN.SafeAreaView, { ref }), uniwind: 'host', unistyles: true },
+ { name: 'Modal content', element: (ref) => h(RN.Modal, { visible: true }, h(RN.View, { ref })), uniwind: 'host' },
+ { name: 'Animated.View', element: (ref) => h(RN.Animated.View, { ref }), uniwind: 'host', unistyles: true },
+ { name: 'Animated.ScrollView', element: (ref) => h(RN.Animated.ScrollView, { ref }, h(RN.View)), uniwind: 'ScrollView', unistyles: true },
+ {
+ name: 'Animated.FlatList',
+ element: (ref) => h(RN.Animated.FlatList, { ref, data: ['a'], keyExtractor: (i: string) => i, renderItem: () => h(RN.View) }),
+ uniwind: 'list',
+ unistyles: true,
+ },
+ ]
+
+ const mount = (element: (ref: any) => any) => {
+ const box: { current: any } = { current: null }
+ const tag = render(element((r: any) => r && (box.current = r)))
+ assert.ok(box.current, 'ref attached')
+ return { ref: box.current, tag }
+ }
+
+ for (const c of CASES) {
+ test(`${platform}: ${c.name}: getHostFor / getShadowNodeFor = what the libraries resolve today`, () => {
+ const { ref, tag } = mount(c.element)
+ const host = Fiber.getHostFor(ref)
+
+ assert.ok(host, 'resolved')
+ const node = Fiber.getShadowNodeFor(ref)
+ assert.equal(node, Fiber.getShadowNode(host!))
+ assert.equal(typeof node, 'object')
+
+ if (c.unistyles) {
+ assert.equal(node, unistylesShadowNode(ref), 'Unistyles findShadowNodeForHandle')
+ }
+ if (c.uniwind) {
+ assert.equal(node, uniwindShadowNode[c.uniwind](ref), `Uniwind ${c.uniwind}`)
+ }
+
+ for (let i = 0; i < 3; i++) {
+ rerender(c.element(() => {}), tag)
+ }
+ assert.equal(Fiber.getHostFor(ref), host)
+ assert.ok(Fiber.getShadowNodes(host!).length >= 1)
+ assert.equal(Fiber.isHidden(host!), false)
+ assert.equal(typeof Fiber.getNativeTag(host!), 'number')
+ })
+ }
+
+ test(`${platform}: ActivityIndicator: getParentHost = Unistyles' platform-specific wrapper walk`, () => {
+ const { ref } = mount((r) => h(RN.ActivityIndicator, { ref: r, style: { margin: 4 } }))
+ const wrapper = unistylesActivityIndicatorWrapper(ref).__internalInstanceHandle
+ const parent = Fiber.getParentHost(Fiber.getHostFor(ref)!)
+
+ assert.equal(wrapper.tag, 5, `today's .return${isAndroid ? '.return' : ''} lands on a host`)
+ assert.equal(Fiber.getShadowNode(parent!), wrapper.stateNode.node)
+ })
+
+ test(`${platform}: content container = Uniwind accents (contentContainer, column wrapper, header/footer)`, () => {
+ // accents.ts:15-29 ScrollView
+ const scroll = mount((r) => h(RN.ScrollView, { ref: r, refreshControl: h(RN.RefreshControl, { refreshing: false }) }, h(RN.View))).ref
+ assert.equal(Fiber.getNativeTag(Fiber.getContentContainerFor(scroll)!), scroll.getInnerViewRef().__nativeTag)
+ assert.equal(Fiber.getNativeTag(Fiber.getHostFor(scroll)!), scroll.__nativeTag, 'endFillColor: the scroll view itself')
+
+ // accents.ts:38 (column wrapper / header / footer), :50 (contentContainer), :62 (endFillColor)
+ const flat = mount((r) => h(RN.FlatList, { ref: r, data: ['a'], keyExtractor: (i: string) => i, renderItem: () => h(RN.View) })).ref
+ const flatContent = Fiber.getContentContainerFor(flat)!
+ assert.equal(Fiber.getShadowNode(flatContent), flat.getNativeScrollRef().getInnerViewRef().__internalInstanceHandle.stateNode.node)
+ assert.equal(Fiber.getNativeTag(flatContent), flat.getScrollResponder().getInnerViewRef().__nativeTag)
+ assert.equal(Fiber.getNativeTag(Fiber.getHostFor(flat)!), flat.getNativeScrollRef().__nativeTag)
+
+ // accents.ts:78 (contentContainer), :90 (header/footer via the private _innerView), :102 (endFillColor)
+ for (const List of [RN.SectionList, RN.VirtualizedList]) {
+ const props =
+ List === RN.SectionList
+ ? { sections: [{ key: 's', data: ['a'] }], keyExtractor: (i: string) => i, renderItem: () => h(RN.View) }
+ : { data: ['a'], keyExtractor: (i: string) => i, getItem: (d: any, i: number) => d[i], getItemCount: (d: any) => d.length, renderItem: () => h(RN.View) }
+ const list = mount((r) => h(List, { ref: r, ...props })).ref
+ const content = Fiber.getContentContainerFor(list)!
+ assert.equal(Fiber.getNativeTag(content), list.getScrollResponder().getInnerViewRef().__nativeTag)
+ assert.equal(Fiber.getShadowNode(content), list.getScrollResponder()._innerView.nativeInstance.__internalInstanceHandle.stateNode.node, '_innerView is getInnerViewRef()')
+ assert.equal(Fiber.getNativeTag(Fiber.getHostFor(list)!), list.getScrollResponder().getNativeScrollRef().__nativeTag)
+ }
+
+ const scrollHost: any = Fiber.getHostFor(scroll)
+ let first = scrollHost.child
+ while (first && first.tag !== 5) first = first.child
+ const content: any = Fiber.getContentContainerFor(scroll)
+ if (isAndroid) {
+ // the refreshControl wraps the scroll host instead (ScrollView.js:1941)
+ assert.equal(first?.stateNode?.canonical?.nativeTag, content.stateNode.canonical.nativeTag)
+ } else {
+ // RCTRefreshControl up to RN 0.88, PullToRefreshView from 0.89
+ assert.match(first?.type, /RefreshControl|PullToRefresh/, 'iOS: refreshControl is the first child of the scroll host')
+ assert.notEqual(first?.stateNode?.canonical?.nativeTag, content.stateNode.canonical.nativeTag)
+ }
+ })
+
+ test(`${platform}: hidden state on real components: Activity, Suspense, react-freeze shape`, () => {
+ const pending = { then() {} }
+ const Gate = ({ suspend, children }: any) => {
+ if (suspend) throw pending
+ return children
+ }
+ const refs: Record = {}
+ const at = (key: string) => (r: any) => r && (refs[key] = r)
+ const app = (s: { activity: 'visible' | 'hidden'; suspend: boolean; n: number }) =>
+ h(RN.View, { testID: `root-${s.n}` },
+ h(React.Activity, { mode: s.activity }, h(RN.ScrollView, { ref: at('scroll') }, h(RN.Text, { ref: at('text') }, 'a'))),
+ h(React.Suspense, { fallback: h(RN.ActivityIndicator, { ref: at('spinner') }) },
+ h(Gate, { suspend: s.suspend }, h(RN.Pressable, { ref: at('pressable') }))))
+ let state = { activity: 'visible' as const, suspend: false, n: 0 } as { activity: 'visible' | 'hidden'; suspend: boolean; n: number }
+ const tag = render(app(state))
+ const host = (key: string) => Fiber.getHostFor(refs[key]) as Host
+
+ const steps: Array<[Partial, Record]> = [
+ // key: [isHidden, isInHiddenActivity, isSuspended]
+ [{ activity: 'hidden' }, { scroll: [true, true, false], text: [true, true, false], pressable: [false, false, false] }],
+ [{ activity: 'visible', suspend: true }, { scroll: [false, false, false], text: [false, false, false], pressable: [true, false, true], spinner: [false, false, false] }],
+ [{ suspend: false }, { pressable: [false, false, false] }],
+ [{}, { scroll: [false, false, false], pressable: [false, false, false] }],
+ ]
+ for (const [step, expected] of steps) {
+ state = { ...state, ...step, n: state.n + 1 }
+ rerender(app(state), tag)
+ for (const [key, [hidden, activity, suspended]] of Object.entries(expected)) {
+ const x = host(key)
+ assert.deepEqual([Fiber.isHidden(x), Fiber.isInHiddenActivity(x), Fiber.isSuspended(x), Fiber.isFrozen(x)], [hidden, activity, suspended, false], `${key} after ${JSON.stringify(step)}`)
+ }
+ }
+ })
+
+ test(`${platform}: batch refresh over a real screen = per-view oracle`, () => {
+ const refs = new Set()
+ const at = (r: any) => r && refs.add(r)
+ // link while visible, then hide: React doesn't attach refs in an Activity that mounts hidden
+ const screen = (mode: 'visible' | 'hidden') =>
+ h(RN.View, null,
+ h(RN.ScrollView, { ref: at }, h(RN.View, { ref: at }, h(RN.Text, { ref: at }, 'a'), h(RN.Image, { ref: at, source: { uri: 'a' } }))),
+ h(RN.FlatList, { ref: at, data: ['a', 'b'], keyExtractor: (i: string) => i, renderItem: () => h(RN.Pressable, { ref: at }) }),
+ h(React.Activity, { mode }, h(RN.View, { ref: at }, h(RN.TextInput, { ref: at }))))
+ const tag = render(screen('visible'))
+ const hosts = [...refs].map((r) => Fiber.getHostFor(r)!)
+ rerender(screen('hidden'), tag)
+ assert.equal(hosts.filter((x) => Fiber.isInHiddenActivity(x)).length, 2, 'two linked views are now hidden')
+ const visited: Array = []
+ Fiber.collectVisibleAncestors(hosts, (x) => visited.push(Fiber.getNativeTag(x)!))
+
+ const expected = new Set()
+ for (const x of hosts) {
+ if (Fiber.isHidden(x)) continue
+ for (let c: Host | undefined = x; c; c = Fiber.getParentHost(c)) expected.add(Fiber.getNativeTag(c)!)
+ }
+ assert.deepEqual(visited.sort((a, b) => a - b), [...expected].sort((a, b) => a - b))
+ assert.equal(new Set(visited).size, visited.length, 'each host once')
+
+ // Uniwind fiberBaseline.ts:216-236
+ const uniwind = new Set()
+ const seenFibers = new Set()
+ for (const x of hosts) {
+ for (let current: any = x; current && !seenFibers.has(current); current = current.return) {
+ seenFibers.add(current)
+ if (current.alternate) seenFibers.add(current.alternate)
+ if (current.tag !== 5) continue
+ for (const target of [current, current.alternate]) if (target?.stateNode?.node) uniwind.add(target.stateNode.node)
+ }
+ }
+ const ours = new Set()
+ Fiber.collectAncestors(hosts, (x) => Fiber.getShadowNodes(x).forEach((n) => ours.add(n)))
+ assert.deepEqual([...ours].filter((n) => !uniwind.has(n)).length + [...uniwind].filter((n) => !ours.has(n)).length, 0)
+ const all: Array = []
+ Fiber.collectAncestors(hosts, (x) => all.push(Fiber.getNativeTag(x)!))
+ assert.equal(all.length, visited.length + 2, 'the two hidden views are included, visible ones once each')
+ })
+
+ test(`${platform}: verify + patchRenderedProps on a real View`, () => {
+ const style = [{ backgroundColor: 'white' }, { padding: 2 }]
+ const { ref } = mount((r) => h(RN.View, { ref: r, style }))
+ const host = Fiber.getHostFor(ref)!
+
+ assert.deepEqual(Fiber.verify(host, { backgroundColor: 'black' }), [{ key: 'backgroundColor', react: 'white', expected: 'black' }])
+ assert.equal(Fiber.patchRenderedProps(host, (props) => ({ ...props, style: [{ backgroundColor: 'black' }, { padding: 2 }] })), true)
+ assert.deepEqual(Fiber.verify(host, { backgroundColor: 'black', padding: 2 }), [])
+ })
+
+ test(`${platform}: dev owner names reach the user's component through RN's wrappers`, () => {
+ function ProfileCard() {
+ return h(RN.Pressable, { ref: (r: any) => r && (card = r) }, h(RN.Text, null, 'x'))
+ }
+ let card: any = null
+ render(h(ProfileCard))
+ const owners = Fiber.getOwners(Fiber.getHostFor(card)!)
+
+ if (mode === 'development') {
+ assert.ok(owners.includes('ProfileCard'), owners.join(' > '))
+ assert.match(Fiber.getCreationStack(Fiber.getHostFor(card)!)!, /\n/)
+ } else {
+ assert.deepEqual(owners, [])
+ }
+ })
+}
diff --git a/test/unistyles-android.test.ts b/test/unistyles-android.test.ts
new file mode 100644
index 0000000..19ff319
--- /dev/null
+++ b/test/unistyles-android.test.ts
@@ -0,0 +1,4 @@
+// Unistyles' real wrappers + registry JS on React Native's own components, android. See unistyles-suite.ts.
+import { runSuite } from './unistyles-suite'
+
+runSuite('android')
diff --git a/test/unistyles-ios.test.ts b/test/unistyles-ios.test.ts
new file mode 100644
index 0000000..b0a5f79
--- /dev/null
+++ b/test/unistyles-ios.test.ts
@@ -0,0 +1,4 @@
+// Unistyles' real wrappers + registry JS on React Native's own components, ios. See unistyles-suite.ts.
+import { runSuite } from './unistyles-suite'
+
+runSuite('ios')
diff --git a/test/unistyles-suite.ts b/test/unistyles-suite.ts
new file mode 100644
index 0000000..51844f8
--- /dev/null
+++ b/test/unistyles-suite.ts
@@ -0,0 +1,155 @@
+// Unistyles' real wrappers and registry JS on React Native's own components: what it links, unlinks, suspends and
+// refreshes vs unifiber's answers. Where they differ, unifiber must match the committed tree.
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { Fiber, type Host } from '../src/index'
+
+export const runSuite = (platform: 'ios' | 'android') => {
+ const require = createRequire(import.meta.url)
+ const mode = process.env.UNIFIBER_MODE ?? 'development'
+ const { React, RN, render, act } = require('./harness/rn.cjs')({ platform, mode })
+ const U = require('./harness/unistyles.cjs')({ platform, mode })
+ const { Freeze } = require(require('./harness/paths.cjs').REACT_FREEZE)
+ const h = React.createElement
+ const rerender = (element: any, tag: number) => (mode === 'development' ? act(() => render(element, tag)) : render(element, tag))
+ const C = U.components
+
+ // a style object as Unistyles' babel plugin leaves it
+ const unistyle = (id: string) => ({ backgroundColor: 'red', [`unistyles_${id}`]: { uni__getStyles: () => ({ backgroundColor: 'red' }) } })
+ const linkedNodes = (from: number) => U.calls.slice(from).filter((c: any) => c.op === 'link').map((c: any) => c.node)
+
+ const list = { data: ['a'], keyExtractor: (i: string) => i, renderItem: () => h(RN.View) }
+ const CASES: Array<[string, (props: any) => any]> = [
+ ['View', (p) => h(C.View, p)],
+ ['Text', (p) => h(C.Text, p, 'text')],
+ ['Image', (p) => h(C.Image, { ...p, source: { uri: 'a' } })],
+ ['ImageBackground', (p) => h(C.ImageBackground, { ...p, source: { uri: 'a' } })],
+ ['TextInput', (p) => h(C.TextInput, p)],
+ ['Switch', (p) => h(C.Switch, p)],
+ ['ScrollView', (p) => h(C.ScrollView, p, h(RN.View))],
+ ['ScrollView + refreshControl', (p) => h(C.ScrollView, { ...p, refreshControl: h(RN.RefreshControl, { refreshing: false }) }, h(RN.View))],
+ ['FlatList', (p) => h(C.FlatList, { ...p, ...list })],
+ ['SectionList', (p) => h(C.SectionList, { ...p, sections: [{ key: 's', data: ['a'] }], keyExtractor: (i: string) => i, renderItem: () => h(RN.View) })],
+ ['VirtualizedList', (p) => h(C.VirtualizedList, { ...p, ...list, getItem: (d: any, i: number) => d[i], getItemCount: (d: any) => d.length })],
+ ['KeyboardAvoidingView', (p) => h(C.KeyboardAvoidingView, p)],
+ ['RefreshControl', (p) => h(C.RefreshControl, { ...p, refreshing: false })],
+ ['Pressable', (p) => h(C.Pressable, p)],
+ ['TouchableOpacity', (p) => h(C.TouchableOpacity, p)],
+ ['TouchableHighlight', (p) => h(C.TouchableHighlight, p, h(RN.View))],
+ ['SafeAreaView', (p) => h(C.SafeAreaView, p)],
+ ['NativeView', (p) => h(C.NativeView, p)],
+ ['NativeText', (p) => h(C.NativeText, p, 'text')],
+ ['Animated.View', (p) => h(C.Animated.View, p)],
+ ['Animated.ScrollView', (p) => h(C.Animated.ScrollView, p, h(RN.View))],
+ ['Animated.FlatList', (p) => h(C.Animated.FlatList, { ...p, ...list })],
+ ]
+
+ for (const [name, element] of CASES) {
+ test(`${platform}: Unistyles ${name}: the node Unistyles links = Fiber.getShadowNodeFor(the ref the app gets)`, () => {
+ let ref: any = null
+ const from = U.calls.length
+ const tag = render(element({ style: unistyle(name), ref: (r: any) => r && (ref = r) }))
+ const linked = linkedNodes(from)
+
+ assert.equal(linked.length, 1, 'Unistyles linked once')
+ assert.equal(Fiber.getShadowNodeFor(ref), linked[0])
+ const host = Fiber.getHostFor(ref)!
+ assert.equal(Fiber.getShadowNode(host), linked[0])
+
+ const beforeUnmount = U.calls.length
+ rerender(h(RN.View), tag)
+ const removed = U.calls.slice(beforeUnmount).filter((c: any) => c.op === 'unlink' || c.op === 'suspend')
+ // ImageBackground has two refs, Pressable re-links: expect only unlinks
+ assert.ok(removed.length >= 1 && removed.every((c: any) => c.op === 'unlink'), removed.map((c: any) => c.op).join(','))
+ assert.ok(removed.some((c: any) => c.node === linked[0]), 'the linked node is unlinked')
+ assert.equal(Fiber.isHidden(host), false)
+ })
+ }
+
+ test(`${platform}: Unistyles ActivityIndicator: the wrapper view Unistyles links = Fiber.getParentHost`, () => {
+ let ref: any = null
+ const from = U.calls.length
+ render(h(C.ActivityIndicator, { style: unistyle('spinner'), ref: (r: any) => r && (ref = r) }))
+ const linked = linkedNodes(from)
+
+ assert.equal(linked.length, 1)
+ assert.equal(Fiber.getShadowNode(Fiber.getParentHost(Fiber.getHostFor(ref)!)!), linked[0])
+ })
+
+ test(`${platform}: Unistyles remove on freeze: suspend while frozen, unlink on a real unmount`, () => {
+ let ref: any = null
+ const app = (freeze: boolean, show = true, n = 0) =>
+ h(RN.View, { testID: `root-${n}` }, show ? h(Freeze, { freeze }, h(C.View, { style: unistyle('frozen'), ref: (r: any) => r && (ref = r) })) : null)
+ const tag = render(app(false))
+ const host = Fiber.getHostFor(ref)!
+ const decisions: Array = []
+ const step = (element: any) => {
+ const from = U.calls.length
+ rerender(element, tag)
+ return U.calls.slice(from).filter((c: any) => c.op === 'unlink' || c.op === 'suspend' || c.op === 'link').map((c: any) => c.op).join(',')
+ }
+
+ for (let n = 1; n <= 4; n++) {
+ decisions.push(`freeze ${step(app(true, true, n * 10))} (unifiber: hidden ${Fiber.isHidden(host)}, frozen ${Fiber.isFrozen(host)})`)
+ decisions.push(`thaw ${step(app(false, true, n * 10 + 1))} (unifiber: hidden ${Fiber.isHidden(host)})`)
+ }
+ decisions.push(`unmount ${step(app(false, false, 99))}`)
+ console.log(`# ${platform} ${decisions.join('\n# ')}`)
+
+ for (const line of decisions.filter((d) => d.startsWith('freeze'))) assert.match(line, /^freeze suspend .*hidden true, frozen true/)
+ for (const line of decisions.filter((d) => d.startsWith('thaw'))) assert.match(line, /^thaw link .*hidden false/)
+ assert.equal(decisions.at(-1), 'unmount unlink')
+ })
+
+ test(`${platform}: Unistyles refreshReactNodes vs Fiber.collectVisibleAncestors, incl. after freeze/thaw and on a fallback`, () => {
+ const pending = { then() {} }
+ const Gate = ({ suspend, children }: any) => {
+ if (suspend) throw pending
+ return children
+ }
+ const refs = new Map()
+ const at = (key: string) => (r: any) => r && refs.set(key, r)
+ type S = { freeze: boolean; suspend: boolean; n: number }
+ const app = (s: S) =>
+ h(C.View, { style: unistyle('root'), testID: `root-${s.n}`, ref: at('root') },
+ h(Freeze, { freeze: s.freeze },
+ h(C.ScrollView, { style: unistyle('screen'), ref: at('screen') }, h(C.Text, { style: unistyle('title'), ref: at('title') }, 'a'))),
+ h(React.Suspense, { fallback: h(C.View, { style: unistyle('fallback'), ref: at('fallback') }) },
+ h(Gate, { suspend: s.suspend }, h(C.Pressable, { style: unistyle('button'), ref: at('button') }))))
+ let state: S = { freeze: false, suspend: false, n: 0 }
+ const tag = render(app(state))
+ const differences: Array = []
+
+ const refresh = (label: string) => {
+ // what C++ hands back after a commit: creation fibers of mounted linked views
+ const handles = [...refs.values()].map((r) => r.__internalInstanceHandle).filter((f) => f?.stateNode)
+ U.setCommittedHandles(handles)
+ const from = U.calls.length
+ U.runChangeListeners()
+ const today = new Set(U.calls.slice(from).filter((c: any) => c.op === 'refreshReactNodes').flatMap((c: any) => c.nodes))
+
+ const ours = new Set()
+ Fiber.collectVisibleAncestors(handles.map((f) => Fiber.getHostForInstanceHandle(f)), (x: Host) => Fiber.getShadowNodes(x).forEach((n) => ours.add(n)))
+
+ const expected = new Set()
+ for (const f of handles) {
+ const x = Fiber.getHostForInstanceHandle(f)!
+ if (Fiber.isHidden(x)) continue
+ for (let c: Host | undefined = x; c; c = Fiber.getParentHost(c)) Fiber.getShadowNodes(c).forEach((n) => expected.add(n))
+ }
+ assert.deepEqual([...ours].sort(), [...expected].sort(), label)
+ const missing = [...expected].filter((n) => !today.has(n)).length
+ const extra = [...today].filter((n) => !expected.has(n)).length
+ if (missing || extra) differences.push(`${label}: today misses ${missing} node(s), has ${extra} extra`)
+ }
+
+ refresh('initial')
+ for (const step of [{ freeze: true }, { freeze: false }, { freeze: true }, { freeze: false }, { suspend: true }, { suspend: false }, {}]) {
+ state = { ...state, ...step, n: state.n + 1 }
+ rerender(app(state), tag)
+ refresh(JSON.stringify(step))
+ }
+ console.log(`# ${platform} Unistyles refreshReactNodes today vs committed tree:\n# ${differences.join('\n# ') || 'no differences'}`)
+ })
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..c80ebf4
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUncheckedIndexedAccess": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "exactOptionalPropertyTypes": false,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+ "skipLibCheck": true,
+ "noEmit": true,
+ "types": ["node"]
+ },
+ "include": ["src", "test", "bench"],
+ "exclude": ["test/harness", "bench/out", "node_modules"]
+}