one
Version:
One is a React framework for web, iOS, and Android apps.
179 lines (163 loc) • 6.54 kB
text/typescript
import path from 'node:path'
import { transformSync } from '@babel/core'
import { describe, expect, it } from 'vitest'
import oneBabelPreset, { buildOneBabelPlugins } from './index'
const projectRoot = path.resolve(__dirname, '../../')
/**
* Integration test: end-to-end load oneBabelPreset through @babel/core and
* run it against representative One sources. Catches breakage in:
* - plugin specifier resolution (each `'one/babel-plugin-*'` must resolve)
* - plugin option shape (each plugin gets a valid config)
* - chain ordering (plugins compose without conflicts)
*
* Per-plugin semantic tests live alongside each plugin file. This file only
* verifies that the preset wires them up correctly.
*/
describe('one/babel-preset integration', () => {
// wiring tests run the One chain directly so assertions see untransformed
// modules; syntax tests below run the full preset with its RN base.
const onePlugins = buildOneBabelPlugins({ projectRoot, relativeRouterRoot: 'app' })
const presetWithBase = [oneBabelPreset, { projectRoot }] as const
// caller the Metro babel transformer sets on the `one dev` path, where the
// Vite plugin already injected the One chain via customTransformOptions.
const metroViteCaller = {
name: 'metro',
bundler: 'metro',
platform: 'ios',
projectRoot,
oneViteMetroBabelConfig: true,
}
it('runs against a route file without throwing', () => {
const code = `
import { useLoader } from 'one'
export default function Page() {
return null
}
`
const result = transformSync(code, {
filename: path.join(projectRoot, 'app/index.tsx'),
cwd: projectRoot,
plugins: onePlugins,
parserOpts: { sourceType: 'module', plugins: ['jsx'] },
})
expect(result?.code).toBeTruthy()
// module-resolver should turn 'one' into a relative path under this
// package because tsconfig.base.json has `one` aliased to packages/one
// No assertion on the exact path — but it must still mention 'useLoader'
expect(result?.code).toContain('useLoader')
})
it('substitutes ONE_ROUTER_* process.env placeholders in metro-entry', () => {
// metro-entry-ctx.js uses process.env.ONE_ROUTER_* as placeholders that
// one-router-metro replaces with the configured values at bundle time.
const code = `
const ctx = require.context(
process.env.ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY,
true,
process.env.ONE_ROUTER_REQUIRE_CONTEXT_REGEX_STRING
)
const folder = process.env.ONE_ROUTER_ROOT_FOLDER_NAME
module.exports = { ctx, folder }
`
const result = transformSync(code, {
filename: path.join(projectRoot, 'metro-entry-ctx.js'),
cwd: projectRoot,
plugins: onePlugins,
parserOpts: { sourceType: 'module' },
})
expect(result?.code).toBeTruthy()
// placeholders must be replaced
expect(result?.code).not.toContain(
'process.env.ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY'
)
expect(result?.code).not.toContain('process.env.ONE_ROUTER_ROOT_FOLDER_NAME')
expect(result?.code).not.toContain(
'process.env.ONE_ROUTER_REQUIRE_CONTEXT_REGEX_STRING'
)
// routerRoot should be inlined as 'app'
expect(result?.code).toContain('"app"')
// require-context regex should be inlined as a regex literal
expect(result?.code).toMatch(/\/\^.*\.tsx\?\$\//)
})
it('keeps client code intact in non-route files', () => {
const code = `
const x = 1
export default x
`
const result = transformSync(code, {
filename: path.join(projectRoot, 'src/utils/x.ts'),
cwd: projectRoot,
plugins: onePlugins,
parserOpts: { sourceType: 'module' },
})
expect(result?.code).toContain('const x = 1')
})
it('strips `import type` on the one dev Metro path', () => {
// every route file is TS. with no base preset Metro dies with
// `TransformError: Unexpected token, expected "from"` on this line.
const code = `
import { Tabs } from 'one'
import type { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs'
export default function Layout() {
return null
}
`
const result = transformSync(code, {
filename: path.join(projectRoot, 'app/(tabs)/_layout.tsx'),
cwd: projectRoot,
presets: [presetWithBase],
caller: metroViteCaller,
})
expect(result?.code).toBeTruthy()
expect(result?.code).not.toContain('import type')
expect(result?.code).toContain('Layout')
})
it('strips flow `import typeof` on the one dev Metro path', () => {
// react-native/index.js ships `import typeof ... from './index.js.flow'`.
// unstripped, Metro tries to resolve the .flow file and the iOS bundle
// 500s with UnableToResolveError.
const code = `import typeof * as ReactNativePublicAPI from './index.js.flow'
const x = 1
module.exports = x
`
const result = transformSync(code, {
filename: path.join(projectRoot, 'node_modules/react-native/index.js'),
cwd: projectRoot,
presets: [presetWithBase],
caller: metroViteCaller,
})
expect(result?.code).toBeTruthy()
expect(result?.code).not.toContain('index.js.flow')
})
it('executes namespace re-exports on the Metro path', () => {
const result = transformSync('export * as core from "./core.js"', {
filename: path.join(projectRoot, 'node_modules/dependency/index.js'),
cwd: projectRoot,
presets: [presetWithBase],
caller: metroViteCaller,
})
const exports: Record<string, unknown> = {}
const core = { answer: 42 }
new Function('exports', 'require', result?.code ?? '')(exports, () => core)
expect(exports.core).toMatchObject(core)
})
it('removes namespace type exports before transforming runtime exports', () => {
const result = transformSync(
'export type * as Types from "./types"; export * as core from "./core.js"',
{
filename: path.join(projectRoot, 'src/namespace.ts'),
cwd: projectRoot,
presets: [presetWithBase],
caller: metroViteCaller,
}
)
const requested: string[] = []
const exports: Record<string, unknown> = {}
new Function('exports', 'require', result?.code ?? '')(exports, (id: string) => {
requested.push(id)
return { answer: 42 }
})
expect(requested).toEqual(['./core.js'])
expect(exports.core).toMatchObject({ answer: 42 })
expect(exports).not.toHaveProperty('Types')
})
})