One uses Rolldown for native builds by default. Metro is available when you select it explicitly.
Configure the native bundler to Metro in your vite.config.ts:
Install the Expo Babel preset in the app first:
bun add -d babel-preset-expoOne reports an installation error when Metro starts without it. The default Rolldown path does not load the preset.
import type { UserConfig } from 'vite'import { one } from 'one/vite'
export default { plugins: [ one({ native: { // Enable Metro bundler for native builds bundler: 'metro', }, }), ],} satisfies UserConfigWhen using Metro mode, only some of the Vite features are supported. One provides only a subset of Vite functionality in Metro mode to maintain compatibility and stability.
metro.config.js.Only the following Vite features and configurations are supported in Metro mode for now:
import.meta.env Built-in Constants Partially Supported
: supported but may not work as expected for web specific variables.import.meta.env Env Variables Supported
tsconfig-paths Partially Supported
: As one automatically configuresvite-tsconfig-paths to support path mappings defined in tsconfig.json for your project, the same applies to Metro mode. However, if you are using the vite-tsconfig-paths plugin manually, the custom options you provide there will not be applied for Metro.By default, One starts from React Native’s Metro configuration and applies the same One router, environment, and server transforms used by the native Rolldown path.
Like ordinary React Native apps, you can still configure Metro using metro.config.js for more advanced settings, such as resolver or watcher options:
const { withOne } = require('one/metro-config')
module.exports = withOne(__dirname)metro.config.js works exactly as it would in a standard React Native project, giving you full control over the bundling process.
By default, One uses its own Metro worker built around Oxc. The worker combines Oxc’s Rust parser and transforms with TypeScript implementations of One’s native transforms, Hermes parsing for Flow, and esbuild for syntax lowering. This pipeline runs without Babel.
A Babel plugin One has no port for would silently do nothing, so instead the build fails and names it. You have three options:
native.bundlerOptions.nativeTransformModules. Each entry is a module id (Metro runs transforms in separate worker processes, so it has to be serializable) that default-exports (code, ctx) => string | null. Your transforms run first, on the original source, with an absolute ctx.filename.native.bundlerOptions.babelConfigOverrides.native.bundlerOptions.nativeTransforms: false to use Babel, or ONE_METRO_NATIVE_TRANSFORMS=0 for a single run. Install @react-native/babel-preset, then run one metro-eject to create a Babel config that delegates to one/babel-preset, which builds on React Native’s preset. Expo presets are optional app configuration and are never loaded by One.A native transform receives source text and returns updated source text. It can use Oxc or another tool suited to the transformation:
module.exports = function myTransform(code, ctx) { // ctx: { filename, platform, dev, projectRoot } if (!code.includes('__BUILD_ID')) return null return code.replace(/__BUILD_ID/g, JSON.stringify(process.env.BUILD_ID))}one({ native: { bundler: 'metro', bundlerOptions: { nativeTransformModules: ['./scripts/my-transform.cjs'], }, },})Return null to leave the file alone. Transforms run in order, before One’s built-in transforms. The first receives the original file contents; each subsequent transform receives the previous transform’s output. A transform that records original source positions should run first.
By default, Metro starts as soon as the Vite dev server is ready. If you’re primarily doing web development but have Metro configured, you can defer Metro startup until a native client actually connects:
import type { UserConfig } from 'vite'import { one } from 'one/vite'
export default { plugins: [ one({ native: { bundler: 'metro', bundlerOptions: { startup: 'lazy', }, }, }), ],} satisfies UserConfigOr via environment variable:
ONE_METRO_LAZY=1 one devWith lazy startup:
one dev/status, it gets an immediate response while Metro starts in the backgroundThis can significantly speed up dev server startup when you don’t always need native builds.
Metro can use more mature React Native tooling. One’s default Rolldown path remains the recommended choice for new apps. Both native bundlers use Oxc by default; Metro can opt back into Babel.
Edit this page on GitHub.