one
Version:
One is a new React Framework that makes Vite serve both native and web.
113 lines (102 loc) • 4 kB
text/typescript
import path from 'node:path'
import { buildMetroConfigInputFromViteConfig } from '@vxrn/vite-plugin-metro'
import { getViteMetroPluginOptions } from './getViteMetroPluginOptions'
import { loadUserOneOptions } from '../vite/loadConfig'
export type WithOneOptions = {
/** Absolute path to the project root. Defaults to `process.cwd()`. */
projectRoot?: string
/** Router root folder relative to the project root. Defaults to `'app'`. */
routerRoot?: string
/** Patterns to exclude from router file resolution. */
ignoredRouteFiles?: Array<`**/*${string}`>
/** Routing linking config — mirrors `one({ router: { linking } })`. */
linking?: unknown
/** Native setup file path relative to the project root. */
setupFile?: string | { native?: string; ios?: string; android?: string }
/**
* Load the app's vite.config and use the real One native Metro options.
* Defaults to true so generated Expo/EAS configs match One's own native path.
*/
loadViteConfig?: boolean
}
async function loadUserViteMetroOptions(projectRoot: string) {
const previousCwd = process.cwd()
const previousIsVxrnCli = process.env.IS_VXRN_CLI
try {
process.chdir(projectRoot)
process.env.IS_VXRN_CLI = 'true'
return await loadUserOneOptions('build', true)
} finally {
process.chdir(previousCwd)
if (previousIsVxrnCli === undefined) {
delete process.env.IS_VXRN_CLI
} else {
process.env.IS_VXRN_CLI = previousIsVxrnCli
}
}
}
/**
* Produce a Metro config that invokes the EXACT same `getMetroConfigFromViteConfig`
* pipeline that One's native production builds use. This way `expo export`,
* `eas update`, and any other Metro-direct workflow produce a bundle that's
* byte-equivalent to what `react-native bundle` (the iOS build phase) produces.
*
* The first argument is ignored — kept only for ergonomic compatibility with
* the typical `withOne(getDefaultConfig(__dirname))` call shape that Expo
* users are used to. We discard it because @expo/metro-config's defaults
* differ from what One needs, and the production pipeline applies its own
* defaults internally.
*
* @example
* ```js
* // metro.config.cjs
* const { withOne } = require('one/metro-config')
*
* module.exports = withOne(__dirname)
* ```
*/
export async function withOne(
baseConfigOrProjectRoot: string | object | undefined,
options: WithOneOptions = {}
): Promise<unknown> {
const projectRoot = path.resolve(
typeof baseConfigOrProjectRoot === 'string'
? baseConfigOrProjectRoot
: (options.projectRoot ?? process.cwd())
)
const loaded =
options.loadViteConfig === false
? undefined
: await loadUserViteMetroOptions(projectRoot)
// Reuse the proven pipeline rather than re-implementing it: same resolver,
// same babel transformer path, same sourceExts ordering, same blockList,
// same @babel/runtime workaround. When possible, load the app's actual
// vite.config so native.bundlerOptions/defaultConfigOverrides match the
// One dev/build path.
const metroPluginOptions =
loaded?.metroOptions ??
getViteMetroPluginOptions({
projectRoot,
relativeRouterRoot: options.routerRoot ?? 'app',
ignoredRouteFiles: options.ignoredRouteFiles,
linking: options.linking,
setupFile: options.setupFile,
})
// Call `buildMetroConfigInputFromViteConfig`, NOT `getMetroConfigFromViteConfig` —
// the latter calls Metro's `loadConfig`, which loads the user's metro.config.cjs,
// which calls `withOne`, infinite recursion. The outer `loadConfig` (driven by
// Expo CLI / Metro CLI) does the final config merge.
const viteConfig = {
...loaded?.config?.config,
root: path.resolve(loaded?.config?.config?.root ?? projectRoot),
} as any
const { defaultConfig } = await buildMetroConfigInputFromViteConfig(
viteConfig,
{
...metroPluginOptions,
mainModuleName: 'one/metro-entry',
}
)
return defaultConfig
}
export default withOne