Skip to content

Release v2.0.0 - #21

Merged
pedrol2b merged 45 commits into
mainfrom
develop
Aug 2, 2026
Merged

pedrol2b merged 45 commits into
mainfrom
develop

Conversation

@pedrol2b

@pedrol2b pedrol2b commented Aug 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

Major release: migrates the library from the legacy TurboModule bridge to VisionCamera v5 + Nitro Modules. This is a breaking release (v1 → v2) — see docs/migration-v1-to-v2.md.

  • Core rewrite: VisionCamera v5 + Nitro Modules migration, React Native bumped to 0.86.
  • Feature work: live-frame ML Kit wiring for both platforms, barcode scanning added as a full feature, shared Region-of-Interest (ROI) cropping for text recognition and barcode scanning, static-image processing (EXIF-aware orientation, full-resolution downsampling fix).
  • Platform fixes: image-orientation correctness (iOS), full BoundingBox field population (Android), a cross-Worklet-Runtime closure bug fix, Bottom Sheet dependency swap in the example app.
  • Distribution: Expo config plugin for selective ML Kit installs, plus a fix restoring true compile-time selective install on Android (Gradle conditional source sets) to match iOS's existing #if-based behavior — disabling textRecognition/barcodeScanning now genuinely removes the native SDK from the build on both platforms, not just at runtime.
  • CI hardening: fixed iOS (Xcode version pin, Bundler/Ruby compatibility, Turborepo env passthrough, xcodebuild destination string bug) and Android (codegen duplicate-class bug, missing CMake dependency) build failures uncovered while auditing the branch for release-readiness.
  • Docs/metadata: new docs/selective-mlkit-install.md and docs/expo-plugin.md, refreshed package.json description/keywords and GitHub repo description/topics to reflect the Nitro architecture, selective install, and Expo plugin.

package.json version is already 2.0.0 on this branch (no further bump needed before tagging).

Test plan

  • iOS and Android example app builds pass in CI on this branch
  • Full yarn test suite passes (93/93)
  • Locally verified selective install: barcodeScanning:false and textRecognition:false (+ all language variants) each compile cleanly and the corresponding com.google.mlkit:* artifact is fully absent from the resolved Gradle dependency graph
  • Smoke-test npm install react-native-vision-camera-mlkit@2.0.0 in a scratch project after publish

pedrol2b added 30 commits July 5, 2026 21:11
Add Nitro specs, native HybridObject scaffolding, Android CMake integration, and VisionCamera v5 example frame-output wiring.

Replace the old VisionCamera v4 frame processor plugin registration with the new Nitro-backed factory surface. Live ML Kit frame processing remains scaffolded for the next implementation phase.
Bumps react-native 0.81.1 -> 0.86.0, react 19.1.0 -> 19.2.3, and related
packages (reanimated, screens, svg, worklets, nitro-image, RN CLI) across
the workspace and example app.

Also bumps react-native-gesture-handler 2.30.0 -> 3.0.2, required because
RN 0.86 (New Architecture only) removed the legacy Paper-only renderer
shim (react-native/Libraries/Renderer/shims/ReactNative) that
gesture-handler 2.x still imported, breaking the Metro bundle step.

Regenerates native project files that follow from the upgrade: Gradle
wrapper 8.14.3 -> 9.3.1, MainApplication.kt bridgeless-only template,
AndroidManifest cleartext-traffic placeholder (folding the debug-only
manifest into main), Podfile.lock, Xcode project settings, Info.plist,
and jest preset.
The Android-to-Nitro BoundingBox mapping only set x/y/width/height,
leaving centerX, centerY, top, left, bottom and right at their zero
default even though the JS BoundingBox type declares all of them.
…ntime execution

useMLKitPlugin wrapped the native TextRecognizer/BarcodeScanner
HybridObject in a JS-level worklet function, and useTextRecognition/
useBarcodeScanning wrapped that again in their own worklet. A worklet
closing over another worklet, two levels deep, does not reliably survive
being re-shared into a different Worklet Runtime, degrading into a
non-callable object and throwing "Object is not a function" whenever
called through useAsyncRunner's native-thread-backed runtime.

useMLKitPlugin now returns the memoized native HybridObject directly, so
each feature hook only needs a single worklet layer closing directly
over it. CameraView also drops the useAsyncRunner hop entirely and calls
the plugin inside the top-level onFrame worklet, matching the pattern
the library's README documents.
…ive FPS graph

CameraView enabled VisionCamera v5's native gesture controllers
(enableNativeZoomGesture, enableNativeTapToFocusGesture) at the same time
as a hand-rolled react-native-gesture-handler GestureDetector for tap-to-
focus and double-tap-to-flip. Native UIKit/CameraX recognizers intercept
touches before RNGH's JS-driven detector ever sees them, so enabling any
native gesture silenced the RNGH gestures entirely, and disabling zoom
let pinch touches misfire as double-taps. It also meant tap-to-focus
never called back into JS, so the focus-ring overlay never rendered.

All gestures now live in one RNGH composition: pinch-to-zoom drives a
Reanimated-backed zoom SharedValue via Gesture.Pinch(), combined with
the existing tap/double-tap Gesture.Exclusive() through
Gesture.Simultaneous(). The native gesture props are removed.

Also:
- Fixes a Reanimated "reading `value` during render" violation where
  `savedZoom` was seeded by reading `zoom.value` instead of the same
  plain initial expression.
- Stops logging the expected "metering operation has been canceled"
  rejection that fires whenever a new focus request supersedes a still-
  settling one.
- Restores the FPS graph toggle, which the v5 migration left as a dead
  no-op (state value was discarded and the `enableFpsGraph` Camera prop
  it fed was removed upstream in v5). Frame arrivals are now counted in
  the onFrame worklet and sampled on an interval to drive a real
  FpsGraphOverlay component.
…ted compatibility

The Terminal bottom sheet presented but never animated open. Installed
5.2.8 predates react-native-gesture-handler v3.0.0 (2026-05-28) and
bottom-sheet's own "Full support for react-native-reanimated 4 and
worklets" issue was still open upstream at the time. 5.2.14 was already
within the declared "^5" range but had never been re-resolved.
… orientation settings

Surfaces VisionCamera v5 Camera-behavior capabilities that were fully
supported by the library but never exposed in the Settings tab:

- Exposure Bias slider, clamped to the device's supported range.
- Low Light Boost switch, gated on device.supportsLowLightBoost - the
  enableLowLightBoost prop is only included on <Camera /> when it should
  be true, since passing it as false still triggers native validation and
  throws on devices that don't support the feature at all.
- Target FPS constraint picker (15/24/30/60/120), applied after the
  existing resolutionBias constraint so the ML frame output's resolution
  match keeps priority.
- Video Stabilization mode picker, covering all 8 real
  TargetStabilizationMode values.
- Orientation Source picker (interface/device).

Also cleans up stale copy: relabels "Frame Processor FPS (runAtTargetFps)"
(runAtTargetFps doesn't exist in v5) to describe the actual JS-side frame
throttle, and removes the deprecated "Frame Process Interval" picker,
which duplicated that throttle at the native level via an option already
marked @deprecated.
README.md, docs/text-recognition.md, and docs/barcode-scanning.md still
documented the removed v4 API end-to-end (useFrameProcessor,
runAsync(frame, cb), runAtTargetFps, react-native-worklets-core plus its
Babel plugin) and listed the wrong peer dependencies, even though the
library and example app were migrated to v5 earlier. runAtTargetFps no
longer exists anywhere in react-native-vision-camera's source.

Replaces every v4 reference with the real v5 usage (useFrameOutput,
direct in-worklet calls, frame.dispose(), the actual v5 peer deps and
Babel plugin), and points links at visioncamera.margelo.com instead of
the old react-native-vision-camera.com domain.

Also rewords the frameProcessInterval JSDoc in src/core/types.ts, which
pointed consumers at the same removed runAtTargetFps API.
pedrol2b added 14 commits July 13, 2026 12:11
…on and barcode scanning (#13)

* feat(core): add shared RegionOfInterest type and validator

Introduces the public RegionOfInterest/RegionOfInterestUnit types and a
fail-fast JS-side validator, wired into both the live-frame (PluginFactory)
and static-image (NativeBridge) paths before any native call is made.
Extends the Nitro-facing MLKitRecognizerOptions so the field actually
crosses the bridge for text recognition and barcode scanning.

* feat(android): implement region of interest crop and coordinate remap

Crops the working bitmap to the resolved ROI pixel rect after rotation and
before scale/invert, on both the live-frame (Nitro) and static-image
(legacy bridge) paths for text recognition and barcode scanning. Disables
the raw-MediaImage fast path whenever an ROI is set, since it can't crop.

Adds a shared coordinate-remap primitive that maps result bounding boxes
and corner points back to full source-frame coordinates using the
recorded crop offset and scale factor - this also fixes result
coordinates for the pre-existing scaleFactor option, which previously had
no remapping at all.

* feat(ios): implement region of interest crop and coordinate remap

Mirrors the Android ROI implementation: crops via CGImage/CIImage after
orientation resolution and before scale/invert, on both the live-frame
(Nitro) and static-image (legacy bridge) paths for text recognition and
barcode scanning. Disables the raw-CMSampleBuffer fast path whenever an
ROI is set, since it can't crop.

Adds the same shared coordinate-remap primitive as Android, mapping
result coordinates back to full source-frame space using the recorded
crop offset and scale factor. Domain type is named DomainRegionOfInterest
(file included) to avoid colliding with the Nitro-generated RegionOfInterest,
matching the existing DomainOrientation/Orientation precedent.

Also fixes two smaller pre-existing gaps hit while wiring this through:
RecognizeBarcodesUseCase's static-image overload was dropping
outputOrientation when reconstructing ImagePreprocessingOptions, and a
CMSampleBuffer conversion-failure path used fatalError instead of failing
gracefully - now reachable in normal operation via ROI validation.

Includes the Podfile.lock update from re-running pod install after adding
the new native files.

* feat(example): add region of interest control and camera overlay

Adds a shared RegionOfInterestControl (enable switch + 4 normalized
sliders) used by both the Settings tab (live camera options) and the
Image screen (static-image options), plus an ROIOverlay component that
renders the configured rectangle over the camera preview - both for
composing an ROI and for visually confirming the native crop/remap lines
up with the recognized result coordinates.

* docs: document the roi option for text recognition and barcode scanning

Adds a roi row to every frame/image options list in README.md and both
feature docs, plus a "Region of Interest" reference section in the main
README explaining the type shape, default normalized coordinates, and
usage.

* test: add unit tests for validateRegionOfInterest

Covers valid normalized and pixel regions, negative x/y, non-positive
width/height, and normalized x+width/y+height exceeding 1.
Swift-only native modules are never auto-discovered by React Native's
bridge; they require a companion Objective-C file using RCT_EXTERN_MODULE
to actually register the class. That file never existed for
VisionCameraMLKitModule, so TurboModuleRegistry.getEnforcing threw at
runtime for any static-image call (processImageTextRecognition /
processImageBarcodeScanning) on iOS - confirmed via device testing.

Pre-existing since the original v1 rewrite, unrelated to the ROI feature;
never caught because the static-image path had not been exercised on a
real iOS build before. The live-frame path is unaffected - it uses Nitro
HybridObjects, a separate registration mechanism, already working.

Android is unaffected: its equivalent module registers explicitly via
BaseReactPackage/ReactModuleInfoProvider (isTurboModule = false), which
does not depend on this bridge file.
…ve-bottom-sheet (#14)

* fix(example): replace @gorhom/bottom-sheet with @swmansion/react-native-bottom-sheet

Removes the suspected root cause of the reverted Terminal sheet toggle and
close-on-blur bugs: the new library's sheet host, gestures, snapping, and
scroll negotiation all run natively and depend on neither
react-native-gesture-handler nor react-native-reanimated, unlike @gorhom's
implementation which was never validated against our RNGH v3 combo.

TerminalProvider moves from an imperative BottomSheetModal ref
(present()/dismiss()) to a controlled index state (0 closed, 1 open), and
stabilizes open/close with useCallback (previously recreated on every isOpen
change). TerminalBottomSheet swaps ModalBottomSheet/FlatList in for
BottomSheetModal/BottomSheetFlatList and hand-builds the handle pill and
rounded background the old library provided by default, to keep the same
visual styling. Percentage snap points (25/60/90%) become pixel detents
derived from useWindowDimensions, with 0 as the new explicit closed detent.

Scoped narrowly to the swap itself: behavior (button-only open, no toggle,
no auto-close-on-blur) is unchanged from before. Toggle/close-on-blur remain
a separate follow-up, now cheaper thanks to the controlled index model.

* fix(example): restore sheet z-order and add Terminal toggle/close-on-blur

The library swap dropped @gorhom's containerStyle zIndex/elevation
(assumed to be gorhom-specific boilerplate), which was actually what kept
the Terminal sheet above PortalProvider's zIndex:1/elevation:1 host view
(used by PortalSegmentedToggle). Restored via ModalBottomSheet's `style`
prop, since the new library exposes no equivalent containerStyle prop
directly.

Also delivers the toggle and close-on-blur behavior called out as
out-of-scope in the previous commit, now that it's cheap with the
controlled-index model:

- TerminalProvider exposes a stabilized `toggle()` alongside open/close.
- CameraControls' terminal button (icon-only, no explicit label) now
  toggles instead of only ever opening; renamed onOpenTerminal ->
  onToggleTerminal to match the sibling onToggleTorch/onToggleFpsGraph
  props. ImageScreen's two explicit "open" call sites (auto-open after
  processing, "Open Terminal" button) are unchanged, since toggling those
  would be the wrong affordance.
- CameraScreen and ImageScreen close the terminal via useFocusEffect's
  cleanup on blur (covers Settings navigation, Home, and switching
  between Camera/Image through PortalSegmentedToggle's stack replace),
  instead of the previous global NavigationContainer.onStateChange
  attempt that broke opening the sheet.
)

react-native-vision-camera-worklets and react-native-worklets were never
imported anywhere in src/, android/, or ios/ - the library's live-frame
hooks are worklet-callable Nitro HybridObject methods, not code that
imports the Worklets packages directly. They're an app-level opt-in
required to use react-native-vision-camera's frame-processor feature at
all, a requirement flowing from Vision Camera's own frame-processor API
contract, not from this library. Vision Camera itself doesn't declare them
as peer dependencies either, confirming this. Blanketing them as required
peer deps also wrongly implied the static-image APIs
(processImageTextRecognition/processImageBarcodeScanning, which go through
the classic native bridge and never touch Vision Camera or Worklets) need
a camera at all.

react-native-nitro-image is similarly unused anywhere in this library's
own code - confirmed absent from src/, android/src, ios/, nitro.json, and
the iOS podspec. It's a hard transitive peer/pod dependency of
react-native-vision-camera itself (its own peerDependencies and podspec
both declare it), so consumers still get nagged for it via Vision
Camera's own peer-dependency declaration - verified empirically via `yarn
install`, which surfaces "react-native-vision-camera-mlkit doesn't provide
react-native-nitro-image ... requested by react-native-vision-camera"
without this library redeclaring it.

Also removes the matching unused `implementation project(":react-native-nitro-image")`
line from android/build.gradle - not referenced by any Kotlin source or
generated Nitrogen output on either platform.

README updated to reflect the real dependency shape: react-native-vision-camera
+ react-native-nitro-modules for the base install, react-native-vision-camera-worklets
+ react-native-worklets only if using the live-frame hooks, and an explicit
callout that the static-image APIs need neither.
…and npm metadata (#17)

* docs: stop overselling unimplemented ML Kit features in npm metadata and README

The npm description and keywords listed face detection and pose detection
alongside text recognition and barcode scanning as if all were delivered.
Only text recognition v2 and barcode scanning are implemented in src/ (see
the README's own roadmap table) - everything else is unimplemented Phase 3
work. Reworded the description to reflect what ships today and frame the
rest as roadmap, and dropped face-detection/pose-detection from keywords
since they misrepresent the package's actual capabilities to anyone
evaluating it on npm. Applied the same fix to the README's intro paragraph,
which had the identical problem.

Also restructured the README's iOS troubleshooting content: the Apple
Silicon simulator note was oddly nested under "iOS Orientation Notes" (an
unrelated topic), and docs/xcode-26.4-fmt-consteval-workaround.md was never
linked from anywhere. Moved both into a new "Troubleshooting" section.

* docs: expand thin API result-shape sections in feature docs

docs/text-recognition.md's "Result shape" section just named the nesting
levels without describing fields, despite the page's stated goal of
documenting the API "in more detail than the README quick-start". Expanded
it into the full TextRecognitionResult field breakdown (bounds/corners/
languages at every level, Android-only confidence/angle/symbols), sourced
from src/features/text-recognition/types.ts, and added the missing
TextRecognitionResult type to the import example.

docs/barcode-scanning.md's BarcodeData field list didn't explain that
format/valueType are raw ML Kit numeric codes versus the formatName/
valueTypeName string equivalents apps should actually match against, or
that bounds/corners can be absent and rawValue/rawBytes can be null for
potential barcodes - both surprising to a new consumer without reading
BarcodeData's type definition directly.

* docs: fix inaccurate claims and gaps in CONTRIBUTING and CODE_OF_CONDUCT

CONTRIBUTING.md:
- Claimed pre-commit hooks verify tests pass; lefthook.yml's pre-commit
  only runs eslint and tsc, and the commit message check lives in a
  separate commit-msg hook, not pre-commit. Corrected both claims and
  pointed contributors at `yarn test` before opening a PR.
- Referenced "the pull request template" but no
  .github/PULL_REQUEST_TEMPLATE.md exists in this repo - replaced with a
  pointer to the issue templates that do exist.
- This is a Nitro Module (native interface in src/specs/*.nitro.ts) but
  CONTRIBUTING never mentioned `yarn specs` (nitrogen codegen), needed
  whenever a .nitro.ts spec file changes. Added a "Working with the Nitro
  spec" section and the missing script to the Scripts list, plus `yarn
  clean` and `yarn prepare`.
- Linked docs/xcode-26.4-fmt-consteval-workaround.md, which was previously
  unreferenced from anywhere in the repo despite being directly relevant to
  building the example app's iOS target.
- Commit type list was missing `perf`, `ci`, and `build`, all of which are
  actually used in this repo's git history per commitlint's conventional
  config.

CODE_OF_CONDUCT.md:
- Directed harassment/conduct reports to the public GitHub issue tracker,
  which isn't private - inconsistent with SECURITY.md's own private
  reporting channel for vulnerabilities. Switched to a direct email
  contact.
* chore: replace labels.yml with updated shared taxonomy

Adopt the new personal cross-repo label taxonomy (type/status/priority/
platform/technology/area/impact/pr/contribution), trimmed to what
actually applies to this repo: drops backend/web platforms, Java,
Objective-C, Flutter, and Dart (Android impl is Kotlin-only, iOS impl
is Swift-only, no Flutter/Dart anywhere), and drops analytics, auth,
database, design-system, localization, notifications, offline,
synchronization, and the networking-flavored "api" area, plus
"impact: data loss" — none of these concepts exist in this camera/OCR
frame-processor library. Kept accessibility/navigation/ui-ux since the
example/ demo app has real navigation, UI components, and
accessibility labels.

* chore: align workflow and issue-template label references with new taxonomy

Update the stale-bot config (stale/exempt/only labels) and the bug
report, build error, and feature request issue templates to reference
the new label names instead of the old emoji-prefixed ones. GitHub
Actions label matching is exact-string, so leftover references to the
old names would have silently stopped working once labels.yml changed.
* chore: untrack personal AI tooling state from .claude/.codex

Audit of .agents/.claude/.codex (added by an ECC auto-setup process),
using the same rationale that got .context/ gitignored: keep genuinely
shareable repo-specific tooling tracked (skills, slash-command scaffolds,
Codex sub-agent role definitions - none of it contains personal data),
but untrack files that are personal/machine-specific state:

- .claude/identity.json: user's personal AI verbosity/style/domain
  preferences, not a project fact.
- .claude/ecc-tools.json: ECC tool bookkeeping/state manifest with
  generation timestamps and readiness scores.
- .claude/homunculus/instincts/inherited/*.yaml: personal continuous-
  learning cache, content overlaps with the tracked SKILL.md docs.
- .codex/config.toml: personal sandbox/approval policy and MCP server
  list - the Codex equivalent of .claude/settings.local.json, which is
  already untracked.

Files stay on disk (not deleted), just removed from git's index, so
local AI tooling keeps working. .gitignore gains a new section for
these patterns, appended alongside (not replacing) the existing
.context/ entry.

* chore: gitignore the untracked personal AI tooling state files

* chore: untrack .agents/ AI tooling scaffolding as well

Not personal data (verified byte-identical to the kept .claude/skills/
copy, plus generic tool-discovery metadata) - untracked by preference,
to keep the repo free of visible ECC-generated AI-tooling scaffolding
regardless of whether it's personal. Files stay on disk.
Removes the last tracked ECC-generated files (.claude/commands/*.md,
.claude/skills/.../SKILL.md, .codex/AGENTS.md, .codex/agents/*.toml) -
previously kept on a personal-vs-shareable distinction, but going fully
local-only for now regardless of that distinction, matching the .agents/
call. Files stay on disk, just removed from git's index.

.gitignore simplified to blanket-ignore .agents/, .claude/, and .codex/
instead of itemized file patterns, since nothing under them is tracked
anymore.

What AI tooling (if any) should be shared via the git remote is left as
an open question to revisit later, not decided by this commit.
Delivers Phase 4 of the roadmap: an app.plugin.js so Expo prebuild users
don't have to hand-edit android/build.gradle / ios/Podfile to selectively
enable ML Kit models, mirroring the existing manual instructions in the
README exactly (same flag names, same defaults left untouched for any key
the consumer omits).

- plugin/mlkitConfig.js - the 16 documented feature keys, fail-fast
  validation (throws on unknown keys instead of silently ignoring typos,
  matching this repo's validateRegionOfInterest.ts precedent).
- plugin/withAndroid.js - prepends the `ext["react-native-vision-camera-mlkit"]`
  Groovy block to the project-level android/build.gradle via
  withProjectBuildGradle; throws on Kotlin DSL (.kts), which the native
  config doesn't support either.
- plugin/withIOS.js - inserts the `$VisionCameraMLKit` Ruby hash before the
  first `target` block in ios/Podfile via withPodfile, dropping the three
  Android-only keys (faceMeshDetection, subjectSegmentation,
  documentScanner) that don't exist in the podspec's config.
- Both mods are idempotent (marker-checked, not just createRunOncePlugin's
  own dedup, since a real `expo prebuild` invocation builds a fresh config
  object each run) - verified end-to-end against real files via
  compileModsAsync, not just the pure string-transform unit tests.

@expo/config-plugins added as an optional peer dependency (peerDependenciesMeta)
- Expo projects already have it transitively via `expo` itself; non-Expo
  consumers never need it, matching this session's earlier peer-dependency
  cleanup (#15).

README: new "Expo (config plugin)" subsection under ML Kit Models
Installation, and the old blanket "follow the Vision Camera guide" note
now clarifies that's for Vision Camera's own Expo setup, not this plugin.
…w fixes) (#20)

* feat: harden v2 Nitro release architecture

* fix(ios): downsample full-resolution static images

* fix: address v2 release-hardening review findings

- android: recycle intermediate bitmaps in ImagePreprocessor, guarded by
  reference-equality checks since Bitmap.createBitmap/scale return the
  source instance when no transform is actually applied
- android: serialize access to the shared ML Kit detector between the
  synchronous frame-processor path and the async static-image path
- ios: skip the redundant re-render pass in normalized(to:) when the
  target orientation is already .up
- ci: validate the release tag before building the package in
  npm-release.yml, so a mismatch fails fast
- chore: drop native-tests/ios/StaticImageDecodePlan.test.swift, the
  test:ios-static script, and its CI wiring; the hardcoded
  /private/tmp paths and hand-rolled swiftc compile step weren't worth
  keeping without a proper SPM/XCTest target

* fix(ci): stop pinning a hardcoded Xcode version in build-ios.yml

The XCODE_VERSION: 16.3 pin predates this branch and no longer exists
on GitHub's macos-latest runner image (only 26.0.1-26.6.0 are
installed now, per Apple's year-aligned Xcode versioning). Switch to
latest-stable so this doesn't silently break again next time GitHub
rotates old Xcode versions out of the image.

* fix(ci): unpin stale Bundler version that crashes on Ruby 3.4

example/Gemfile.lock's BUNDLED WITH 2.1.4 forces bundle install to
download and switch to that ancient Bundler on every run, but its
vendored thor lib references DidYouMean::SPELL_CHECKERS, which no
longer exists on Ruby 3.4 (the default on current macOS runners) -
crashing before pod install ever runs. Bump to 4.0.17, the version
already installed and working on the runner before the forced
downgrade kicked in.

* fix(android): scope RNGP codegen resolution to this library's package

android/build.gradle sits at the repo root and is linked into the
example app by local path, unlike every other native dependency here
(all node_modules-nested). Without jsRootDir set, RNGP appears to
resolve the JS root against this package's own package.json, which
also declares "workspaces": ["example"], causing it to aggregate
codegen output for every native module in the workspace - including
@swmansion/react-native-bottom-sheet's - duplicating a class that
module already builds for itself and failing app:mergeLibDexDebug.

* fix(ci): forward IOS_DESTINATION into the build:ios turbo task

turbo.json's build:ios task didn't allowlist IOS_DESTINATION, so
Turborepo stripped it from the task's environment before running
"react-native build-ios --destination \"\${IOS_DESTINATION:-...}\"".
The script's fallback (generic/platform=iOS Simulator) then hit
xcodebuild's own destination handling and failed instantly - "xcodebuild
exited with error code '64'" with no compile output, less than 2ms after
starting. Allowlisting it lets the simulator destination computed by
build-ios.yml actually reach the build command.

* fix(ci): stop duplicating the platform key in the iOS build destination

react-native build-ios unconditionally prepends its own
"generic/platform=iOS Simulator," to whatever --destination value is
passed - it never replaces it. Our computed IOS_DESTINATION already
included "platform=iOS Simulator", so the combined string ended up
with two "platform=" keys ("generic/platform=iOS Simulator,platform=iOS
Simulator,OS=...,name=..."), which xcodebuild rejected instantly with
exit code 64 and zero compile output. Drop the redundant prefix from
both the computed value and the local-dev fallback.

Also revert the earlier jsRootDir addition to android/build.gradle: two
CI runs confirmed it had no effect on the BottomSheetSurfaceViewManagerDelegate
duplicate-class error, and the yarn-hoisting theory behind it doesn't
hold (the package isn't hoisted to the repo root node_modules), so it
wasn't worth keeping as unverified guesswork.

* fix(android): restore the package's own react-native.config.js

Every other native dependency in the tree (nitro-image, vision-camera,
vision-camera-worklets) ships an identical self-descriptor at its
package root declaring empty ios/android platforms. Ours was missing
entirely, even though package.json's files list expects it to exist.

Without it, example/react-native.config.js worked around the gap by
inlining an empty platforms override directly on the manual
dependencies[pkg.name] entry. That's a different code path than a
proper self-descriptor, and correlates with the one concrete anomaly
in CI: every sibling library's generateCodegenSchemaFromJavaScript
task logs "No modules to process" - ours didn't, and our own module
ended up compiling com.facebook.react.viewmanagers.BottomSheetSurface-
ViewManagerDelegate into itself, colliding with bottom-sheet's own
copy at dex-merge time.

Restoring the root config and dropping the now-redundant inline
override lets the CLI resolve our package the same way it resolves
every other properly-behaved dependency.

* fix(android): scope codegen to our own src/specs directory

Root cause found via a stray local build artifact
(android/build/generated/source/codegen/schema.json): our library's
generateCodegenSchemaFromJavaScript task was producing a 158-module
schema covering the ENTIRE example app's dependency tree (BlurView,
RNGestureHandler, RNSVG, BottomSheetSurfaceView, etc.), not just our
own package. That's why our module compiled its own copy of
com.facebook.react.viewmanagers.BottomSheetSurfaceViewManagerDelegate,
colliding with bottom-sheet's own copy at D8 dex-merge time.

Without a codegenConfig, RNGP has nothing to scope the schema
combiner to just our package, so it fell back to scanning our whole
package root - which, unlike sibling npm packages, has the entire
example/ workspace nested inside it.

Verified locally with combine-js-to-schema-cli:
  - scanning "." (previous behavior): 158 modules, includes
    BottomSheetSurfaceView
  - scanning "src/specs" (this fix): 0 modules, "No modules to
    process" - matching every other Nitro library in the tree

src/specs only contains .nitro.ts files, which never match the old
architecture's codegenNativeComponent/TurboModuleRegistry patterns,
so scoping codegen there is safe and produces an empty schema by
design.

* fix(android): drop unused react-native-nitro-image CMake dependency

The dex-merge duplicate that failed earlier builds was masking a
second issue: android/build.gradle never declares a Gradle dependency
on react-native-nitro-image, so its prefab package was never on
CMAKE_FIND_ROOT_PATH, and find_package(react-native-nitro-image
REQUIRED) failed with "Findreact-native-nitro-image.cmake not found".

Verified react-native-nitro-image is never referenced anywhere in
cpp/, nitrogen/, or src/ - it's unused leftover from a template, not
an actual dependency. Removing the find_package call and its
target_link_libraries entry fixes the CMake configure step without
touching anything the library actually uses.
…tion and barcode scanning

The v2 hardening PR fixed a real compile failure by making
com.google.mlkit:text-recognition and com.google.mlkit:barcode-scanning
unconditional dependencies, defeating the library's selective-install
purpose: a consumer who only wants barcode scanning still got the
full text-recognition SDK bundled, and vice versa.

Root cause: Kotlin has no #if-equivalent preprocessor. Six Kotlin files
(TextRecognitionServiceFactory/MLKitTextRecognitionService/MLKitTextAdapter
and their barcode equivalents) import concrete MLKit types at file
scope, so they fail to compile whenever the corresponding artifact is
missing from the classpath - regardless of whether the runtime feature
flag ever lets that code execute. iOS never had this problem since its
Swift equivalents use #if MLKIT_TEXT_RECOGNITION_ANY / #if
MLKIT_BARCODE_SCANNING, which genuinely elides the guarded code (and
its imports) at compile time.

Fixed by giving Kotlin the same guarantee via Gradle conditional
source sets instead of reflection (MLKit's real client APIs return
async Task<T> objects with listener callbacks, which would need a
java.lang.reflect.Proxy to invoke reflectively - fragile under R8 and
adds reflection overhead to code that runs every camera frame):

- Moved the 6 MLKit-coupled files into textRecognitionEnabled/
  barcodeScanningEnabled source sets (content unchanged, path only).
- Added tiny stub factories in textRecognitionDisabled/
  barcodeScanningDisabled that throw if ever reached (they aren't -
  HybridVisionCameraMLKit already gates construction before this
  point), existing purely so the always-compiled Hybrid* classes have
  something to compile against.
- android/build.gradle picks the enabled/disabled source dir per flag,
  and re-gates the two base MLKit SDKs behind their flags again.
- Added com.google.mlkit:vision-common as an explicit, always-on
  dependency: three files (ProcessedImage, ImagePreprocessor,
  HybridFrameSpec+toProcessedImage) use its InputImage type
  unconditionally for the shared image-preprocessing pipeline. This
  isn't a version bump - it's already resolved transitively today at
  the same 17.3.0.
- Updated the README caveat that documented this exact limitation,
  since it's no longer true.

Verified locally (not just "it compiles"): with barcodeScanning:false,
com.google.mlkit:barcode-scanning is fully absent from the resolved
debugRuntimeClasspath; with textRecognition and all 4 language
variants false, com.google.mlkit:text-recognition is fully absent.
Full yarn test suite passes (93/93).
Audited README.md and docs/*.md against what v2 actually ships.
Content was accurate, but two Expo plugin behaviors were undocumented
anywhere (Android-only keys silently dropped from the iOS Podfile
hash, and no Kotlin DSL/.kts support), and neither selective install
nor the Expo plugin had a dedicated doc - both only existed condensed
in the README.

Add docs/selective-mlkit-install.md: what "selective" guarantees now
(compile-time removal on both platforms per the recent Android fix,
not just runtime gating), defaults table, Android/iOS/Expo setup, and
how to verify a disabled feature's SDK is actually absent from the
resolved dependency graph.

Add docs/expo-plugin.md: usage examples, what the plugin actually
writes and where, validation behavior, and the previously-undocumented
platform caveats (Android-only key dropping, Groovy-only support,
idempotency via marker comments, createRunOncePlugin behavior).

Link both from README's API Docs list and from the inline Expo
section.
Description now mentions the selective/optional install guarantee
restored this release. Keywords gain nitro, expo, and config-plugin
to reflect v2's Nitro architecture and the Expo config plugin, both
of which had no keyword coverage before.
@ecc-tools

ecc-tools Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

…it stubs

The textRecognitionDisabled/barcodeScanningDisabled stub factories
added for selective install had single-throw block bodies, which
ktlint's standard:function-expression-body rule flags in favor of
= throw ... expression-body form. Auto-fixed via `ktlint --format`;
no behavior change.
@ecc-tools

ecc-tools Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

@pedrol2b pedrol2b self-assigned this Aug 2, 2026
@pedrol2b
pedrol2b merged commit 433997d into main Aug 2, 2026
16 checks passed
@pedrol2b
pedrol2b deleted the develop branch August 2, 2026 22:20
@pedrol2b
pedrol2b restored the develop branch August 2, 2026 22:25
@pedrol2b pedrol2b added the type: breaking change Introduces an incompatible change that may require migration label Aug 2, 2026
@pedrol2b
pedrol2b deleted the develop branch August 2, 2026 22:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: breaking change Introduces an incompatible change that may require migration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant