#49752·expo

expo-widgets: widget bundle cannot resolve hoisted packages in a monorepo (override Metro config never watches the workspace root)

Author: grayorganizationCreated Sep 4, 2026Updated Sep 17, 2026
Labelsneeds reviewcontributor: external

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-widgets

apps/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

bash
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_modulesbut 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:

javascript
// 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 bytes

This 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.