Browser Metro Bundler
browser-metro is a unique, client-side JavaScript and TypeScript bundler designed to run entirely within a web browser, typically leveraging a Web Worker for performance. Inspired by React Native's Metro bundler, it provides features like Hot Module Replacement (HMR), React Refresh support, and integration with Expo Router for file-based routing. It manages modules through a VirtualFS, performs rapid compilation of TypeScript and JSX via Sucrase transforms, and supports on-demand bundling of npm packages through an external ESM server (e.g., `https://esm.reactnative.run`). As of version 1.0.15, it emphasizes rapid development feedback loops in browser-based playgrounds and development environments. Its key differentiator is its completely client-side operation, removing the need for a Node.js build server for many common development tasks, making it ideal for interactive coding environments and sandboxes.
Common errors
-
Error: Module not found: Can't resolve 'some-module' in '/path/to/file.ts'
cause The bundler could not locate the imported module. This often happens due to incorrect import paths, missing files in the `VirtualFS`, or an unconfigured `sourceExts` for the module type.fixVerify the import path is correct and the module exists in the `VirtualFS`. Ensure `resolver.sourceExts` in your `BundlerConfig` includes the file extension of the module (e.g., `['ts', 'tsx', 'js', 'jsx']`). If importing an npm package, check network connectivity to your `packageServerUrl` and that the package exists on the remote server. -
TypeError: transformer is not a function
cause The `transformer` option in `BundlerConfig` was not correctly provided or is not a valid transformer function (e.g., `typescriptTransformer` or `reactRefreshTransformer`).fixEnsure you are importing and passing a valid transformer, such as `typescriptTransformer` or `reactRefreshTransformer`, to your `BundlerConfig`. If creating a custom transformer, it must adhere to the `Transformer` interface specified by `browser-metro`. -
React Refresh: A wild runtime error occurred. (or similar HMR-related console errors)
cause React Refresh typically fails when components are not exportable (e.g., default exports of anonymous functions), or when there are issues with multiple instances of React/React Refresh, or problems with the HMR update application logic on the client side.fixEnsure all React components are named exports or default exports of named functions. Verify your HMR client-side logic correctly processes `hmrUpdate` objects from `IncrementalBundler` and applies patches to the iframe or target environment. Check for duplicate React installations if possible.
Warnings
- gotcha Performance for large projects can be a bottleneck. As `browser-metro` runs entirely client-side, bundling extremely large or complex codebases may lead to noticeable performance degradation, especially on less powerful devices or older browsers.
- gotcha `browser-metro` relies on an external ESM package server (e.g., `https://esm.reactnative.run`) for resolving and bundling npm packages. Downtime, network issues, or rate limiting from this service can prevent package resolution and bundling.
- gotcha Correctly configuring HMR and React Refresh requires careful setup. Incorrect transformer choices (e.g., not using `reactRefreshTransformer`) or improper client-side handling of HMR updates will prevent hot updates and lead to full page reloads.
Install
-
npm install browser-metro -
yarn add browser-metro -
pnpm add browser-metro
Imports
- Bundler
import Bundler from 'browser-metro'; // Not a default export
import { Bundler } from 'browser-metro'; - IncrementalBundler
const IncrementalBundler = require('browser-metro').IncrementalBundler; // Primarily ESM-focusedimport { IncrementalBundler } from 'browser-metro'; - VirtualFS
import { Fs as VirtualFS } from 'browser-metro'; // Incorrect aliasimport { VirtualFS } from 'browser-metro'; - typescriptTransformer
import { tsTransformer } from 'browser-metro';import { typescriptTransformer } from 'browser-metro'; - BundlerConfig
import type { BundlerConfig } from 'browser-metro';
Quickstart
import { Bundler, VirtualFS, typescriptTransformer } from "browser-metro";
import type { FileMap } from "browser-metro";
async function initializeBundler() {
const files: FileMap = {
"/index.ts": 'import { greet } from "./utils";\nconsole.log(greet("World"));',
"/utils.ts": 'export function greet(name: string) { return "Hello, " + name; }',
};
const bundler = new Bundler(new VirtualFS(files), {
resolver: { sourceExts: ["ts", "tsx", "js", "jsx"] },
transformer: typescriptTransformer,
server: { packageServerUrl: "https://esm.reactnative.run" }, // Required for npm packages
});
try {
const code = await bundler.bundle("/index.ts");
console.log("Bundled Code:\n", code);
// To execute in browser context, you might create a Blob URL or inject into an iframe
// const blob = new Blob([code], { type: 'application/javascript' });
// const url = URL.createObjectURL(blob);
// const iframe = document.createElement('iframe');
// iframe.src = url;
// document.body.appendChild(iframe);
} catch (error) {
console.error("Bundling failed:", error);
}
}
initializeBundler();