-
Notifications
You must be signed in to change notification settings - Fork 0
fix: resolve native module hoisting failures for EAS monorepo builds #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "expo": { | ||
| "extra": { | ||
| "eas": { | ||
| "projectId": "4418b05e-cf5e-4ccc-a472-1bc936253a63" | ||
| } | ||
| }, | ||
| "android": { | ||
| "package": "com.karolix.agronavis" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { Redirect } from 'expo-router'; | ||
|
|
||
| /** | ||
| * Root index — Expo Router needs this file to handle the app's entry URL. | ||
| * Redirects immediately to the auth welcome screen. | ||
| * Once authentication is integrated (Clerk), add your auth check here: | ||
| * - isSignedIn → redirect to /(tabs) | ||
| * - not signed in → redirect to /(auth)/welcome | ||
| */ | ||
| export default function Index() { | ||
| return <Redirect href="/(auth)/welcome" />; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "cli": { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "version": ">= 19.0.8", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "appVersionSource": "remote" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "build": { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "development": { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "developmentClient": true, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "distribution": "internal" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "preview": { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "distribution": "internal" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "production": { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "autoIncrement": true | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "submit": { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "production": {} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| /** | ||
| * Comprehensive monorepo fix for ALL native modules hoisted to root node_modules. | ||
| * | ||
| * ROOT CAUSE: | ||
| * npm workspaces hoists native modules AND react-native to root node_modules. | ||
| * Native packages' build.gradle files use relative paths or pass incorrect | ||
| * paths to CMake that only work in a flat (non-monorepo) node_modules layout. | ||
| * | ||
| * STRATEGY: Create symlinks from expected sibling paths → actual react-native paths. | ||
| * This is more robust than copying individual files because: | ||
| * - Symlinks give access to ALL files in those directories (cmake-utils, etc.) | ||
| * - No need to know which specific files each package needs | ||
| * - No file duplication | ||
| * | ||
| * SYMLINKS CREATED (in root node_modules): | ||
| * ReactAndroid/ → react-native/ReactAndroid/ | ||
| * Fixes: react-native-gesture-handler (ReactAndroid/gradle.properties) | ||
| * react-native-worklets (ReactAndroid/cmake-utils/folly-flags.cmake) | ||
| * react-native-reanimated (ReactAndroid/cmake-utils/*) | ||
| * | ||
| * ReactCommon/ → react-native/ReactCommon/ | ||
| * Fixes: react-native-worklets (ReactCommon/cmake-utils/react-native-flags.cmake) | ||
| * react-native-reanimated (ReactCommon/cmake-utils/*) | ||
| * | ||
| * gradle/ → react-native/gradle/ | ||
| * Fixes: react-native-svg (gradle/libs.versions.toml) | ||
| * | ||
| * EXT PROPERTIES SET (for @react-native-picker/picker and similar): | ||
| * REACT_NATIVE_DIR = react-native directory | ||
| * REACT_NATIVE_NODE_MODULES_DIR = node_modules directory | ||
| */ | ||
| const { withProjectBuildGradle } = require("@expo/config-plugins"); | ||
|
|
||
| const withMonorepoFix = (config) => { | ||
| return withProjectBuildGradle(config, (config) => { | ||
| const contents = config.modResults.contents; | ||
|
|
||
| if (contents.includes("_MONOREPO_FIX_")) { | ||
| return config; | ||
| } | ||
|
|
||
| const injection = ` | ||
| // _MONOREPO_FIX_ ────────────────────────────────────────────────────────────── | ||
| // Fixes native modules that break when npm workspaces hoists them to root. | ||
| // Creates symlinks so all packages find react-native's subdirectories. | ||
|
|
||
| def _rnDir = null | ||
| def _rnCandidates = [ | ||
| new File(rootDir, "../node_modules/react-native"), // non-hoisted | ||
| new File(rootDir, "../../../node_modules/react-native"), // hoisted to monorepo root | ||
| ] | ||
| for (def _c : _rnCandidates) { | ||
| if (_c.exists() && new File(_c, "ReactAndroid/gradle.properties").exists()) { | ||
| _rnDir = _c | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if (_rnDir != null) { | ||
| def _nodeModulesDir = _rnDir.parentFile | ||
| logger.lifecycle("[MonorepoFix] react-native at: " + _rnDir.canonicalPath) | ||
|
|
||
| // ── Ext properties (for @react-native-picker/picker etc.) ────────────── | ||
| ext.REACT_NATIVE_DIR = _rnDir | ||
| ext.REACT_NATIVE_NODE_MODULES_DIR = _nodeModulesDir | ||
| subprojects { | ||
| project.ext.REACT_NATIVE_DIR = _rnDir | ||
| project.ext.REACT_NATIVE_NODE_MODULES_DIR = _nodeModulesDir | ||
| } | ||
|
|
||
| // ── Symlinks ─────────────────────────────────────────────────────────── | ||
| // Create sibling symlinks so packages can find react-native's directories | ||
| // regardless of how they construct paths internally or pass args to CMake. | ||
| // | ||
| // node_modules/ReactAndroid/ → react-native/ReactAndroid/ | ||
| // node_modules/ReactCommon/ → react-native/ReactCommon/ | ||
| // node_modules/gradle/ → react-native/gradle/ | ||
| // | ||
| def _symlinkDirs = ["ReactAndroid", "ReactCommon", "gradle"] | ||
| for (def _dirname : _symlinkDirs) { | ||
| def _source = new File(_rnDir, _dirname) | ||
| def _link = new File(_nodeModulesDir, _dirname) | ||
| if (_source.exists()) { | ||
| if (!_link.exists()) { | ||
| try { | ||
| java.nio.file.Files.createSymbolicLink( | ||
| _link.toPath(), | ||
| _source.toPath() | ||
| ) | ||
| logger.lifecycle("[MonorepoFix] Symlinked: node_modules/" + _dirname + " -> react-native/" + _dirname) | ||
| } catch (Exception _e) { | ||
| // Symlink failed — fall back to copying key files | ||
| logger.warn("[MonorepoFix] Symlink failed for " + _dirname + ": " + _e.message + " — falling back to file copy") | ||
| _link.mkdirs() | ||
| ant.copy(todir: _link.absolutePath, overwrite: false) { | ||
| fileset(dir: _source.absolutePath) | ||
| } | ||
| } | ||
| } else { | ||
| logger.lifecycle("[MonorepoFix] Already exists: node_modules/" + _dirname) | ||
| } | ||
| } else { | ||
| logger.warn("[MonorepoFix] Source not found: react-native/" + _dirname) | ||
| } | ||
| } | ||
|
|
||
| } else { | ||
| logger.warn("[MonorepoFix] WARNING: react-native not found in any candidate path!") | ||
| } | ||
| // ───────────────────────────────────────────────────────────────────────────── | ||
| `; | ||
|
|
||
| config.modResults.contents = contents + injection; | ||
| return config; | ||
| }); | ||
| }; | ||
|
|
||
| module.exports = withMonorepoFix; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /** | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This plugin file is dead code — it's never registered in Prompt for AI agents |
||
| * Comprehensive monorepo fix for native modules that can't find react-native. | ||
| * | ||
| * Problems fixed: | ||
| * - @react-native-picker/picker → reads REACT_NATIVE_NODE_MODULES_DIR | ||
| * - react-native-gesture-handler → reads rootProject.ext.REACT_NATIVE_DIR | ||
| * | ||
| * Root cause: npm workspaces hoists react-native to the monorepo root | ||
| * node_modules. Native modules use relative paths that break when react-native | ||
| * isn't at the same node_modules level as themselves. | ||
| * | ||
| * Strategy: | ||
| * 1. Use pure Groovy (no `node` command) to check both hoisted and | ||
| * non-hoisted locations for react-native. | ||
| * 2. Set REACT_NATIVE_DIR directly on the ROOT PROJECT's ext so RNGH's | ||
| * safeExtGet("REACT_NATIVE_DIR") (which calls rootProject.ext.get()) | ||
| * can find it. | ||
| * 3. Also propagate via subprojects{} for other native modules that read | ||
| * from their own project.ext. | ||
| * | ||
| * Paths (rootDir = apps/mobile/android/): | ||
| * ../node_modules/react-native → apps/mobile/node_modules/react-native | ||
| * ../../../node_modules/react-native → <monorepo-root>/node_modules/react-native (hoisted) | ||
| */ | ||
| const { withProjectBuildGradle } = require("@expo/config-plugins"); | ||
|
|
||
| const withReactNativePickerFix = (config) => { | ||
| return withProjectBuildGradle(config, (config) => { | ||
| const contents = config.modResults.contents; | ||
|
|
||
| if (contents.includes("REACT_NATIVE_DIR")) { | ||
| // Already patched, skip | ||
| return config; | ||
| } | ||
|
|
||
| const injection = ` | ||
| // ─── Monorepo Fix ──────────────────────────────────────────────────────────── | ||
| // Resolves react-native location for native modules in a monorepo where npm | ||
| // workspaces may hoist react-native to the root node_modules. | ||
| // Uses pure Groovy file checks — no dependency on 'node' being in PATH. | ||
| def _reactNativeDir = null | ||
| def _rnCandidates = [ | ||
| new File(rootDir, "../node_modules/react-native"), // non-hoisted: apps/mobile/node_modules | ||
| new File(rootDir, "../../../node_modules/react-native"), // hoisted: <monorepo-root>/node_modules | ||
| ] | ||
| for (def candidate : _rnCandidates) { | ||
| if (candidate.exists() && new File(candidate, "ReactAndroid/gradle.properties").exists()) { | ||
| _reactNativeDir = candidate | ||
| break | ||
| } | ||
| } | ||
| if (_reactNativeDir != null) { | ||
| // Set on ROOT PROJECT ext directly — read by RNGH via rootProject.ext.get("REACT_NATIVE_DIR") | ||
| ext.REACT_NATIVE_DIR = _reactNativeDir | ||
| ext.REACT_NATIVE_NODE_MODULES_DIR = _reactNativeDir.parentFile | ||
| // Also propagate to all subprojects for other native modules | ||
| subprojects { | ||
| ext.REACT_NATIVE_DIR = _reactNativeDir | ||
| ext.REACT_NATIVE_NODE_MODULES_DIR = _reactNativeDir.parentFile | ||
| } | ||
| logger.lifecycle("[MonorepoFix] react-native resolved at: " + _reactNativeDir.absolutePath) | ||
| } else { | ||
| logger.warn("[MonorepoFix] WARNING: Could not resolve react-native location!") | ||
| } | ||
| // ───────────────────────────────────────────────────────────────────────────── | ||
| `; | ||
|
|
||
| config.modResults.contents = contents + injection; | ||
| return config; | ||
| }); | ||
| }; | ||
|
|
||
| module.exports = withReactNativePickerFix; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| { | ||
| "cli": { | ||
| "version": ">= 18.13.0", | ||
| "appVersionSource": "remote" | ||
| }, | ||
| "build": { | ||
| "development": { | ||
| "developmentClient": true, | ||
| "distribution": "internal" | ||
| }, | ||
| "preview": { | ||
| "distribution": "internal", | ||
| "android": { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Production builds should use AAB format (the default), not APK. APK is appropriate for Prompt for AI agents |
||
| "buildType": "apk" | ||
| }, | ||
| "env": { | ||
| "npm_config_legacy_peer_deps": "true" | ||
| } | ||
| }, | ||
| "production": { | ||
| "autoIncrement": true, | ||
| "android": { | ||
| "buildType": "apk" | ||
| }, | ||
| "env": { | ||
| "npm_config_legacy_peer_deps": "true" | ||
| } | ||
| } | ||
| }, | ||
| "submit": { | ||
| "production": {} | ||
|
Comment on lines
+20
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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 Citations:
Switch 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
RECORD_AUDIOpermission 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