one
Version:
One is a new React Framework that makes Vite serve both native and web.
344 lines (286 loc) • 11.6 kB
text/typescript
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
generateBundlerConfig,
isCiEnvironment,
maybeGenerateBundlerConfigOnInstall,
ONE_GENERATED_MARKER,
} from './generateBundlerConfig'
describe('generateBundlerConfig', () => {
let tmpDir: string
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'one-gen-bundler-test-'))
})
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true })
})
it('writes both files when missing', () => {
const { ok, results } = generateBundlerConfig({ cwd: tmpDir, quiet: true })
expect(ok).toBe(true)
expect(results.map((r) => r.action)).toEqual(['wrote', 'wrote'])
const babel = fs.readFileSync(path.join(tmpDir, 'babel.config.cjs'), 'utf8')
expect(babel).toContain(ONE_GENERATED_MARKER)
expect(babel).toContain("require('one/babel-preset')")
expect(babel).toContain('oneBundlerOptions')
const metro = fs.readFileSync(path.join(tmpDir, 'metro.config.cjs'), 'utf8')
expect(metro).toContain(ONE_GENERATED_MARKER)
expect(metro).toContain("require('one/metro-config')")
expect(metro).toContain('withOne')
})
it('is idempotent on second run', () => {
generateBundlerConfig({ cwd: tmpDir, quiet: true })
const before = fs.readFileSync(path.join(tmpDir, 'babel.config.cjs'), 'utf8')
const { results } = generateBundlerConfig({ cwd: tmpDir, quiet: true })
expect(results.map((r) => r.action)).toEqual(['kept', 'kept'])
const after = fs.readFileSync(path.join(tmpDir, 'babel.config.cjs'), 'utf8')
expect(after).toBe(before)
})
it('refuses to overwrite a customized file without --force', () => {
fs.writeFileSync(
path.join(tmpDir, 'babel.config.cjs'),
"// hand-written\nmodule.exports = { presets: [] }\n"
)
const { ok, results } = generateBundlerConfig({ cwd: tmpDir, quiet: true })
const babelResult = results.find((r) => r.filePath.endsWith('babel.config.cjs'))!
expect(babelResult.action).toBe('skipped-customized')
// ok is true: customized = legitimate user state, not an error
expect(ok).toBe(true)
const after = fs.readFileSync(path.join(tmpDir, 'babel.config.cjs'), 'utf8')
expect(after).toContain('hand-written')
})
it('overwrites a customized file when --force is set', () => {
fs.writeFileSync(
path.join(tmpDir, 'babel.config.cjs'),
"// hand-written\nmodule.exports = { presets: [] }\n"
)
const { results } = generateBundlerConfig({
cwd: tmpDir,
force: true,
quiet: true,
})
const babelResult = results.find((r) => r.filePath.endsWith('babel.config.cjs'))!
expect(babelResult.action).toBe('wrote')
const after = fs.readFileSync(path.join(tmpDir, 'babel.config.cjs'), 'utf8')
expect(after).toContain(ONE_GENERATED_MARKER)
expect(after).not.toContain('hand-written')
})
it('rewrites a stale marked file', () => {
fs.writeFileSync(
path.join(tmpDir, 'babel.config.cjs'),
`// ${ONE_GENERATED_MARKER}\n// old content from previous version\nmodule.exports = {}\n`
)
const { results } = generateBundlerConfig({ cwd: tmpDir, quiet: true })
const babelResult = results.find((r) => r.filePath.endsWith('babel.config.cjs'))!
expect(babelResult.action).toBe('wrote')
const after = fs.readFileSync(path.join(tmpDir, 'babel.config.cjs'), 'utf8')
expect(after).toContain("require('one/babel-preset')")
expect(after).not.toContain('old content from previous version')
})
describe('--check mode', () => {
it('exits ok when files exist and match', () => {
generateBundlerConfig({ cwd: tmpDir, quiet: true })
const { ok, results } = generateBundlerConfig({
cwd: tmpDir,
check: true,
quiet: true,
})
expect(ok).toBe(true)
expect(results.every((r) => r.action === 'kept')).toBe(true)
})
it('exits not-ok when files are missing', () => {
const { ok, results } = generateBundlerConfig({
cwd: tmpDir,
check: true,
quiet: true,
})
expect(ok).toBe(false)
expect(results.every((r) => r.action === 'would-write')).toBe(true)
// and didn't actually write
expect(fs.existsSync(path.join(tmpDir, 'babel.config.cjs'))).toBe(false)
})
it('exits not-ok when a marked file is stale', () => {
fs.writeFileSync(
path.join(tmpDir, 'babel.config.cjs'),
`// ${ONE_GENERATED_MARKER}\nmodule.exports = {}\n`
)
fs.writeFileSync(
path.join(tmpDir, 'metro.config.cjs'),
`// ${ONE_GENERATED_MARKER}\nmodule.exports = {}\n`
)
const { ok, results } = generateBundlerConfig({
cwd: tmpDir,
check: true,
quiet: true,
})
expect(ok).toBe(false)
expect(results.every((r) => r.action === 'would-overwrite')).toBe(true)
})
})
describe('--eject mode', () => {
it('writes files WITHOUT the @one/generated marker', () => {
const { results } = generateBundlerConfig({ cwd: tmpDir, eject: true, quiet: true })
expect(results.map((r) => r.action)).toEqual(['wrote', 'wrote'])
const babel = fs.readFileSync(path.join(tmpDir, 'babel.config.cjs'), 'utf8')
expect(babel).not.toContain(ONE_GENERATED_MARKER)
expect(babel).toContain('you own this file')
expect(babel).toContain("require('one/babel-preset')")
const metro = fs.readFileSync(path.join(tmpDir, 'metro.config.cjs'), 'utf8')
expect(metro).not.toContain(ONE_GENERATED_MARKER)
expect(metro).toContain('withOne')
})
it('subsequent auto-gen run (no --eject) treats ejected files as customized', () => {
generateBundlerConfig({ cwd: tmpDir, eject: true, quiet: true })
const before = fs.readFileSync(path.join(tmpDir, 'babel.config.cjs'), 'utf8')
// simulate CI postinstall: regular generateBundlerConfig should skip these
const { results } = generateBundlerConfig({ cwd: tmpDir, quiet: true })
expect(results.every((r) => r.action === 'skipped-customized')).toBe(true)
const after = fs.readFileSync(path.join(tmpDir, 'babel.config.cjs'), 'utf8')
expect(after).toBe(before)
})
})
it('does not clobber a non-.cjs config in the same family', () => {
fs.writeFileSync(
path.join(tmpDir, 'babel.config.js'),
'module.exports = { presets: [] }\n'
)
const { results } = generateBundlerConfig({ cwd: tmpDir, quiet: true })
const babelResult = results.find(
(r) => r.filePath.endsWith('babel.config.js') || r.filePath.endsWith('babel.config.cjs')
)!
expect(babelResult.action).toBe('skipped-other-format')
// didn't write the .cjs alongside
expect(fs.existsSync(path.join(tmpDir, 'babel.config.cjs'))).toBe(false)
})
it('embeds loaded One router/setup options into both config files', () => {
generateBundlerConfig({
cwd: tmpDir,
quiet: true,
oneOptions: {
router: {
root: 'src/routes',
ignoredRouteFiles: ['**/*.native-test.*'],
linking: { scheme: 'myapp', prefixes: ['https://example.com/app'] },
},
setupFile: {
native: 'src/setup.native.ts',
},
},
})
const babel = fs.readFileSync(path.join(tmpDir, 'babel.config.cjs'), 'utf8')
const metro = fs.readFileSync(path.join(tmpDir, 'metro.config.cjs'), 'utf8')
for (const file of [babel, metro]) {
expect(file).toContain('"routerRoot": "src/routes"')
expect(file).toContain('"ignoredRouteFiles"')
expect(file).toContain('"**/*.native-test.*"')
expect(file).toContain('"scheme": "myapp"')
expect(file).toContain('"native": "src/setup.native.ts"')
}
})
it('refuses to silently drop non-serializable options', () => {
expect(() =>
generateBundlerConfig({
cwd: tmpDir,
quiet: true,
oneOptions: {
router: {
linking: {
// the plugin API only accepts serializable router.linking fields
filter: () => true,
} as any,
},
},
})
).toThrow(/JSON-serializable/)
})
})
describe('isCiEnvironment', () => {
const originalCi = process.env.CI
const originalEasBuild = process.env.EAS_BUILD
afterEach(() => {
if (originalCi === undefined) delete process.env.CI
else process.env.CI = originalCi
if (originalEasBuild === undefined) delete process.env.EAS_BUILD
else process.env.EAS_BUILD = originalEasBuild
})
it('is true when CI=true', () => {
process.env.CI = 'true'
delete process.env.EAS_BUILD
expect(isCiEnvironment()).toBe(true)
})
it('is true when CI=1', () => {
process.env.CI = '1'
delete process.env.EAS_BUILD
expect(isCiEnvironment()).toBe(true)
})
it('is true when EAS_BUILD=true', () => {
delete process.env.CI
process.env.EAS_BUILD = 'true'
expect(isCiEnvironment()).toBe(true)
})
it('is false when neither is set', () => {
delete process.env.CI
delete process.env.EAS_BUILD
expect(isCiEnvironment()).toBe(false)
})
it('is false when CI=false or CI=0', () => {
delete process.env.EAS_BUILD
process.env.CI = 'false'
expect(isCiEnvironment()).toBe(false)
process.env.CI = '0'
expect(isCiEnvironment()).toBe(false)
})
})
describe('maybeGenerateBundlerConfigOnInstall', () => {
let tmpDir: string
const originalCi = process.env.CI
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'one-gen-bundler-ci-test-'))
})
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true })
if (originalCi === undefined) delete process.env.CI
else process.env.CI = originalCi
delete process.env.EAS_BUILD
})
it('does NOT generate files when not in CI even if expo-updates is present', () => {
delete process.env.CI
delete process.env.EAS_BUILD
// fake expo-updates in node_modules
const fakeUpdates = path.join(tmpDir, 'node_modules/expo-updates')
fs.mkdirSync(fakeUpdates, { recursive: true })
fs.writeFileSync(
path.join(fakeUpdates, 'package.json'),
JSON.stringify({ name: 'expo-updates' })
)
maybeGenerateBundlerConfigOnInstall(tmpDir)
expect(fs.existsSync(path.join(tmpDir, 'babel.config.cjs'))).toBe(false)
expect(fs.existsSync(path.join(tmpDir, 'metro.config.cjs'))).toBe(false)
})
it('does NOT generate files in CI when expo-updates is absent', () => {
process.env.CI = 'true'
// no expo-updates installed
maybeGenerateBundlerConfigOnInstall(tmpDir)
expect(fs.existsSync(path.join(tmpDir, 'babel.config.cjs'))).toBe(false)
expect(fs.existsSync(path.join(tmpDir, 'metro.config.cjs'))).toBe(false)
})
it('generates files in CI when expo-updates is present', () => {
process.env.CI = 'true'
const fakeUpdates = path.join(tmpDir, 'node_modules/expo-updates')
fs.mkdirSync(fakeUpdates, { recursive: true })
fs.writeFileSync(
path.join(fakeUpdates, 'package.json'),
JSON.stringify({ name: 'expo-updates' })
)
// suppress info logs
const consoleInfo = vi.spyOn(console, 'info').mockImplementation(() => {})
try {
maybeGenerateBundlerConfigOnInstall(tmpDir)
} finally {
consoleInfo.mockRestore()
}
expect(fs.existsSync(path.join(tmpDir, 'babel.config.cjs'))).toBe(true)
expect(fs.existsSync(path.join(tmpDir, 'metro.config.cjs'))).toBe(true)
})
})