ESM Package Server
reactnative-esm is an Express service that bundles npm packages on-demand for browser consumption. It follows an unpkg-style URL scheme and caches results to disk.
In production, it's hosted at https://esm.reactnative.run.
For optimal performance, browser-metro uses batch dependency fetching to request all packages in a single request instead of 60+ individual ones.
URL format
GET /pkg/<package>[@<version>][/<subpath>]| URL | Package | Version | Subpath |
|---|---|---|---|
/pkg/lodash | lodash | latest | - |
/pkg/[email protected] | lodash | 4.17.21 | - |
/pkg/react-dom/client | react-dom | latest | /client |
/pkg/react-dom@19/client | react-dom | 19 | /client |
/pkg/@scope/[email protected]/sub | @scope/name | 1.0 | /sub |
How it works
- Parse the URL into package name, version, and optional subpath
- Check disk cache - if cached, serve immediately
- Create temp directory and run
npm install <package>@<version> - Read package metadata to collect all
dependencies+peerDependenciesas externals - Detect React Native/Expo packages and apply special config
- Bundle with esbuild (IIFE format, browser platform) with selective external plugin
- Wrap output with
module.exports = __module - Cache to disk (both
.jsbundle and.externals.jsonmanifest) - Return response with
X-Externalsheader
Dependency externalization
All dependencies and peerDependencies are externalized during bundling. This means they remain as require() calls in the output rather than being inlined. This ensures shared transitive deps are loaded once at runtime.
Selective external plugin
- Bare imports (
require("react")) - always externalized - Subpath imports of react/react-dom/react-native - always externalized
- Other subpath imports - try to resolve locally first; only externalize if resolution fails
Version pinning via X-Externals
The server tracks installed versions of all externalized dependencies:
X-Externals: {"react":"19.1.0","react-dom":"19.1.0","memoize-one":"4.1.0"}The bundler reads this header and uses pinned versions for transitive dependencies, preventing version mismatches.
Version resolution priority:
- User's
package.jsonversion (explicit constraint) - Transitive dep version from
X-Externals(pinned by parent) - Latest (fallback)
React Native / Expo package handling
Packages are detected as React Native/Expo when:
- Package name starts with
@expo/ - Package name contains
react-native - Package keywords include
react-nativeorexpo
For these packages, esbuild gets additional config:
- Resolve extensions:
.web.tsx,.web.ts,.web.jsprioritized - Loaders:
.jstreated as JSX, fonts/images as data URLs - Extra externals:
react-native,react,react-domalways externalized
Caching
Bundled packages are cached to disk:
cache/
[email protected]
[email protected]
[email protected]__client.js # subpath "/" → "__"
[email protected]__client.externals.jsonTo clear all cached packages:
rm -f reactnative-esm/cache/*.js reactnative-esm/cache/*.jsonRunning locally
cd reactnative-esm
npm install
npm run dev # with auto-reload
npm start # production
# Server runs on http://localhost:5200