Conversation
<!-- CURSOR_SUMMARY --> > [!NOTE] > Enforces import sorting via eslint-plugin-simple-import-sort and updates import order across the codebase. > > - **Tooling/ESLint**: > - Add `eslint-plugin-simple-import-sort` and enable `simple-import-sort/imports` and `simple-import-sort/exports` rules in `package.json`. > - **Codebase**: > - Reorder and group imports in `example/App.tsx`, `src/ReactNativeZoomableView.tsx`, `src/components/StaticPin.tsx`, `src/debugHelper/index.tsx`, `src/helper/index.ts`, `src/animations/index.ts`, `src/index.tsx`, and `src/typings/index.ts`. > - Minor export order adjustments in `src/index.tsx`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b5f576e. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: thomasvo <thomas.vo@openspace.ai>
<!-- CURSOR_SUMMARY --> > [!NOTE] > Upgrade to React Native 0.79 with Metro 0.82 and updated tooling, plus import/export reordering and regenerated artifacts. > > - **Build/Tooling**: > - Upgrade to React Native `0.79.7` (Metro `0.82.x`, new CLI/debug middleware, Gradle/codegen updates, Hermes/terser stack). > - Bump Jest/Babel/tooling (e.g., `babel-jest` 29.x), add `eslint-plugin-simple-import-sort`, and raise peer `react-native` to `>=0.79.0`. > - **Codebase tweaks**: > - Reorder/normalize imports and exports across `src/`, `lib/`, and module files to match new lint rules; regenerate sourcemaps. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e105c42. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: thomasvo <thomas.vo@openspace.ai>
|
bugbot run |
|
@claude please review this PR |
|
@claude review |
## Summary - Removes lib/ from git tracking to avoid merge conflicts - Moves build step from pre-commit hook to CI/CD - PR checks now verify builds succeed - GitHub Actions handles build + npm publish on release ## Changes - Remove `bob build` from pre-commit hook - Add lib/ to .gitignore - Update CI workflow to build on PRs - Create release workflow to build and publish to npm - Configure release-it to skip npm publish (GitHub Actions handles it) - Update CONTRIBUTING.md with new process ## Release Process 1. Run `yarn release` locally (creates tag + GitHub release) 2. GitHub Actions automatically builds and publishes to npm Requires `NPM_TOKEN` secret to be set in GitHub repo settings. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Implements CI/CD for builds and releases, and removes built output from the repo. > > - **Checks workflow**: Updates `.github/workflows/lint.yml` to run on `push` (master) and `pull_request`; sets up Node 16 with yarn cache; installs deps; runs TypeScript, ESLint, and `react-native-builder-bob` build > - **Release workflow**: Adds `.github/workflows/release.yml` to build and `npm publish` on GitHub Release creation (uses `NPM_TOKEN`) > - **Changelog config**: Adds `.github/release.yml` with label-based release notes categories/exclusions > - **Repo hygiene**: Adds `lib/` to `.gitignore` and removes committed `lib/commonjs/*` build artifacts > - **Docs**: Updates `CONTRIBUTING.md` and `README.md` with new build, release, and changelog process > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 205b2df. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an automated Release workflow to publish packages on GitHub release events. * **Chores** * CI now runs on PRs and master pushes, uses Node 20, enables Yarn cache, enforces frozen installs, and includes a build check; added build output to .gitignore and simplified pre-commit hooks; package scripts updated. * **Documentation** * CONTRIBUTING clarified build/release process and publishing requirements. * **Breaking Changes** * Several runtime components, helpers, animation utilities, and TypeScript typings were removed — imports and types will need updating. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: thomasvo <thomas.vo@openspace.ai> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…e-define # Conflicts: # .github/workflows/lint.yml # lib/commonjs/ReactNativeZoomableView.js # lib/commonjs/ReactNativeZoomableView.js.map # lib/commonjs/animations/index.js # lib/commonjs/animations/index.js.map # lib/commonjs/components/AnimatedTouchFeedback.js # lib/commonjs/components/AnimatedTouchFeedback.js.map # lib/commonjs/components/StaticPin.js # lib/commonjs/components/StaticPin.js.map # lib/commonjs/components/index.js # lib/commonjs/components/index.js.map # lib/commonjs/debugHelper/index.js # lib/commonjs/debugHelper/index.js.map # lib/commonjs/helper/calcNewScaledOffsetForZoomCentering.js # lib/commonjs/helper/calcNewScaledOffsetForZoomCentering.js.map # lib/commonjs/helper/coordinateConversion.js # lib/commonjs/helper/coordinateConversion.js.map # lib/commonjs/helper/index.js # lib/commonjs/helper/index.js.map # lib/commonjs/index.js # lib/commonjs/index.js.map # lib/commonjs/typings/index.js # lib/commonjs/typings/index.js.map # lib/module/ReactNativeZoomableView.js # lib/module/ReactNativeZoomableView.js.map # lib/module/animations/index.js # lib/module/animations/index.js.map # lib/module/components/AnimatedTouchFeedback.js # lib/module/components/AnimatedTouchFeedback.js.map # lib/module/components/StaticPin.js # lib/module/components/StaticPin.js.map # lib/module/components/index.js # lib/module/components/index.js.map # lib/module/debugHelper/index.js # lib/module/debugHelper/index.js.map # lib/module/helper/calcNewScaledOffsetForZoomCentering.js # lib/module/helper/calcNewScaledOffsetForZoomCentering.js.map # lib/module/helper/coordinateConversion.js # lib/module/helper/coordinateConversion.js.map # lib/module/helper/index.js # lib/module/helper/index.js.map # lib/module/index.js # lib/module/index.js.map # lib/module/typings/index.js # lib/module/typings/index.js.map # lib/typescript/ReactNativeZoomableView.d.ts # lib/typescript/animations/index.d.ts # lib/typescript/components/AnimatedTouchFeedback.d.ts # lib/typescript/components/StaticPin.d.ts # lib/typescript/components/index.d.ts # lib/typescript/debugHelper/index.d.ts # lib/typescript/helper/calcNewScaledOffsetForZoomCentering.d.ts # lib/typescript/helper/coordinateConversion.d.ts # lib/typescript/helper/index.d.ts # lib/typescript/index.d.ts # lib/typescript/typings/index.d.ts # package.json # src/ReactNativeZoomableView.tsx # src/animations/index.ts # yarn.lock
PR #158 removed checked-in lib/ and added it to .gitignore. These remaining tracked files are now redundant.
| onPanResponderRelease: _handlePanResponderEnd, | ||
| onPanResponderTerminate: (evt, gestureState) => { | ||
| // We should also call _handlePanResponderEnd | ||
| // to properly perform cleanups when the gesture is terminated | ||
| // (aka gesture handling responsibility is taken over by another component). | ||
| // This also fixes a weird issue where | ||
| // on real device, sometimes onPanResponderRelease is not called when you lift 2 fingers up, | ||
| // but onPanResponderTerminate is called instead for no apparent reason. | ||
| _handlePanResponderEnd(evt, gestureState); | ||
| props.onPanResponderTerminate?.( | ||
| evt, | ||
| gestureState, | ||
| _getZoomableViewEventObject() | ||
| ); | ||
| }, | ||
| onPanResponderTerminationRequest: (evt, gestureState) => | ||
| !!props.onPanResponderTerminationRequest?.( | ||
| evt, | ||
| gestureState, | ||
| _getZoomableViewEventObject() | ||
| ), | ||
| // Defaults to true to prevent parent components, such as React Navigation's tab view, from taking over as responder. | ||
| onShouldBlockNativeResponder: (evt, gestureState) => | ||
| props.onShouldBlockNativeResponder?.( | ||
| evt, | ||
| gestureState, | ||
| _getZoomableViewEventObject() | ||
| ) ?? true, | ||
| onStartShouldSetPanResponderCapture: (evt, gestureState) => | ||
| !!props.onStartShouldSetPanResponderCapture?.(evt, gestureState), |
There was a problem hiding this comment.
🟣 Five PanResponder callbacks in the empty-deps useLayoutEffect close directly over the render-scope props variable, permanently capturing initial-mount values and ignoring all later prop updates. This is a pre-existing issue carried forward unchanged by the refactor: onPanResponderTerminate, onPanResponderTerminationRequest, onShouldBlockNativeResponder, onStartShouldSetPanResponderCapture, and onMoveShouldSetPanResponderCapture should be wrapped with useLatestCallback the same way the other four handlers are.
Extended reasoning...
Root cause
In src/ReactNativeZoomableView.tsx, PanResponder.create is called inside a useLayoutEffect(() => { ... }, []) — a one-time mount effect. The five inline arrow functions passed to the PanResponder each directly reference the function-component's props variable:
onPanResponderTerminate: (evt, gestureState) => {
_handlePanResponderEnd(evt, gestureState);
props.onPanResponderTerminate?.(evt, gestureState, _getZoomableViewEventObject());
},
onPanResponderTerminationRequest: (evt, gestureState) =>
!!props.onPanResponderTerminationRequest?.(evt, gestureState, _getZoomableViewEventObject()),
onShouldBlockNativeResponder: (evt, gestureState) =>
props.onShouldBlockNativeResponder?.(evt, gestureState, _getZoomableViewEventObject()) ?? true,
onStartShouldSetPanResponderCapture: (evt, gestureState) =>
!!props.onStartShouldSetPanResponderCapture?.(evt, gestureState),
onMoveShouldSetPanResponderCapture: (evt, gestureState) =>
!!props.onMoveShouldSetPanResponderCapture?.(evt, gestureState),Because the useLayoutEffect runs only once (empty deps), JavaScript's closure captures the props object from the very first render. On every subsequent render, React creates a new props object — but the callbacks still call the initial prop functions.
Why the fix pattern exists but was not applied here
The other four main handlers — _handlePanResponderEnd, _handlePanResponderMove, _handlePanResponderGrant, _handleStartShouldSetPanResponder — are all wrapped with useLatestCallback. That hook stores the latest function in a useRef (updated each render) and returns a stable wrapper that always calls ref.current. This correctly breaks the stale-closure problem.
The five inline callbacks above were never given the same treatment, making them permanently stale after the first render.
Concrete proof
Suppose a parent component conditionally toggles onShouldBlockNativeResponder based on a piece of state:
const [blocking, setBlocking] = useState(true);
<ReactNativeZoomableView
onShouldBlockNativeResponder={() => blocking}
/>
// Later: setBlocking(false)After setBlocking(false), the view re-renders with a new props.onShouldBlockNativeResponder that returns false. However, the PanResponder was created once at mount with the closure capturing the original props, which returned true. Every subsequent native gesture event will still call the stale function and return true, so native scroll containers will always be blocked regardless of what the parent passes.
Pre-existing nature
The same stale-closure pattern existed in the old code before this PR. The diff shows the removed lines had identical direct props.onPanResponderTerminate?., props.onShouldBlockNativeResponder?., etc. references inside the same one-time useLayoutEffect. This PR is a pure code reorganization (no-use-before-define compliance) and does not change this behavior in either direction.
Fix
Wrap each of the five callbacks with useLatestCallback before passing them to PanResponder.create, identical to how the four main handlers are handled.
| "eslint-plugin-prettier": "^3.1.3", | ||
| "eslint-plugin-simple-import-sort": "^12.1.1", | ||
| "husky": "^4.2.5", | ||
| "lodash": "^4.17.21", | ||
| "pod-install": "^0.1.0", | ||
| "prettier": "^2.0.5", | ||
| "react": "17.0.2", |
There was a problem hiding this comment.
🟣 lodash is imported at runtime in ReactNativeZoomableView.tsx but is listed only in peerDependencies, which are not automatically installed by package managers for library consumers. Any consumer project that does not explicitly add lodash to their own dependencies will get a hard crash (Cannot find module 'lodash') at runtime. Move lodash to the dependencies field so it is always installed automatically. Note: this is a pre-existing issue (lodash was previously in devDependencies, also not auto-installed), but this PR makes it more visible by moving it to peerDependencies.
Extended reasoning...
What the bug is and how it manifests
lodash is a hard runtime dependency of this library — ReactNativeZoomableView.tsx imports debounce and defaults from lodash at the top of the file. These are called during normal component lifecycle (defaults is applied on every render to merge prop defaults; debounce wraps a pinch handler). Despite being a runtime requirement, lodash appears only in peerDependencies (and previously was in devDependencies), never in dependencies. Package managers (npm, yarn classic v1) do NOT auto-install peerDependencies for library consumers, so any consumer who has not independently installed lodash will encounter a hard crash.
The specific code path that triggers it
In src/ReactNativeZoomableView.tsx:
- Line 1:
import { debounce, defaults } from 'lodash'; - defaults is called when building the component's resolved props on every render cycle.
- debounce is used to wrap
debouncedOnStaticPinPositionChangewhich is invoked during pinch gesture handling.
Both usages are in the hot path of the component. There is no dynamic require, no try/catch fallback, and no optional chaining around the lodash calls.
Why existing code does not prevent it
peerDependencies communicates to consuming projects: 'you must install this yourself'. The library's own package.json has no entry in the dependencies field for lodash, so npm/yarn will never automatically install lodash for a consumer. If a consumer does npm install react-native-zoomable-view into a fresh project with no lodash, they will get no error during install — the crash only appears at runtime when the component is first mounted.
Impact
Any consumer project that does not already have lodash installed will encounter:
Error: Cannot find module 'lodash'
...the moment ReactNativeZoomableView is first rendered. This is a hard crash with no graceful degradation. Consumer projects built around this library (especially RN apps bootstrapped from a clean template) have no lodash by default, so this affects a realistic portion of users.
How to fix it
The simplest fix: move lodash from peerDependencies to dependencies in package.json:
"dependencies": {
"lodash": "^4.17.21"
}Remove it from peerDependencies entirely (or keep it there if you want to allow version deduplication). An alternative fix is to inline the two usages (debounce is available in react-native's bundled scheduler utilities; defaults can be replaced with Object.assign or spread syntax) and remove lodash as a dependency altogether.
Step-by-step proof
- Consumer runs:
npm install react-native-zoomable-view - npm resolves dependencies[] for the library (empty, lodash is not there). peerDependencies are noted but not installed.
- Consumer project has no lodash in their own package.json.
- Consumer renders
<ReactNativeZoomableView>in their app. - Metro bundler (or Node.js) attempts to resolve the import
from 'lodash'. - Module resolution fails — lodash is not on disk anywhere in node_modules.
- Hard crash:
Error: Cannot find module 'lodash'.
Pre-existing note
Before this PR, lodash was in devDependencies (also not auto-installed for consumers). This PR moves it to peerDependencies, which is marginally better (it at least communicates the requirement), but still does not automatically install lodash. The root fix — placing lodash in dependencies — has never been applied. This issue is pre-existing but worth flagging because the PR touches package.json and is an opportunity to resolve it properly.
Reanimated has a hard requirement that a worklet must be defined before the worklet that calls it is. Otherwise, the app will crash. At the same time there's no linter that would apply the rule only for worklets, so we have to turn it on for everything. Quite inconvenient, but gets us one step toward reanimated for now, and can be revisited for future improvements.
There's no logic change in this PR. Only functions getting moved around.
Note
Refactors and reorders code to comply with
no-use-before-define, extracting shared logic into hooks/helpers and adjusting call sites without changing behavior.useZoomSubject(measurement) anduseDebugPoints(pinch debug) hooks, and moves zoom step calc tohelper/getNextZoomStepReactNativeZoomableViewto use new hooks, replaces inline helpers, and reorders pan/gesture handlers and setupAnimatedTouchFeedback,StaticPin, and debug helpers; map files addedWritten by Cursor Bugbot for commit 16483a8. Configure here.