Skip to content

Run 54 React Native and Expo libraries on macOS, and add real pointer input to the e2e driver - #530

Closed
jmeistrich wants to merge 98 commits into
mainfrom
feat/desktop-libs-macos-land
Closed

jmeistrich wants to merge 98 commits into
mainfrom
feat/desktop-libs-macos-land

Conversation

@jmeistrich

@jmeistrich jmeistrich commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Depends on #529 (upgrade/expo-55, Expo SDK 55 / React Native 0.83). Merge it after #529. Until then, the diff against main also shows #529's 12 commits; this PR's own change is upgrade/expo-55..feat/desktop-libs-macos-land (83 commits).

This makes 54 libraries that the Kitchen Sink adds for macOS build and work there. 35 have version-pinned Spark patches, plus Quick Base64 as a support library; the rest work without a patch, and that is now verified. Each library gets a row in docs/external-libraries.md, a Kitchen Sink screen and a macOS e2e flow. The PR also adds opt-in real mouse input to the e2e driver. Windows support is in a separate branch.

Behavior changes

  • Pinned libraries: before: no Spark patch · now: patched at native preparation; any other version logs a warning and builds unpatched · when: the app declares one of the 36 pinned libraries · why: optional pins must not break apps on other versions.
  • macOS codegen: before: classes registered for iOS-only codegen libraries, which were never built · now: excluded on macOS only · when: every macOS build.
  • MMKV storage location: before: ~/Documents/mmkv · now: ~/Library/Application Support/<namespace>/mmkv, with no migration · when: macOS, react-native-mmkv 4.3.2.
  • Library behavior on macOS: expo-haptics plays haptics instead of rejecting. Keyboard Controller no longer throws at import, and Screen Corner Radius reports 0. react-native-graph builds paths with Skia 2. Release Profiler traces go to the app's namespaced Caches folder. Full list: docs/external-libraries.md.
  • e2e driver: before: windows stayed hidden and the app never activated · now, with --pointer only: the target window is shown and the app activated, then restored at flow end or disconnect. Requires an Accessibility grant. The release gate rejects --pointer, and default runs are unchanged.

Changes

  • Native preparation: patches/dependencies.json adds optional pins. A pin on another installed version stays unpatched and also passes chore: upgrade to Expo SDK 55 and React Native 0.83 #529's "every active pin was prepared" check. Patching handles tab-indented manifests and exports maps that hide package.json.
  • macOS build: codegen libraries with iOS-only podspecs are kept out of macOS autolinking. The macOS Podfile passes --source-dir __dir__ to use_native_modules!, and patched pods declare macOS 14.
  • Kitchen Sink: screens load lazily inside an error boundary, so one failing library breaks only its own screen. Unistyles and Worklets Core Babel plugins are scoped to their demos. The driver's bundle server serves exported assets.
  • Pointer input: bun run e2e:run <flow> --backend driver --pointer sends real Quartz mouse events for tapOn. Each tap waits for mounted UI to settle within the step timeout. See docs/test-driver.md#real-pointer-input-macos.
  • Flows: the new macOS library checks are blocking in the release gate (49 flows). gesture-handler and pressable-pointer need real clicks (--pointer), so they live in e2e/pointer/ outside the gate and run with bun run gate:pointer in the pre-release checklist.
  • Windows: the 43 new native libraries are on the Kitchen Sink windows.autolinking.exclude list until the Windows work lands separately.

Verification

Run on a2531141:

  • bun run typecheck: passes.
  • Vitest, the 24 affected files: 429/431 pass. One failure is pre-existing on the base (e2e-coverage, release-gate dialog flows). The other, kitchen-sink's Windows rich-text test, comes from a stale local node_modules patch; a fresh install has not been tried.
  • bun run e2e:validate and bun run e2e:lint: 0 problems (79 flows).
  • bun.lock agrees with every workspace manifest (static check).

macOS, on a2531141 plus the gate commits (99c74c64):

  • Kitchen Sink macOS Debug build succeeds. e2e:run e2e/flows --backend driver: 72 pass, 23 fail, 4 skip; every failure uses a step the driver backend does not support yet, or was a stale dock golden (now updated, 4/4 pass).
  • bun run gate:pointer (gesture-handler, pressable-pointer with --pointer): 2/2 pass.
  • kitchen-sink, e2e-coverage and e2e-gate tests after a fresh install: 44/44 pass.

Not run: Windows, Intel macOS, the full test suite locally (CI runs it).

Move the SDK matrix to Expo 55.0.32, React Native 0.83.10, React 19.2.0,
React Native macOS 0.83.0 and React Native Windows 0.83.2, with the
Expo Desktop SDK 55 packages (template 55.83.10, metro-config 55.83.3,
modules-core and stubs 55.0.2). Expo Desktop CLI, prebuild-config and
config-plugins stay on their SDK-agnostic 1.x releases.

- Regenerate the expo-audio patch on 55.0.18; upstream now cancels
  stale artwork loads but still delivers them off the main thread and
  keeps artwork across sessions.
- Retarget the RN macOS source patches, RNW geometry adapter, Expo CLI
  key patch and autolinking symlink patch; their anchors still match.
- Port the iOS and Android @expo/ui adapters to the 55 API.
- Raise React peer floors to ^19.2.0.
RNW 0.83.2's CLI requires @react-native-windows/find-dotnet-tools without
declaring it, and its react-native.config.js probes for PowerShell when
Expo 55 autolinking loads it on macOS. Depend on the helper as Expo
Desktop's template does, and exclude RNW from macOS autolinking.
Template 55.83.10 replaces the Objective-C host with a SwiftUI App and a
Swift ExpoAppDelegate, so the plugin wrote Spark's AppDelegate.mm into
AppDelegate.swift. Swap the Swift delegate for AppDelegate.mm, main.m and
the menu storyboard (from the SDK 54 template) in the macOS target.
The Expo Desktop 55 Podfile strips this alias from expo-modules-core
during pod install, after Spark fingerprints the package, so the first
spark dev after every build asked for a rebuild. Apply the same edit
before collecting native packages.
Expo 55 binds --localhost to "localhost", which Node resolves to ::1,
while desktop runtimes and Spark's Metro clients connect to 127.0.0.1.
Prefer IPv4 in the Expo process so the app can load its bundle.
MMKV builds on macOS with Spark's Nitro patches, but its default store was
~/Documents/mmkv, the user's TCC-protected Documents folder shared by every
app. A patch moves it to Application Support under SparkNamespace(), like
Spark's other app data.

Optional pins in patches/dependencies.json apply a patch when the app itself
installs the pinned version; other versions stay unpatched.

Adds a Kitchen Sink MMKV screen and a driver flow covering typed values,
listeners, persistence across launches and clearState.
Unistyles 3.6.0 ships only a UIKit native runtime. Its optional patch adds
a macOS podspec platform and an AppKit runtime: the main window's size and
backing scale, appearance changes for adaptive themes, and RTL. Insets,
bars and the keyboard report zero on desktop.

Adds a Kitchen Sink Unistyles screen, with the Babel plugin scoped to it,
and a driver flow that flips the app appearance and sees the theme follow.
Quick Crypto already targets macOS and its OpenSSL has a macOS slice, but
fastpbkdf2 includes <endian.h>, which macOS lacks; the patch uses
<machine/endian.h> as Mac Catalyst does. Its peer Quick Base64 declares
only iOS, so macOS skipped linking it and the first import failed; its
patch adds the macOS platform.

Adds a Kitchen Sink security screen and a driver flow checking hashes,
HMAC, PBKDF2, AES-GCM, random values and WebCrypto digest vectors.
Nitro Fetch's URLSession client compiles for AppKit, but its podspec
declares only iOS and tvOS. The patch adds macOS, runs launch prefetches
on NSApplicationDidFinishLaunching, and gives its URLCache an absolute
directory under the app's namespaced Caches folder: the relative diskPath
put Cache.db in the process's working directory on macOS.

Adds a Kitchen Sink networking screen that asserts the native client is
present (the JS otherwise falls back to global fetch) and a driver flow.
Blur is UIKit only, and without ios.componentProvider in its codegen
config RN 0.83 never registers its Fabric components. The patch declares
both components and adds AppKit BlurView and VibrancyView on
NSVisualEffectView materials, with light/dark styles mapped to appearances
and the reduced-transparency fallback. blurAmount has no AppKit
equivalent.

Adds a Kitchen Sink blur screen and a driver flow checking registration,
layout and vibrancy children.
Worklets Core's native code is platform-neutral, but its podspec declares
only iOS; the patch adds macOS. The Kitchen Sink adds the worklet Babel
plugin and the three Babel plugins it requires, which Expo SDK 55 apps no
longer carry.

Adds a Kitchen Sink screen and a driver flow running a worklet context,
a shared value and a call back to the main runtime.
CocoaPods skips a pod whose podspec lists platforms without macOS, but
codegen still registered the library's Fabric components and TurboModules.
The registry then mapped them to classes that were never built, and the app
crashed at launch inserting nil into the component dictionary
(react-native-fast-image). Spark's macOS native selection now excludes
those codegen libraries from the macOS Podfile only; iOS and Android builds
reading the shared react-native.config.js and Expo autolinking settings
keep them.

The podspec check reads the platforms assignment itself, singular
s.platform or plural s.platforms, in either :osx => or osx: syntax, so
react-native-gesture-handler (platforms { ios: ..., osx: ... }) stays in.
FastImage's native view is UIKit only. Its optional patch adds a macOS
entry that renders FastImage and FastImageBackground with React Native's
Image: sources, objectFit, tint, placeholders while loading, load events,
reload and preload. Cache APIs resolve not ok. onLoad reports the size from
Image.getSize, since React Native macOS scales a 1x image's load size by
the screen's.

Adds a Kitchen Sink screen and an offline driver flow.
The profiler's Hermes module has no UIKit code, but its podspec lists only
iOS. The patch adds macOS and writes traces to the app's namespaced Caches
folder: on a Mac, the root of ~/Library/Caches is shared by every app.

Adds a Kitchen Sink devtools screen and a driver flow that profiles busy
JavaScript and reads the trace back.
prepareNativePatches returned an optional pin it declined to patch (another
version installed), so non-desktop autolinking still forced that copy.
Record a package only once its version matches the pin. Also call the MMKV
check's file a file.
ATTrackingManager exists on macOS 11, but react-native-tracking-transparency
lists only iOS, checks UIApplication's state before prompting, and its
JavaScript returns unavailable off iOS. The patch adds macOS with AppKit's
activation check and lets macOS reach the native module.

Adds a Kitchen Sink security screen and a driver flow reading the status.
Skia 2.14 supports macOS upstream and needs no Spark patch. Adds a Kitchen
Sink screen and a driver flow that read back an on-screen Canvas and an
offscreen raster surface by pixel.

The Worklets Core Babel plugin, applied app-wide, rewrote Skia's worklets
and read its optional Reanimated proxy at load, so the app never rendered.
The Kitchen Sink now applies that plugin only to its Worklets Core demo.
Writing the patch receipt reformats a dependency's package.json as
two-space JSON, so a package.json hunk that matched a tab-indented upstream
file failed on every later preparation. Normalize package.json before
applying hunks, so patches target one form.
Blurhash is UIKit only and its codegen config names no component provider,
so RN 0.83 never registered its view. The patch declares the provider and
adds AppKit support: an NSView showing the decoded image as layer contents,
NSImage decoding and encoding, and resize modes as contents gravity.

Adds a Kitchen Sink screen and a driver flow that decode a Blurhash in the
native view and encode a local image.
Its iOS-only native module is absent on macOS, and the JavaScript read a
property of it at import, so importing the library threw. The patch reads
it optionally, so desktop reports a 0 radius.

Adds a Kitchen Sink screen and a driver flow.
Nitro SQLite supports macOS upstream and links beside Spark's OP-SQLite,
since its SQLite symbols are prefixed. Adds a Kitchen Sink storage screen
and a driver flow covering parameterized, batched and async queries.
OSLogStore's current-process scope exists on macOS 12, but the library's
podspec lists only iOS and only its iOS bindings call native. The patch
adds macOS 12 to the podspec and a macOS entry using the iOS bindings.

Adds a Kitchen Sink devtools screen and a driver flow that read back a
console marker from the unified log.
Nitro ZXing is C++ on top of VisionCamera 5 and Nitro Image, which Spark
already builds for macOS; its podspec only lacked the platform.

Adds a Kitchen Sink media screen and a driver flow that scan a QR code
from a still image.
react-native-graph 1.4.0 builds its line paths with Skia 1's mutable SkPath
methods, which Skia 2 removed, so its graphs threw on render. The patch builds
them with Skia.PathBuilder instead. Kitchen Sink renders a static and an
animated graph with a gradient fill, with a driver flow.
Keep-awake always releases its tag, polls pmset instead of sleeping, and
tolerates the assertion development builds already hold. The drawer counts
onClose only after it opens, since it reports a close on mount. The bottom
sheet retries its snap until the sheet has measured itself. The pressable demo
gets the GestureHandlerRootView its native button requires.
The catalog golden still showed 6 populated areas from before the library
screens were added; it now matches the current catalog's counts and screens.
A blurhash, thumbhash, SF Symbol or ImageRef source rendered nothing and
fired no load events, so a screen waiting on onLoadEnd or onError waited
forever. Such a source now reports onLoadStart, onError and onLoadEnd once.
Sources also keep their width, height and scale, so a require()d @2x asset
draws at its point size rather than at 1x.
The macOS stand-in rebuilt object sources from uri and headers alone, so a
source's scale was lost and a @2x image drew at 1x, and a placeholder given
as { uri, headers } fell through to defaultSource and never showed. Sources
now keep width, height and scale, and uri placeholders load.
The date was applied before minimumDate and maximumDate, and NSDatePicker
clamps its date whenever the limits move, so one update changing the value
and both limits left the picker on a clamped date that JavaScript never
heard about. The limits now apply first, and the date is set after any
change to either.
…sing

Without NSLocationWhenInUseUsageDescription, expo-location calls RCTFatal,
which Debug builds swallow and Release builds throw, so a Release app
terminated on its first permission check. On macOS it now logs an error and
reports denied, as Debug did. The docs named NSLocationUsageDescription,
which CoreLocation and expo-location never read; they now name the
WhenInUse key.
The animated LineGraph wraps itself in a GestureDetector, which throws
without a GestureHandlerRootView in development builds, so the screen
showed the error in a dev Kitchen Sink. The e2e driver bundles with
__DEV__ off, which hid it.
Fourteen patched podspecs declared macOS 11 or 12, below Spark's macOS 14
floor. Masked View in particular uses macOS 14's CADisplayLink and only
built because the Podfile raised every pod's deployment target. They now
declare 14.0.
Driver windows are never shown, so an external CGWindow lookup cannot find
one for a real click. Add an opt-in --pointer backend using the existing
testID lookup and an external CGEventPostToPid helper.

Encode the events in the window-owning process so AppKit preserves the
window-local hit point. Show the window without presentation animation and
let the gesture demo accept its first background click on macOS. Missing
input-posting permission fails immediately without a prompt.

**Test plan**

Focused native protocol, pointer backend and existing driver backend checks
passed. Scoped strict TypeScript checking passed.

✅ Debug arm64 Kitchen Sink build via heavy, -jobs 6, no signing.

✅ Real native mouse-view probe observed one down and one up callback.

✅ Gesture-handler and drawer-layout flows via --backend driver --pointer:
GestureDetector's successful tap callback and drawer open/close were
observed, including a screenshot showing tap. The console remained locked.
An unknown window or a non-finite point now fails at once with E_NO_WINDOW or E_INVALID_ARGUMENT, as capture does, instead of waiting and reporting E_NOT_SETTLED.
…ettles

tapOn passed no timeout, so the driver's fixed 5 s settle wait applied and E_NOT_SETTLED failed the step. It now waits up to the step's timeout in slices under the driver's reply limit, re-resolving the target between them, and taps once the timeout is spent, as it did before the settle wait. pointerEvents with timeoutMs 0 skips the wait.
…ation

Main now requires every active pin to resolve to a prepared package. An optional
pin that finds another installed version stays unpatched on purpose, so it no
longer fails that check.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-31T06:57:54.834699Z
We couldn’t complete this review. Comment @codex review to try again.
a253114 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…ws outside the gate

The 50 library checks this branch adds pass on the driver backend, so the
release gate now blocks on them. The gesture-handler and pressable-pointer
flows need --pointer, which the gate does not accept, so they move to
e2e/pointer/flows/ and run with bun run gate:pointer before a release.
@react-native-community/blur and the other native libraries this branch adds
have no Windows implementation yet, so the Windows build excludes them like
the rich text libraries. Windows support comes on a separate branch.
The sidebar catalog counts changed as library screens were added, the title
bar tint changed slightly, and in Arabic the Dock screen title is now
translated and the sidebar uses the Latin digits the shell pins.
@jmeistrich

Copy link
Copy Markdown
Member Author

Replaced by #531 (same change rebased onto main after #529 merged).

@jmeistrich jmeistrich closed this Oct 31, 2026
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