From ba479c1f8398bd61adf00e130de74cac38e8ca4c Mon Sep 17 00:00:00 2001 From: Jacek Pudysz Date: Fri, 16 Oct 2026 18:50:29 +0200 Subject: [PATCH] feat: unifiber v1 Typed access to React fiber internals for Unistyles and Uniwind: Fiber host lookups, hidden/suspended/frozen checks, ancestor walks, rendered props patching, diagnostics, owner registry and the Reanimated CSS adapter. Tested against the real Fabric renderer, React Native's own components and Unistyles' JS. --- .github/workflows/ci.yml | 65 ++ .gitignore | 5 + LICENSE | 21 + README.md | 90 +++ bench/build.mjs | 34 + bench/collect.ts | 244 +++++++ bench/get-shadow-node.ts | 228 +++++++ bench/hermes.mjs | 15 + bench/hidden.ts | 41 ++ bench/link-path.ts | 63 ++ bench/metro.mjs | 56 ++ bench/run-hermes.mjs | 24 + bench/shadowNode.ts | 129 ++++ bun.lock | 882 ++++++++++++++++++++++++++ package.json | 53 ++ src/adapters/reanimated.ts | 170 +++++ src/adapters/registry.ts | 105 +++ src/diagnostics.ts | 55 ++ src/fiber.ts | 262 ++++++++ src/host.ts | 214 +++++++ src/index.ts | 43 ++ test/collect.test.ts | 169 +++++ test/context.test.ts | 41 ++ test/diagnostics.test.ts | 111 ++++ test/edges.test.ts | 257 ++++++++ test/fiber-shapes.test.ts | 212 +++++++ test/harness/asset-stub.cjs | 1 + test/harness/fabric.cjs | 58 ++ test/harness/flush.cjs | 26 + test/harness/initialize-core-stub.cjs | 2 + test/harness/paths.cjs | 32 + test/harness/rn.cjs | 162 +++++ test/harness/stubs.cjs | 100 +++ test/harness/unistyles.cjs | 149 +++++ test/hidden.test.ts | 164 +++++ test/malformed.test.ts | 62 ++ test/metro-inline-requires.test.ts | 62 ++ test/reanimated-adapter.test.ts | 282 ++++++++ test/reanimated-contract.test.ts | 35 + test/rn-android.test.ts | 4 + test/rn-ios.test.ts | 4 + test/rn-suite.ts | 281 ++++++++ test/unistyles-android.test.ts | 4 + test/unistyles-ios.test.ts | 4 + test/unistyles-suite.ts | 155 +++++ tsconfig.json | 23 + 46 files changed, 5199 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bench/build.mjs create mode 100644 bench/collect.ts create mode 100644 bench/get-shadow-node.ts create mode 100644 bench/hermes.mjs create mode 100644 bench/hidden.ts create mode 100644 bench/link-path.ts create mode 100644 bench/metro.mjs create mode 100644 bench/run-hermes.mjs create mode 100644 bench/shadowNode.ts create mode 100644 bun.lock create mode 100644 package.json create mode 100644 src/adapters/reanimated.ts create mode 100644 src/adapters/registry.ts create mode 100644 src/diagnostics.ts create mode 100644 src/fiber.ts create mode 100644 src/host.ts create mode 100644 src/index.ts create mode 100644 test/collect.test.ts create mode 100644 test/context.test.ts create mode 100644 test/diagnostics.test.ts create mode 100644 test/edges.test.ts create mode 100644 test/fiber-shapes.test.ts create mode 100644 test/harness/asset-stub.cjs create mode 100644 test/harness/fabric.cjs create mode 100644 test/harness/flush.cjs create mode 100644 test/harness/initialize-core-stub.cjs create mode 100644 test/harness/paths.cjs create mode 100644 test/harness/rn.cjs create mode 100644 test/harness/stubs.cjs create mode 100644 test/harness/unistyles.cjs create mode 100644 test/hidden.test.ts create mode 100644 test/malformed.test.ts create mode 100644 test/metro-inline-requires.test.ts create mode 100644 test/reanimated-adapter.test.ts create mode 100644 test/reanimated-contract.test.ts create mode 100644 test/rn-android.test.ts create mode 100644 test/rn-ios.test.ts create mode 100644 test/rn-suite.ts create mode 100644 test/unistyles-android.test.ts create mode 100644 test/unistyles-ios.test.ts create mode 100644 test/unistyles-suite.ts create mode 100644 tsconfig.json 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"] +}