npm.io
0.1.0-alpha.0 • Published 2d ago

@facilitronworks/expo-metro-windows

Licence
MIT
Version
0.1.0-alpha.0
Deps
0
Size
21 kB
Vulns
0
Weekly
0

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:

  1. a platform resolver that rewrites react-nativereact-native-windows for platform === 'windows' (metroPlatformResolver.js), and
  2. 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-nativereact-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 to config.projectRoot (set by Expo's getDefaultConfig).
  • 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-camerareact-native-windows-camera, …).
  • blockWindowsBuildArtifacts: boolean — default true.

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:

  1. @expo/metro-config: recognize out-of-tree platforms. When platform === 'windows' (declared via expo.platforms or detected from react-native-windows in deps), apply the platform redirect (fix 3) and the InitializeCore swap (fix 4) exactly as @react-native/community-cli-plugin does for standalone RNW apps — both behaviors already exist upstream in the community plugin; Expo just needs to honor them.
  2. expo-modules-core: a windows polyfill/no-op entry so expo's boot path (winter runtime, requireNativeModule guards) degrades gracefully on platforms without Expo native runtime support, removing the need for app-side stub maps (fix 2 + the shims option).

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-config output.
  • 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.

Keywords