Skip to content

docs: explain the @react-native-harness/bundler-metro yarn patch#330

Open
mfazekas wants to merge 156 commits into
mainfrom
docs/bundler-metro-patch
Open

docs: explain the @react-native-harness/bundler-metro yarn patch#330
mfazekas wants to merge 156 commits into
mainfrom
docs/bundler-metro-patch

Conversation

@mfazekas

@mfazekas mfazekas commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Documents why we patch @react-native-harness/bundler-metro@1.4.0-rc.1 (the harness entry-point swap doesn't recognize Expo's server-root-relative or .expo/.virtual-metro-entry requests, so the real app boots and every render() times out), the exact diff, how to diagnose the failure mode, and the criteria for dropping the patch once fixed upstream.

mfazekas and others added 30 commits June 1, 2026 11:19
Add new experimental iOS backend (ios/new/) with synchronous API,
move legacy backend files to ios/legacy/, add getEnums() support,
retry listener streams on missingData, and restore TestComponentOverlay.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
getEnums() in legacy now throws directing users to the experimental
backend instead of creating throwaway Worker+File instances.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
… binding

Each Worker has its own C++ command server with its own m_artboards handle map.
Creating separate Workers per file meant artboard handles from one file were
invalid on another file's server. Using a shared singleton Worker fixes cross-file
artboard property set. Also wires fit/alignment through experimental Fit enum and
improves asset type detection with audio/font magic bytes.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Happy <yesreply@happy.engineering>
Passing fit to the Rive() constructor breaks layout mode because
the MTKView drawable isn't ready yet. Set rive.fit after
setupRiveUIView() instead.
Port Flutter data binding tests for VM access, enums, creation
variants, list properties, artboard and image properties. Includes
new .riv test assets and react-native-harness upgrade to alpha.25.
…ceByIndex

Prevents fatal crash when passing negative numbers to Swift APIs
expecting unsigned integers.
CocoaPods doesn't embed SPM-resolved dynamic frameworks automatically.
Patches the embed script to include RiveRuntime when USE_RIVE_SPM=1.
Move 19 backend-specific files to src/legacy/java/, add Gradle sourceSets
switching via USE_RIVE_NEW_API property, prepare empty experimental dirs.
Uses the new handle-based Rive Android SDK (app.rive.*) with CommandQueue,
path-based ViewModelInstance property access, and Kotlin Flows for reactivity.
Throws UnsupportedOperationException for SMI inputs, text runs, and events.
Also fixes Gradle property resolution to use rootProject.findProperty.
Custom TextureView renderer with Choreographer render loop, CommandQueue
polling infrastructure, ViewModel resolution and property validation,
and example Compose test activities for manual testing.
Also fix viewModelByIndex/ByName on Android legacy to catch SDK
exceptions instead of letting them propagate as JniExceptions.
…fferences

Fix colorProperty setter Double→Int overflow on Android legacy
(toLong().toInt()). Make enum and replaceViewModel tests tolerant
of Android SDK behavioral differences.
Use .viewModelDefault(from: .name(vmName)) for default instance
creation instead of artboard-based lookup that only worked for
the default artboard's VM. Skip instanceName, color get/set, and
non-existent property checks on experimental iOS where the SDK
doesn't support them.
…anceName

Add async variants for viewModelByName, defaultArtboardViewModel, and
ViewModel create methods. Pass instanceName through at creation time on
both iOS and Android experimental backends as a workaround for the SDK
not exposing ViewModelInstance.name.
…rimental iOS

Color.argbValue is now public in rive-ios 6.15.2 — implement getValue via
blockingAsync, addListener via valueStream, and fix setter crash by using
UInt32(bitPattern:) for negative ARGB values from JS.
- Always use SPM for RiveRuntime (remove CocoaPods fallback)
- Add RiveSPMEmbedFix module to auto-embed RiveRuntime.framework
- Pin SPM to exactVersion instead of upToNextMajorVersion
- Replace USE_RIVE_SPM with USE_RIVE_EXPERIMENTAL_RUNTIME to select
  legacy vs experimental backend independently of SPM
…erimental iOS test issues

- Rename USE_RIVE_EXPERIMENTAL_RUNTIME to USE_RIVE_NEW_API (matches Android)
- Remove USE_RIVE_SPM and all SPM embedding hacks from podspec/Podfile
- Use standard CocoaPods dependency for RiveRuntime
- Emit initial value in experimental addListener (number/string/bool/enum/color)
- Guard tests that crash on experimental iOS (list ops, autoPlay, artboard/image loading)
- Handle createInstanceByName throwing on experimental backend
mfazekas and others added 29 commits June 12, 2026 06:38
…ental

# Conflicts:
#	example/package.json
#	package.json
#	yarn.lock
Brings in #298 (data-binding thread-safety) + 0.4.12 release.

Conflict resolutions:
- ios/legacy/HybridViewModel{,Instance}.swift: keep #298's MainThread.run
  thread-safety on the getters AND the experimental getPropertiesAsync().
- package.json + yarn.lock: react-native-nitro-modules -> 0.35.10 (required by
  #298's iOS Promise.onMain routing); yarn.lock regenerated via yarn install.
- .gitignore: keep both entries.
- example/ios/Podfile.lock: kept current; regenerated by pod install (CI build-ios
  does rm + pod install).
- test-harness-ios-legacy pinned Xcode 16.4, which runners no longer have
  (it was added before main bumped the shared jobs to 26.3, and main has no
  legacy job to merge a fix from) -> bump to 26.3 and its simulator
  os_version 18.6 -> >=26.0 to match the iOS 26 runtimes Xcode 26.3 ships.
- Pin all iOS jobs (build-ios, test-harness-ios, -legacy) to macos-15:
  macos-latest is mid-migrating to macOS 26, whose sim set lacks an
  iPhone 16 Pro on iOS >=26, which fails the simulator-action boot.
…sh) (#313)

Bumps `runtimeVersions.android` 11.4.1 → 11.6.1 on the experimental
backend line to fix the intermittent native `SIGSEGV` in
`test-harness-android`.

Root cause is a single-threaded re-entrancy / iterator-invalidation
**use-after-free** in `rive::DataBindContainer::updateDataBinds` (a bind
that auto-binds a nested view model re-enters
`addDataBind`/`removeDataBind` and invalidates the active iterator).
Fixed upstream in rive-runtime
[#12649](https://github.com/rive-app/rive-runtime/pull/12649), shipped
in rive-android 11.6.1 (11.4.1/11.5.0/11.6.0 are vulnerable). The
experimental backend compiles + builds clean against 11.6.1 (no API
changes needed).

Full investigation: #308. (`main` is unaffected — it already pins 11.6.1
and does not carry the experimental backend.)
Resolves conflicts in ci.yml (kept macos-15 pin comments) and
use-rive-trigger.harness.tsx (combined the #230 deterministic-rerender
de-flake with the experimental backend's awaitViewReady readiness gating).
…314)

Cherry-picks the `fmt`-as-C++17 Podfile workaround onto the experimental
backend line so its iOS build survives the Xcode 26.4+ (Apple clang 21)
`consteval` error in `fmt` (via RCT-Folly on RN 0.80.3).

Same change as #306 (which targets `main`); applies cleanly here and the
branch had no prior fmt workaround. Remove once we bump to RN 0.83+ (fix
is upstream there).
Merges `main` up to the 0.4.14 release into the iOS experimental branch.
No conflicts.

Notable changes pulled in:
1. fix(android): don't re-acquire a disposed Rive file on view re-attach
(#319)
2. fix(hooks): no-op stale property setter calls after dispose (#316)
3. fix(android): bump rive-android to 11.6.2 (#317) — resolves to 11.6.2
here (up from 11.6.1)

On the #319 native fix: this branch has the legacy/new Android backend
split, so git's rename detection applied the fix to the
`android/src/legacy` variant of `RiveReactNativeView.kt` — which is
correct, since the crash is a property of upstream `RiveAnimationView`'s
cached `rendererAttributes.resource`. The `android/src/new` backend is a
separate `TextureView`/`CommandQueue` implementation with no such code
path, so the same tweak neither applies nor is needed there. A re-attach
lifecycle bug in the new backend, if any, would be a separate fix.

---------

Co-authored-by: Rive Engineering <60802183+rive-engineering@users.noreply.github.com>
…ental

# Conflicts:
#	rive-debug-utils/src/specs/DebugUtils.nitro.ts
… handles (experimental)

- Re-apply data binding when the view reloads (artboard/state-machine/file
  change) — the fresh state machine was left unbound, freezing bound
  graphics at defaults (verified with the Reload Re-bind reproducer).
- Pass alignment into the experimental Fit constructors; it was converted
  but never wired into draw or pointer mapping, so everything rendered
  center-aligned (verified with the Alignment Prop reproducer).
- Release native command-server handles: delete the old state machine on
  reload and dispose, close the RiveSurface on surface destroy and dispose,
  close view-owned bound instances (Auto/ByName) when replaced, close
  ViewModelInstance handles in HybridViewModelInstance.dispose(), and close
  the artboard created by defaultArtboardViewModel* when its wrapper is
  disposed. Measured with the Reload/VMI Handle Leak reproducer: the
  per-instance leak (~11 kB each) is gone; reload-path growth halved
  (a ~6 kB/reload residue remains with all client-side deletes succeeding,
  suspected upstream in rive-android's command server).
- Wrappers that alias instances owned elsewhere (RiveView.getViewModelInstance)
  are marked ownsInstance=false so dispose() never closes a shared handle.
…dy, add main-hop dispose (experimental)

- A failed configure (bad artboardName etc.) previously only logged
  natively: the onError prop never fired and awaitViewReady() hung forever,
  keeping the pending promise (and the view it retains) alive. The catch
  block now reports the error through onError and resolves pending
  awaitViewReady() callers with false (verified with the Load Error Path
  reproducer: onError fires once and the promise settles).
- Restore the legacy backend's dispose() main-thread hop and make deinit
  safe off-main: Nitro can drop the last reference on the JS/GC thread,
  where MainActor.assumeIsolated { cleanup() } would trap. deinit now only
  captures the resources and finishes on main. Not reproduced in practice
  (30 mount/unmount cycles with varied timings plus 30 raw-host-component
  cycles with forced GC all pass), so this is hardening per Swift
  concurrency semantics rather than a confirmed crash fix.
…ackend

The RiveView JS wrapper disposes its HybridObject in the unmount effect
(#202), but the experimental HybridRiveView had no dispose() override, so
teardown only happened via RiveViewManager.onDropViewInstance. Forward
Nitro's dispose() to RiveReactNativeView.dispose(), hopping to the main
thread (Nitro calls it on the JS thread; Choreographer is thread-local),
and make dispose() idempotent so the onDropViewInstance backstop no-ops.
Also settle awaitViewReady() with false on dispose so pending callers
don't hang after unmount.

Verified on emulator: per unmount, the eager path runs the teardown on
main ~10ms before onDropViewInstance, which then early-returns.
The recommended hooks internally used the deprecated sync accessors, which
on the experimental backend block the JS thread on the command queue and
log deprecation warnings users never opted into. They also swallowed
invalid-path errors: the experimental accessors return unvalidated handles
whose sync getters default to 0/""/false, so useRiveNumber('typo', vmi)
yielded value 0 with no error.

- useRiveProperty: seed the value via getValueAsync() (guarded against
  overwriting a newer listener emission) and surface rejections as the
  hook's error — bad paths now produce error instead of a fabricated
  default; write through set() instead of the deprecated value setter.
- useRiveList: length via getLengthAsync (rejections surface as error);
  all operations now return Promises via the *Async methods.
  BREAKING: UseRiveListResult methods return Promises.
- useViewModelInstance: instance creation is async
  (viewModelByNameAsync / defaultArtboardViewModelAsync /
  create*InstanceAsync) with cancellation that disposes an instance
  resolving after unmount; the intermediate ViewModel wrapper is disposed
  after creation (releases the artboard it owns on the experimental
  backend). onInit now runs before the instance is published rather than
  synchronously during render.

Verified on the Android experimental build: QuickStart onInit + setter
work end-to-end and mounting logs zero [Rive/Deprecation] warnings
(previously several per mount).
PR #304 supersedes the in-place async rewrite with a dedicated
useViewModelInstanceAsync hook (isLoading state, RiveViewRef bind polling,
error causes, companion native fixes) and deprecates this one instead.
useRiveProperty and useRiveList stay on the async API.
… active

yarn release would bypass release-please and publish a stable to the
latest dist-tag. The 0.5.0-beta version bump itself needs no config
change: both merge strategies for #134 carry a breaking marker (the PR
title 'feat!:' for a squash, the 'feat(hooks)!:' commit for a merge
commit) and bump-minor-pre-major turns that into the minor bump.
runtimeVersions.{ios,android} live in package.json, which none of the
harness app-binary keys (nor the legacy cocoapods key) hashed — bumping
only the runtime pin produced an exact cache hit and the job silently
tested the old RiveRuntime/rive-android.
…e (experimental)

- Android: RiveErrorLogger.addListener is idempotent — views re-register
  on every reload, and duplicates fanned the same error out N times to
  onError (review M11).
- Android: auto-binding probes getDefaultViewModelInfo instead of
  checking the created instance's handle against the magic value 1L,
  which relied on undocumented handle allocation (review M4).
- iOS: configure() applies isPaused = !autoPlay synchronously before
  launching the config task, and the task no longer recomputes it — a
  play()/pause() issued while configure is in flight (common with
  autoPlay={false} + play() in an effect) is no longer clobbered when
  the task completes (review M10).
… handles

The property accessors cannot validate paths on the experimental backend
(lookup happens on the async command server), so a bad path returns a
handle and surfaces on use instead of as an undefined return. State this
on the accessor docs with the migration guidance (getPropertiesAsync /
getValueAsync rejection / hook error).

Restoring the legacy nullable contract with a blocking probe in the
accessors was considered and rejected: the accessors are not deprecated
— the hooks create properties through them on every mount, and a
runBlocking round-trip there would reintroduce the JS-thread stall the
async hooks migration just removed.
…fast path; opt-in JS log forwarding

- legacy: setValueAsync wrote property.value inside Promise.async (a
  background task), bypassing the MainThread.run discipline every other
  legacy mutation uses — races the main-thread render loop (review M12).
- experimental: value/trigger listener tasks captured self strongly for
  their infinite stream loop, so raw-API consumers dropping a property
  with an active listener leaked wrapper + instance + JS callback (the
  deinit cancelAll was unreachable). Capture the instance/prop locals
  instead (review M6).
- experimental: a dataBind-only prop change with a live Rive instance now
  re-binds the running state machine instead of rebuilding artboard +
  state machine, which visually restarted the animation (review M9).
- RiveLog no longer installs a JS console handler as an import side
  effect (every consumer paid a JSI round-trip per native log line, and
  console.error raised dev RedBoxes for conditions apps can't act on).
  Forwarding is opt-in via RiveLog.forwardToConsole(), and resetHandler()
  now actually detaches the JS handler via the native resetHandler
  instead of re-installing the JS default (review M13). Also turn the
  vacuous no-op-handler harness test into a real guard (review L14).
…perimental

The experimental command server hands out handles/enqueues for any index:
getInstanceAt(999) returned a truthy wrapper whose operations silently
no-op, and addInstanceAt/swap/removeInstanceAt resolved on enqueue even
out of range (review M5). The async paths now probe the list size -
out-of-range lookups return null/nil like the legacy backend and
out-of-range mutations reject.

useRiveList also bumps its revision after its own mutations: the
experimental backend has no native list-change notifications (addListener
is a no-op), so the length result never refreshed after add/remove/swap
there.
…n, deprecated-throws coverage

- getPropertiesAsync on instances without ViewModel metadata (nested
  paths, list items, view-obtained) now rejects with a clear message
  instead of resolving [] - "no metadata" must not read as "no
  properties" (review M7).
- Spec docs: the nine data-binding-deprecated RiveView methods state
  that they throw on the experimental backend (review M14); instanceName
  documents the experimental "" limitation (review M8, needs upstream
  SDK support for a real fix); getEnums and getPropertiesAsync document
  their legacy-backend behavior; getBackend() is typed
  'legacy' | 'experimental' (review L11).
- README gains a Runtime Backends section: default backend, the durable
  USE_RIVE_LEGACY opt-out per platform (Podfile global vs env-var
  pitfall), and the behavioral differences.
- docs/riv-files.md: drop the stale experimental-crash claims - those
  files are exercised green by the harness suites CI runs on the
  experimental backend (review M15).
- view-methods harness: cover that the deprecated SMI-input, text-run,
  and event methods actually throw on the experimental backend - the
  headline behavioral break previously had zero coverage.
…tal)

Referenced assets are registered globally by bare name on the shared
worker/command queue and were never unregistered, so decoded image/font/
audio bytes accumulated for the app's lifetime (review M1). registerAssets
now returns the assets it registered so the owning HybridRiveFile can
release them (unregister + close on Android, removeGlobal* on iOS) in its
dispose().

The name-global registry itself is an upstream design: two live files with
an asset of the same name still clobber each other (last registration
wins) - per-file namespacing needs SDK support.
- iOS: index conversions use Int(exactly:) so NaN/inf/overflow inputs
  return nil instead of trapping (L2).
- iOS: blockingAsync throws a RuntimeError when the deprecated blocking
  API is invoked on the main thread (it would deadlock the MainActor work
  it waits on) instead of hard-trapping the app (L6); afterUpdate's
  file-nil transient logs a warning instead of a dev RedBox (L5).
- Podspec: drop the dead -DRIVE_EXPERIMENTAL_API from consumer-leaking
  s.xcconfig - nothing references the define (L4).
- Backend opt-out accepts both tokens on both platforms:
  USE_RIVE_LEGACY=1 and =true each work for the Podfile/env and the
  Gradle property (L17).
- CI: the cocoapods "save cache" steps referenced cache-key, which
  actions/cache/restore does not output (correct name:
  cache-primary-key) - the save silently no-oped on every run (L18).

Assessed and deliberately not taken: L7-L9 (image-set serialization,
multi-touch, pause-gated draws - feature work / needs upstream), L12
(superseded by #304's hook), L16 (shared image rawData memory - needs a
per-backend split), L1/L3 (log-handler locking, listener first-emission
semantics - low risk, need a design pass).
… build timing (#324)

Adds an `expo57-example` workspace (Expo SDK 57 / RN 0.86) and makes it
the default iOS build on CI, using Expo's precompiled React Native
XCFrameworks
(https://expo.dev/blog/faster-ios-builds-with-precompiled-xcframeworks)
plus the EAS prebuilt third-party bucket (reanimated/worklets;
unofficial endpoint with automatic source fallback).

1. `build-ios` now builds expo57-example with caching (exact-key
short-circuit, Pods cache, incremental build-dir cache). Cold xcodebuild
is ~8 min vs ~26 min median for the bare example; timing details in the
comments below.
2. `test-harness-expo57-ios` runs the shared harness suite against the
Expo app. This needed the `unstable_path` asset publicPath (out-of-root
shared assets) and a yarn patch on `@react-native-harness/bundler-metro`
— its entry-point swap didn't recognize Expo's
server-root-relative/virtual-entry requests, so the real app booted
instead of the harness host (worth upstreaming).
3. The bare RN 0.79 example keeps its coverage via the existing
`test-harness-ios`/`-legacy` jobs, which build it themselves.
4. expo57-example drops the create-expo-app theme boilerplate; its
workspace typecheck is clean.

Verified locally (app renders, Quick Start data binding works, all 4
harness suites pass) and on CI (all jobs green).
Base automatically changed from feat/rive-ios-experimental to main July 20, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant