Munchable's label camera has tap to focus. You point the phone at an ingredients panel, the autofocus hunts, you tap the words and it locks on them for a few seconds before handing back to continuous focus.
expo-camera does not ship that. So the repo carries a patch, and the whole of it is two lines of config:
# pnpm-workspace.yaml
patchedDependencies:
expo-camera@57.0.5: patches/expo-camera@57.0.5.patch
This is a post about those two lines rather than about the camera, because today I fixed a bug in them that no tool in the repository could have found.
What is in a 221-line patch
Ten files:
android/src/main/java/expo/modules/camera/CameraViewModule.kt
android/src/main/java/expo/modules/camera/ExpoCameraView.kt
ios/CameraViewModule.swift
ios/Current/CameraSessionManager.swift
ios/Current/CameraView.swift
src/Camera.types.ts
src/CameraView.tsx
build/Camera.types.d.ts
build/CameraView.d.ts
build/CameraView.js
121 added lines. The Kotlin side builds a metering action from the touch point and lets the preview's own point factory undo whatever the cover-fit crop did to the coordinates. The Swift side does the equivalent. Then the same method has to exist in JavaScript, and that is where patching a published package gets interesting, because an Expo module ships compiled output:
-
src/CameraView.tsxis what a person reads -
build/CameraView.jsis what Metro actually loads -
build/CameraView.d.tsis what TypeScript believes
All three have to be in the patch. Edit only the source and the method does not exist at runtime. Edit only the build output and your own call site is a type error. This is the part people do not expect from "just patch the package": you are editing a build artefact, by hand, in a diff.
Why a patch and not a fork
A fork means publishing a package and owning its release cadence against an SDK that moves every few weeks. A config plugin cannot add a method to a view manager. A separate native module means re-implementing enough of the camera to own a preview.
The patch, by contrast, is reproducible from the lockfile, which records it by content:
# pnpm-lock.yaml
patchedDependencies:
expo-camera@57.0.5: d3f519a89e5ff8270e755eb5497f063d085496b31906fa63a44704e6aa67275b
And that hash is not just a note. It is part of the package's identity in the resolution graph:
expo-camera@57.0.5(patch_hash=d3f519a8...)(expo@57.0.24)(react-native@0.86.3)...
Change one line of the patch and the dependency becomes a different dependency, so every machine either has the patched copy or re-fetches and re-patches. That property is the whole argument for this approach. A patch that can silently not apply would be worse than a fork.
The version number lives in four hand-written places
Count them:
-
apps/mobile/package.json, as the dependency range:"expo-camera": "~57.0.5" -
pnpm-workspace.yaml, as thepatchedDependencieskey:expo-camera@57.0.5 - The patch filename itself:
patches/expo-camera@57.0.5.patch - A comment in
apps/mobile/app/capture.tsxexplaining wherefocusAtAsynccomes from
Plus two more that pnpm generates in the lockfile, which do not count because nobody types them.
Three of those four are load-bearing, and the install is loud about all three. I tested the second one in a throwaway project rather than taking pnpm's documentation on faith: a key naming a version that is not installed, with everything else correct.
[ERR_PNPM_UNUSED_PATCH] The following patches were not used: is-odd@3.0.0
Either remove them from "patchedDependencies" or update them to match
packages in your dependencies.
Install fails. Not a warning, not a quiet skip, a non-zero exit, which is exactly what you want from a patch that adds a native method the app calls unconditionally. The filename is checked the same way: name a file that is not there and the install stops.
The fourth place is the comment, and nothing on earth checks a comment. When the dependency moved from 57.0.4 to 57.0.5, items one, two and three had to change together or the install would not complete. Item four did not have to change, so it did not, and for a while the file explaining the patch pointed at patches/expo-camera@57.0.4.patch, a path that does not exist, in the one place a new reader would go looking.
That was today's fix. One character.
The fix I did not make
The obvious response is to stop writing the version in the key. pnpm accepts a semver range, or the bare package name, and I checked both work:
patchedDependencies:
is-odd@^3.0.0: patches/is-odd@3.0.0.patch # applies to 3.0.1
is-odd: patches/is-odd@3.0.0.patch # also applies
Tempting, and wrong for this patch. An exact pin means the next expo-camera release stops the install until a human re-reads 221 lines of native diff against the new upstream code. A range means a patch written for 57.0.5 applies itself to 57.1.0 as long as the context lines still match, and "the context lines still match" is not the same statement as "the camera still focuses". For a patch made of Kotlin, Swift and a compiled JavaScript file, I want the interruption.
So the pin stays, and the conclusion goes the other way: the version number should not be in the comment at all. The comment should say that a patch exists and that pnpm-workspace.yaml names it, because that file cannot be stale. A fact with no checker, written in prose, is not documentation. It is a claim about the repository that only decays.
If you want to see what those 121 lines bought, you need the app rather than the website: munchable.app has the install link, and the tap to focus is on the label camera, the screen you reach by scanning a barcode we do not hold yet. If you want the words instead, the about page says what the camera is for.
Top comments (0)