WIP — pre-release. The four-fix recipe inside is production-proven (a shipping app bundles iOS, Android, web, AND Windows from one Metro tree with exactly these transforms), but this extracted standalone package has not yet been consumed by a second app — treat it as unvalidated packaging of a validated pattern until the first tagged release.
@facilitronworks/expo-metro-windows
The bridge that makes Expo's bundler understand react-native-windows: a
withExpoMetroWindows(config) wrapper that applies the four-fix coexistence
recipe letting an Expo app (expo/metro-config + expo start) serve a
react-native-windows New-Architecture app from the same Metro instance and
the same tree as iOS, Android, and web.
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const { withExpoMetroWindows } = require('@facilitronworks/expo-metro-windows')
module.exports = withExpoMetroWindows(getDefaultConfig(__dirname))
Why Expo's Metro can't bundle for Windows out of the box
A standalone RNW app runs Metro through @react-native/community-cli-plugin,
which quietly installs two Windows-critical behaviors:
- a platform resolver that rewrites
react-native→react-native-windowsforplatform === 'windows'(metroPlatformResolver.js), and - an InitializeCore swap in
getModulesRunBeforeMainModule(loadMetroConfig).
Expo's getDefaultConfig + expo start never loads that plugin — Expo
has its own config pipeline — so on Windows you get neither, plus Expo's
stricter resolver surfaces RN-internal files that ship no .windows.js. Four
distinct failures result. This package fixes all of them, each independently
diagnosable:
The four fixes
| # | Fix | Symptom without it |
|---|---|---|
| 1 | Monorepo server-root bundle path (native-side; helper provided) — Expo roots Metro's server at the workspace root, so the native host must request <app-dir>/index.windows, not index.windows. Use windowsBundleRequestPath(projectRoot) to compute it, then set it in your WinMain: settings.JavaScriptBundleFile(L"my-app/index.windows") |
HTTP 404 UnableToResolveError ./index.windows from <workspaceRoot>/. → red-box "Unable to load script" |
| 2 | RN-internals windows stubs — RN core files that ship only .ios.js/.android.js (e.g. ReactDevToolsSettingsManager) are no-op'd for windows (RN community CLI's config tolerates the miss; Expo's surfaces it) |
HTTP 500 UnableToResolveError at bundle time |
| 3 | react-native → react-native-windows resolveRequest redirect (with per-submodule fallback to react-native when RNW doesn't override) — the platform resolver Expo bypasses. The killer detail: RN core's backwards-compat Libraries/Utilities/Platform.js does import Platform from './Platform', which with no Platform.windows.js in RN core resolves to itself → circular → undefined |
runtime TypeError: Cannot read property 'OS' of undefined at boot |
| 4 | InitializeCore serializer swap, gated on a .windows entry — Expo's getModulesRunBeforeMainModule returns RN core's InitializeCore.js by absolute path (bypassing fix 3's name-based redirect); its relative requires then stay inside RN core, so Platform + HMRClient never initialize. We map it to react-native-windows/Libraries/Core/InitializeCore.js for windows entries only |
Failed to call into JS module method HMRClient.setup() … not registered as callable. Registered callable modules (n=1): AppRegistry |
Plus the prerequisite plumbing: registering the windows Metro platform and
block-listing <projectRoot>/windows/** + RNW's build//target/ +
*.ProjectImports.zip so run-windows MSBuild output doesn't EBUSY-crash or
OOM the Metro watcher.
API
withExpoMetroWindows(config, options?)
Mutates and returns the config. Options:
projectRoot— defaults toconfig.projectRoot(set by Expo'sgetDefaultConfig).stubBasenames: string[]— override the RN-internal basenames to stub (default['ReactDevToolsSettingsManager']; grows per RN version as boot surfaces new.ios.js-only internals).shims: Record<string, string>— moduleName → absolute file path aliases applied first on windows. This is where your app maps packages with no Windows native implementation to local shims (react-native-mmkv→ a native-KV shim,expo-camera→ react-native-windows-camera, …).blockWindowsBuildArtifacts: boolean— defaulttrue.
Fixes 2–4 are always applied; your existing resolver.resolveRequest (web
shims etc.) keeps working — it is chained after the windows branch.
windowsBundleRequestPath(projectRoot, serverRoot?, entryBaseName?)
Fix 1's helper: returns the forward-slash bundle path the native host must
request (e.g. "my-app/index.windows"). Fix 1 is inherently native-side
(one line in your WinMain/host settings) — a JS package can't apply it for
you, only compute it.
Status: what's proven vs pending
| Claim | Status |
|---|---|
| The four-fix pattern itself | Production-proven. A shipping Expo 55 / RN 0.83.2 / RNW 0.83.2 monorepo app (Facilitron FIT) bundles all four platforms from one tree: iOS + Android via expo start/EAS, web via Expo export, and the Windows new-arch app served by the same Metro instance — running this way in production since July 2026, verified again on the production branch as of July 10–11, 2026. The wrapper is a line-for-line extraction of that app's metro.config.js windows branch. |
This packaged withExpoMetroWindows function |
Pending — extraction from a working production config; the standalone package has not yet been consumed by a second app. The delta vs the proven code is packaging only (options plumbing, require.resolve from projectRoot, merged blockList), but we haven't run it, so we won't claim it. |
windowsBundleRequestPath workspace-root inference |
Pending (the production app hardcodes its known path) |
The endgame: make this package obsolete
This repo is a working proof + interim path, not a destination. Two surgical upstream PRs would delete it:
@expo/metro-config: recognize out-of-tree platforms. Whenplatform === 'windows'(declared viaexpo.platformsor detected fromreact-native-windowsin deps), apply the platform redirect (fix 3) and the InitializeCore swap (fix 4) exactly as@react-native/community-cli-plugindoes for standalone RNW apps — both behaviors already exist upstream in the community plugin; Expo just needs to honor them.expo-modules-core: a windows polyfill/no-op entry soexpo's boot path (winter runtime,requireNativeModuleguards) degrades gracefully on platforms without Expo native runtime support, removing the need for app-side stub maps (fix 2 + theshimsoption).
We intend to open both PRs with this repo as the running evidence. If you're from the Expo team and reading this: issue links will land here when filed — or reach out, the diff is small and we have a production app to validate against.
Relationship to upstream
- Not a fork of anything. Pure additive config wrapper over
expo/metro-configoutput. - Fixes 3 and 4 are re-implementations (for Expo's pipeline) of behaviors that
live in
@react-native/community-cli-plugin— credit where due; the RNW + RN teams solved this for the standalone case long ago. - Works with or without Expo Router / expo dev-client; the production app uses expo-router on all four platforms.
License
MIT.