expo-widgets: widget bundle cannot resolve hoisted packages in a monorepo (override Metro config never watches the workspace root)
Minimal reproducible example: https://github.com/grayorganization/expo-widgets-monorepo-repro (clone, run the four commands in its README; fails as checked in, passes with the one-line fix shown there). This replaces #49750, which was closed for lacking a repro.
Minimal reproduction for a build-blocking bug in expo-widgets.
The Xcode (Pods) build phase that builds the widget JS bundle fails whenever
expo-widgets resolves to a workspace-local node_modules while its own
dependencies are hoisted to the monorepo root — which is the normal npm layout as
soon as anything occupies the root slot.
Versions
| package | version |
|---|---|
| expo-widgets | 57.0.17 |
| expo | 57.0.20 |
| react-native | 0.86.3 |
| npm | 11.6.2 |
| node | v24.12.0 |
| Xcode | Xcode 26.6 |
| macOS | 26.5.2 |
Shape
An npm-workspaces monorepo:
package.json workspaces: apps/*
apps/mobile the standard Expo SDK 57 blank-typescript template
+ expo-widgets + one hello-world widget
apps/legacy a second workspace pinning an older expo-widgetsapps/legacy exists only to occupy the root node_modules/expo-widgets slot, so
npm nests apps/mobile's copy. That nesting is the whole precondition — any
monorepo where the root slot is taken produces it, and it is not specific to a
version conflict.
Steps
npm ci
cd apps/mobile
npx expo prebuild --platform ios --no-install
mkdir -p ios/Pods
PROJECT_DIR="$PWD/ios/Pods" PROJECT_ROOT="$PWD" PODS_ROOT="$PWD/ios/Pods" \
NODE_BINARY="$(command -v node)" \
bash node_modules/expo-widgets/scripts/xcode-build-bundle.sh(The environment variables are the ones Xcode supplies to that phase. The script
no-ops unless PROJECT_DIR's basename is Pods.)
Expected
The script exits 0 and writes expo-widgets/bundle/build/ExpoWidgets.bundle.
Actual
It exits 1 without producing a bundle:
iOS Bundling failed 13ms apps/mobile/node_modules/expo-widgets/bundle/index.ts (1 module)
Unable to resolve module @babel/runtime/helpers/interopRequireWildcard from
<repro>/apps/mobile/node_modules/expo-widgets/bundle/index.ts:
@babel/runtime/helpers/interopRequireWildcard could not be found within the
project or in these directories:
..
../../../../node_modules
Import stack:
apps/mobile/node_modules/expo-widgets/bundle/index.ts
| import "@babel/runtime/helpers/interopRequireWildcard"@babel/runtime is only the first casualty the graph reaches — it is what
import * as swiftUI from '@expo/ui/swift-ui' compiles to. Nesting a private
copy of @babel/runtime under apps/mobile clears that error and the next one
is metro-runtime/src/modules/empty-module.js, also hoisted. The entire hoisted
tree is unreachable, so this is not a single-package problem.
Root cause
expo-widgets/metro.config.js sets projectRoot to its own directory inside
node_modules so it can bundle bundle/index.ts as the entry, but it inherits
watchFolders from getDefaultConfig(process.cwd()), which in a workspace lists
every package and the root node_modules — but never the repo root itself.
With projectRoot relocated inside node_modules, Metro can no longer serve
anything hoisted to the root, so every hoisted import fails to resolve.
The one-line fix
Adding the repo root to watchFolders makes the same script succeed:
// expo-widgets/metro.config.js
const buildConfig = {
...config,
projectRoot: __dirname,
watchFolders: [...watchFolders, <repo root>], // <-- this
...
};Verified by running the same expo export:embed invocation the build phase uses,
with EXPO_OVERRIDE_METRO_CONFIG pointed at a config that adds only that entry:
iOS Bundled 2136ms apps/mobile/node_modules/expo-widgets/bundle/index.ts (105 modules)
Writing bundle output to: node_modules/expo-widgets/bundle/build/ExpoWidgets.bundle
Done writing bundle output
exit 0 — ExpoWidgets.bundle, 144691 bytesThis repo is checked in unpatched, so the steps above fail as written. The fix above is shown for diagnosis only.
Why a consumer cannot work around it in app config
expo-widgets/scripts/build-bundle.mjs sets EXPO_OVERRIDE_METRO_CONFIG
explicitly in the spawn environment, so the app's own metro.config.js — which
solves exactly this class for the app bundle — is never consulted for the widget
graph. The only lever available is patching the installed file.
Source: expo/expo