UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

344 lines (286 loc) 11.6 kB
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) }) })