fix: resolve native module hoisting failures for EAS monorepo builds#3
Conversation
…e module path resolution plugins
📝 WalkthroughWalkthroughThis PR configures an EAS monorepo build for the Agronavis mobile app by fixing native module hoisting failures, establishing Expo/EAS build settings, defining the app entry point, and pinning critical dependencies. Two custom Expo plugins resolve React Native path resolution issues that occur when native modules are hoisted during npm workspace installation. ChangesEAS Monorepo Build Configuration and Mobile App Setup
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
apps/mobile/plugins/withReactNativePickerFix.js (1)
31-34: ⚡ Quick winUse a plugin-specific marker instead of a broad
REACT_NATIVE_DIRstring check.Current guard can skip injection on unrelated content matches; use a unique sentinel for deterministic idempotency.
Proposed patch
- if (contents.includes("REACT_NATIVE_DIR")) { + if (contents.includes("_MONOREPO_PICKER_FIX_")) { // Already patched, skip return config; } const injection = ` -// ─── Monorepo Fix ──────────────────────────────────────────────────────────── +// _MONOREPO_PICKER_FIX_ ─────────────────────────────────────────────────────── +// ─── Monorepo Fix ────────────────────────────────────────────────────────────🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/plugins/withReactNativePickerFix.js` around lines 31 - 34, The current idempotency check uses a broad string match on contents.includes("REACT_NATIVE_DIR") which may collide with unrelated files; change it to look for and insert a plugin-specific sentinel (e.g., a unique comment like "// withReactNativePickerFix applied" or "/* WITH_RN_PICKER_FIX_MARKER */") so the guard becomes contents.includes("<UNIQUE_SENTINEL>") and the injection code adds that sentinel along with the patch. Update the code in withReactNativePickerFix.js where the variable contents is inspected and where the patch is written (the injection/return path) to use the new unique marker so the plugin is deterministically idempotent.apps/mobile/plugins/withMonorepoFix.js (1)
84-101: ⚡ Quick winRefresh fallback-copied directories instead of permanently skipping existing paths.
When symlink creation fails once, later runs hit the “already exists” branch and never refresh copied files, which can drift after React Native upgrades.
Proposed patch
- } else { - logger.lifecycle("[MonorepoFix] Already exists: node_modules/" + _dirname) + } else { + if (!java.nio.file.Files.isSymbolicLink(_link.toPath())) { + ant.copy(todir: _link.absolutePath, overwrite: true) { + fileset(dir: _source.absolutePath) + } + logger.lifecycle("[MonorepoFix] Refreshed copied dir: node_modules/" + _dirname) + } else { + logger.lifecycle("[MonorepoFix] Already symlinked: node_modules/" + _dirname) + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/plugins/withMonorepoFix.js` around lines 84 - 101, Currently when _link.exists() we skip updating fallback-copied directories, so once a symlink attempt fails the copied files are never refreshed; change the else branch that logs "[MonorepoFix] Already exists: node_modules/" + _dirname to detect that the path exists as a fallback directory and refresh its contents from _source by running the same copy logic (ensure _link.mkdirs() then call ant.copy with todir: _link.absolutePath and overwrite: true using fileset(dir: _source.absolutePath)), and emit a lifecycle/log message (e.g. "[MonorepoFix] Refreshed: node_modules/..." ) so copied files are updated when React Native or source files change.apps/mobile/package.json (1)
85-88: ⚡ Quick winRemove workspace-local
overridesfromapps/mobile/package.jsonIn npm workspaces,
overridesare only applied when defined in the project rootpackage.json; a workspace-leveloverridesblock won’t affect dependency resolution and can mislead readers.Proposed cleanup diff
- "overrides": { - "react-native-svg": "15.12.1", - "react-native-worklets": "0.5.1" - },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/package.json` around lines 85 - 88, The package.json in this workspace contains a local "overrides" block for "react-native-svg" and "react-native-worklets" which has no effect in npm workspaces; remove the entire "overrides" object from the apps/mobile package.json and, if those version pins are required, add them to the repository root package.json "overrides" instead so dependency resolution is applied workspace-wide. Ensure you delete the "overrides" key and its entries ("react-native-svg", "react-native-worklets") from the mobile manifest and run a fresh install to validate the lockfile updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/mobile/eas.json`:
- Around line 11-16: The build profiles in apps/mobile/eas.json (the "preview"
and "production" objects under "build") are missing android.buildType: "apk" and
env.npm_config_legacy_peer_deps: "true" that are present in the root eas.json;
update the "preview" and "production" profiles in apps/mobile/eas.json to
include android.buildType set to "apk" and add an env object with
npm_config_legacy_peer_deps set to "true" so EAS behavior is consistent
regardless of working directory.
In `@eas.json`:
- Around line 20-31: The production profile in eas.json currently forces an
Android APK via the "production" -> "android" -> "buildType": "apk" setting
which prevents eas submit from producing an AAB; remove that override (or change
it to "aab") so the production profile outputs an Android App Bundle for Google
Play submissions and reserve "apk" only for testing/preview profiles (e.g.,
"preview" profile), updating the "production" profile's android.buildType
accordingly.
---
Nitpick comments:
In `@apps/mobile/package.json`:
- Around line 85-88: The package.json in this workspace contains a local
"overrides" block for "react-native-svg" and "react-native-worklets" which has
no effect in npm workspaces; remove the entire "overrides" object from the
apps/mobile package.json and, if those version pins are required, add them to
the repository root package.json "overrides" instead so dependency resolution is
applied workspace-wide. Ensure you delete the "overrides" key and its entries
("react-native-svg", "react-native-worklets") from the mobile manifest and run a
fresh install to validate the lockfile updates.
In `@apps/mobile/plugins/withMonorepoFix.js`:
- Around line 84-101: Currently when _link.exists() we skip updating
fallback-copied directories, so once a symlink attempt fails the copied files
are never refreshed; change the else branch that logs "[MonorepoFix] Already
exists: node_modules/" + _dirname to detect that the path exists as a fallback
directory and refresh its contents from _source by running the same copy logic
(ensure _link.mkdirs() then call ant.copy with todir: _link.absolutePath and
overwrite: true using fileset(dir: _source.absolutePath)), and emit a
lifecycle/log message (e.g. "[MonorepoFix] Refreshed: node_modules/..." ) so
copied files are updated when React Native or source files change.
In `@apps/mobile/plugins/withReactNativePickerFix.js`:
- Around line 31-34: The current idempotency check uses a broad string match on
contents.includes("REACT_NATIVE_DIR") which may collide with unrelated files;
change it to look for and insert a plugin-specific sentinel (e.g., a unique
comment like "// withReactNativePickerFix applied" or "/*
WITH_RN_PICKER_FIX_MARKER */") so the guard becomes
contents.includes("<UNIQUE_SENTINEL>") and the injection code adds that sentinel
along with the patch. Update the code in withReactNativePickerFix.js where the
variable contents is inspected and where the patch is written (the
injection/return path) to use the new unique marker so the plugin is
deterministically idempotent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2049c0f9-359b-4821-9c95-3b2cc8ecc3e8
⛔ Files ignored due to path filters (6)
apps/mobile/assets/images/adaptive-icon.pngis excluded by!**/*.pngapps/mobile/assets/images/favicon.pngis excluded by!**/*.pngapps/mobile/assets/images/icon.pngis excluded by!**/*.pngapps/mobile/assets/images/notification-icon.pngis excluded by!**/*.pngapps/mobile/assets/images/splash-icon.pngis excluded by!**/*.pngpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (9)
app.jsonapps/mobile/app.jsonapps/mobile/app/index.tsxapps/mobile/eas.jsonapps/mobile/package.jsonapps/mobile/plugins/withMonorepoFix.jsapps/mobile/plugins/withReactNativePickerFix.jseas.jsonpackage.json
| "preview": { | ||
| "distribution": "internal" | ||
| }, | ||
| "production": { | ||
| "autoIncrement": true | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Read-only check: show effective profile drift between root and app-level EAS configs.
python - <<'PY'
import json, pathlib
root = json.loads(pathlib.Path("eas.json").read_text())
app = json.loads(pathlib.Path("apps/mobile/eas.json").read_text())
for profile in ["preview", "production"]:
r = root.get("build", {}).get(profile, {})
a = app.get("build", {}).get(profile, {})
print(f"\n[{profile}]")
for key in ["distribution", "android", "env", "autoIncrement"]:
print(f"{key}: root={r.get(key)!r} | app={a.get(key)!r}")
PYRepository: jpdevhub/Agronavis-App
Length of output: 459
Align apps/mobile/eas.json build profiles with root eas.json to prevent build/install drift.
build.preview and build.production in apps/mobile/eas.json omit android.buildType: "apk" and env.npm_config_legacy_peer_deps: "true" that exist in the root config, so running EAS from different locations can yield different behavior.
Proposed alignment diff
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
- "distribution": "internal"
+ "distribution": "internal",
+ "android": {
+ "buildType": "apk"
+ },
+ "env": {
+ "npm_config_legacy_peer_deps": "true"
+ }
},
"production": {
- "autoIncrement": true
+ "autoIncrement": true,
+ "android": {
+ "buildType": "apk"
+ },
+ "env": {
+ "npm_config_legacy_peer_deps": "true"
+ }
}
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "preview": { | |
| "distribution": "internal" | |
| }, | |
| "production": { | |
| "autoIncrement": true | |
| } | |
| "preview": { | |
| "distribution": "internal", | |
| "android": { | |
| "buildType": "apk" | |
| }, | |
| "env": { | |
| "npm_config_legacy_peer_deps": "true" | |
| } | |
| }, | |
| "production": { | |
| "autoIncrement": true, | |
| "android": { | |
| "buildType": "apk" | |
| }, | |
| "env": { | |
| "npm_config_legacy_peer_deps": "true" | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/eas.json` around lines 11 - 16, The build profiles in
apps/mobile/eas.json (the "preview" and "production" objects under "build") are
missing android.buildType: "apk" and env.npm_config_legacy_peer_deps: "true"
that are present in the root eas.json; update the "preview" and "production"
profiles in apps/mobile/eas.json to include android.buildType set to "apk" and
add an env object with npm_config_legacy_peer_deps set to "true" so EAS behavior
is consistent regardless of working directory.
| "production": { | ||
| "autoIncrement": true, | ||
| "android": { | ||
| "buildType": "apk" | ||
| }, | ||
| "env": { | ||
| "npm_config_legacy_peer_deps": "true" | ||
| } | ||
| } | ||
| }, | ||
| "submit": { | ||
| "production": {} |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For Expo EAS as of 2026, does build.production.android.buildType: "apk"produce an artifact compatible with Google Play submission viaeas submit, or is AAB required?
💡 Result:
Yes—however, for Google Play submissions via Expo EAS Submit, AAB is the expected/accepted format, and Expo explicitly recommends AAB for store submission. 1) What build.production.android.buildType: "apk" produces Expo’s EAS Build configuration supports generating an Android.apk by setting android.buildType to apk in your build profile. Expo notes that the default Android build output is an Android App Bundle (AAB), and that.apk is mainly for install/testing on emulators or devices rather than store distribution. [1][2] 2) What eas submit accepts for Google Play Expo’s EAS Submit documentation states that EAS Submit accepts any valid Android App Bundle (.aab) file for Android submissions to Google Play. [3] 3) Is an APK sufficient for Google Play via EAS Submit? Expo’s store-submission guidance is that production builds are ready to be submitted to app stores, and Expo recommends using AAB when submitting to stores; AAB is the default configuration. [4][2] Given (a) Expo’s explicit “AAB recommended/default for Google Play store submission” guidance [4][2] and (b) EAS Submit’s stated acceptance of valid .aab Android binaries for Google Play submissions [3], the safe conclusion as of 2026-05-23 is: AAB is required/expected for Play submission via eas submit, and using buildType: "apk" may not be compatible with Play submission through EAS Submit. Practical recommendation If your goal is Google Play via eas submit, keep your production Android build as the default AAB output (omit android.buildType: "apk" for the production profile), and use eas submit --platform android after building the production profile. [4][5][3]
Citations:
- 1: https://docs.expo.dev/build-reference/apk/
- 2: https://docs.expo.dev/build/eas-json
- 3: https://docs.expo.dev/submit/introduction/
- 4: https://docs.expo.dev/deploy/build-project
- 5: https://docs.expo.dev/submit/android/
Switch production Android build from apk to AAB for eas submit
eas.json sets build.production.android.buildType: "apk" (lines 20-31), which makes EAS Build output an APK. For Google Play submissions via Expo EAS Submit, store submission expects an Android App Bundle (.aab), so keep production aligned to the AAB/default output (e.g., remove the android.buildType: "apk" override) and restrict APK to preview/testing if needed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@eas.json` around lines 20 - 31, The production profile in eas.json currently
forces an Android APK via the "production" -> "android" -> "buildType": "apk"
setting which prevents eas submit from producing an AAB; remove that override
(or change it to "aab") so the production profile outputs an Android App Bundle
for Google Play submissions and reserve "apk" only for testing/preview profiles
(e.g., "preview" profile), updating the "production" profile's android.buildType
accordingly.
There was a problem hiding this comment.
3 issues found across 15 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="eas.json">
<violation number="1" location="eas.json:13">
P1: Production builds should use AAB format (the default), not APK. APK is appropriate for `preview`/internal distribution, but Google Play Store requires AAB for submission. Since you have a `submit.production` block, this will prevent successful store submission.</violation>
</file>
<file name="apps/mobile/app.json">
<violation number="1" location="apps/mobile/app.json:39">
P2: `RECORD_AUDIO` permission is added but no code in the app uses audio recording. Unnecessary permissions increase the app's attack surface and may cause user trust issues or app store review friction. Remove it unless an upcoming feature requires it.</violation>
</file>
<file name="apps/mobile/plugins/withReactNativePickerFix.js">
<violation number="1" location="apps/mobile/plugins/withReactNativePickerFix.js:73">
P2: This plugin file is dead code — it's never registered in `app.json` or imported anywhere. The active plugin `./plugins/withMonorepoFix` already sets `ext.REACT_NATIVE_DIR` and `ext.REACT_NATIVE_NODE_MODULES_DIR` using the same logic. Consider removing this file to avoid confusion.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| }, | ||
| "preview": { | ||
| "distribution": "internal", | ||
| "android": { |
There was a problem hiding this comment.
P1: Production builds should use AAB format (the default), not APK. APK is appropriate for preview/internal distribution, but Google Play Store requires AAB for submission. Since you have a submit.production block, this will prevent successful store submission.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At eas.json, line 13:
<comment>Production builds should use AAB format (the default), not APK. APK is appropriate for `preview`/internal distribution, but Google Play Store requires AAB for submission. Since you have a `submit.production` block, this will prevent successful store submission.</comment>
<file context>
@@ -0,0 +1,33 @@
+ },
+ "preview": {
+ "distribution": "internal",
+ "android": {
+ "buildType": "apk"
+ },
</file context>
| "android.permission.ACCESS_COARSE_LOCATION", | ||
| "android.permission.READ_EXTERNAL_STORAGE" | ||
| "android.permission.READ_EXTERNAL_STORAGE", | ||
| "android.permission.RECORD_AUDIO" |
There was a problem hiding this comment.
P2: RECORD_AUDIO permission is added but no code in the app uses audio recording. Unnecessary permissions increase the app's attack surface and may cause user trust issues or app store review friction. Remove it unless an upcoming feature requires it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/app.json, line 39:
<comment>`RECORD_AUDIO` permission is added but no code in the app uses audio recording. Unnecessary permissions increase the app's attack surface and may cause user trust issues or app store review friction. Remove it unless an upcoming feature requires it.</comment>
<file context>
@@ -27,12 +30,13 @@
"android.permission.ACCESS_COARSE_LOCATION",
- "android.permission.READ_EXTERNAL_STORAGE"
+ "android.permission.READ_EXTERNAL_STORAGE",
+ "android.permission.RECORD_AUDIO"
]
},
</file context>
| @@ -0,0 +1,73 @@ | |||
| /** | |||
There was a problem hiding this comment.
P2: This plugin file is dead code — it's never registered in app.json or imported anywhere. The active plugin ./plugins/withMonorepoFix already sets ext.REACT_NATIVE_DIR and ext.REACT_NATIVE_NODE_MODULES_DIR using the same logic. Consider removing this file to avoid confusion.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/plugins/withReactNativePickerFix.js, line 73:
<comment>This plugin file is dead code — it's never registered in `app.json` or imported anywhere. The active plugin `./plugins/withMonorepoFix` already sets `ext.REACT_NATIVE_DIR` and `ext.REACT_NATIVE_NODE_MODULES_DIR` using the same logic. Consider removing this file to avoid confusion.</comment>
<file context>
@@ -0,0 +1,73 @@
+ });
+};
+
+module.exports = withReactNativePickerFix;
</file context>
Problem
When building the Android app via EAS in our npm workspaces monorepo, the build crashed during the CMake/Gradle configuration phase.
Packages like
react-native-worklets,react-native-reanimated, andreact-native-gesture-handlerwere failing to find React Native's internal C++ and Gradle files (cmake-utils,gradle.properties,libs.versions.toml).This happens because npm hoists both the native modules and
react-nativeto the rootnode_modules, breaking the relative paths hardcoded into these native modules.Solution
withMonorepoFixExpo Plugin: Dynamically locates the hoistedreact-nativedirectory and creates symlinks forReactAndroid,ReactCommon, andgradlein the rootnode_modules. This allows all C++ and Gradle path lookups to succeed natively.withReactNativePickerFixExpo Plugin: Specific fix for@react-native-picker/pickerwhich requiredREACT_NATIVE_NODE_MODULES_DIRto be injected globally intobuild.gradle.app/index.tsxto handle the rootagronavis:///deep link, preventing the app from crashing on the "Unmatched Route" screen on startup.Verification
expo doctorpasses (18/18 checks).eas build -p android --profile previewcompletes successfully.Closes #2
Summary by cubic
Fixes EAS Android builds in our npm workspaces monorepo by resolving hoisted
react-nativepaths for native modules and adding a safe entry route. Builds now complete and the app opens on the welcome screen.Bug Fixes
./plugins/withMonorepoFixto find hoistedreact-nativeand symlinkReactAndroid,ReactCommon, andgradleinto rootnode_modules, restoring Gradle/CMake lookups forreact-native-worklets,react-native-reanimated,react-native-gesture-handler, andreact-native-svg../plugins/withReactNativePickerFixto setREACT_NATIVE_DIRandREACT_NATIVE_NODE_MODULES_DIRfor@react-native-picker/pickerand RNGH across root and subprojects.app/index.tsxredirect to handleagronavis:///and avoid the unmatched route crash on cold start.Dependencies
react-native-workletsto0.5.1andreact-native-svgto15.12.1, with workspace-leveloverridesand Expoinstall.excludeto prevent incompatible upgrades.Written for commit 9ee837b. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
New Features
Configuration