Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PLAT-4932] feat(react-native-cli): Add install command #1141

Merged
merged 2 commits into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/react-native-cli/src/bin/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import commandLineArgs from 'command-line-args'
import commandLineUsage from 'command-line-usage'
import logger from '../Logger'

import automateSymbolication from '../commands/AutomateSymbolicationCommand'
import install from '../commands/InstallCommand'

const topLevelDefs = [
{
Expand Down Expand Up @@ -31,15 +33,18 @@ export default async function run (argv: string[]): Promise<void> {
)
}

const remainingOpts = opts._unknown || []
switch (opts.command) {
case 'init':
case 'install':
case 'insert':
case 'configure':
logger.info(`TODO ${opts.command}`)
break
case 'install':
await install(remainingOpts, opts)
break
case 'automate-symbolication':
await automateSymbolication(opts._unknown || [], opts)
await automateSymbolication(remainingOpts, opts)
break
default:
if (opts.help) return usage()
Expand Down
41 changes: 41 additions & 0 deletions packages/react-native-cli/src/commands/InstallCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import prompts from 'prompts'
import logger from '../Logger'
import { install as npmInstall, detectInstalled, guessPackageManager } from '../lib/Npm'
import { install as podInstall } from '../lib/Pod'
import onCancel from '../lib/OnCancel'

export default async function run (argv: string[], opts: Record<string, unknown>): Promise<void> {
const projectRoot = process.cwd()

try {
const alreadyInstalled = await detectInstalled('@bugsnag/react-native', projectRoot)
if (alreadyInstalled) {
logger.warn('@bugsnag/react-native is already installed, skipping')
} else {
logger.info('Adding @bugsnag/react-native dependency')
const { packageManager } = await prompts({
type: 'select',
name: 'packageManager',
message: 'Using yarn or npm?',
choices: [
{ title: 'npm', value: 'npm' },
{ title: 'yarn', value: 'yarn' }
],
initial: await guessPackageManager(projectRoot) === 'npm' ? 0 : 1
})
imjoehaines marked this conversation as resolved.
Show resolved Hide resolved

const { version } = await prompts({
type: 'text',
name: 'version',
message: 'If you want the latest version of @bugsnag/react-native hit enter, otherwise type the version you want',
initial: 'latest'
}, { onCancel })

await npmInstall(packageManager, '@bugsnag/react-native', version, false, projectRoot)
logger.info('Installing cocoapods')
await podInstall(projectRoot, logger)
}
} catch (e) {
logger.error(e)
}
}
34 changes: 34 additions & 0 deletions packages/react-native-cli/src/lib/Pod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { spawn } from 'child_process'
import { promises as fs } from 'fs'
import { join } from 'path'
import { Logger } from '../Logger'

export async function install (projectRoot: string, logger: Logger): Promise<void> {
try {
const iosDirList = await fs.readdir(join(projectRoot, 'ios'))
if (!iosDirList.includes('Podfile')) {
logger.warn('No Podfile found in ios directory, skipping')
return
}
} catch (e) {
if (e.code === 'ENOENT') {
logger.warn('No ios directory found in project, skipping')
return
}
throw e
bengourley marked this conversation as resolved.
Show resolved Hide resolved
}
return new Promise((resolve, reject) => {
const proc = spawn('pod', ['install'], { cwd: join(projectRoot, 'ios'), stdio: 'inherit' })

proc.on('error', err => reject(err))

proc.on('close', code => {
if (code === 0) return resolve()
reject(
new Error(
`Command exited with non-zero exit code (${code}) "pod install"`
)
)
})
})
}
115 changes: 115 additions & 0 deletions packages/react-native-cli/src/lib/__test__/Pod.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { install } from '../Pod'
import path from 'path'
import { promises as fs } from 'fs'
import { spawn, ChildProcess } from 'child_process'
import { EventEmitter } from 'events'
import logger from '../../Logger'

async function generateNotFoundError () {
try {
await jest.requireActual('fs').promises.readdir(path.join(__dirname, 'does-not-exist'))
} catch (e) {
return e
}
}

jest.mock('fs', () => {
return { promises: { readFile: jest.fn(), writeFile: jest.fn(), readdir: jest.fn() } }
})
jest.mock('child_process')
jest.mock('../../Logger')

afterEach(() => jest.resetAllMocks())

test('install(): success', async () => {
type readdir = (path: string) => Promise<string[]>
const readdirMock = fs.readdir as unknown as jest.MockedFunction<readdir>
readdirMock.mockResolvedValue(['Pods', 'MyProject', 'MyProject.xcodeproj', 'Podfile'])

const spawnMock = spawn as jest.MockedFunction<typeof spawn>
spawnMock.mockImplementation(() => {
const ee = new EventEmitter() as ChildProcess
process.nextTick(() => ee.emit('close', 0))
return ee
})

await install('/example/dir', logger)
expect(spawnMock).toHaveBeenCalledWith('pod', ['install'], { cwd: '/example/dir/ios', stdio: 'inherit' })
})

test('install(): no podfile', async () => {
type readdir = (path: string) => Promise<string[]>
const readdirMock = fs.readdir as unknown as jest.MockedFunction<readdir>
readdirMock.mockResolvedValue(['Pods', 'MyProject', 'MyProject.xcodeproj'])

const spawnMock = spawn as jest.MockedFunction<typeof spawn>
spawnMock.mockImplementation(() => {
const ee = new EventEmitter() as ChildProcess
process.nextTick(() => ee.emit('close', 0))
return ee
})

await install('/example/dir', logger)
expect(spawnMock).not.toHaveBeenCalled()
expect(logger.warn).toHaveBeenCalledWith('No Podfile found in ios directory, skipping')
})

test('install(): no ios dir', async () => {
type readdir = (path: string) => Promise<string[]>
const readdirMock = fs.readdir as unknown as jest.MockedFunction<readdir>
readdirMock.mockRejectedValue(await generateNotFoundError())

const spawnMock = spawn as jest.MockedFunction<typeof spawn>
spawnMock.mockImplementation(() => {
const ee = new EventEmitter() as ChildProcess
process.nextTick(() => ee.emit('close', 0))
return ee
})

await install('/example/dir', logger)
expect(spawnMock).not.toHaveBeenCalled()
expect(logger.warn).toHaveBeenCalledWith('No ios directory found in project, skipping')
})

test('install(): bad exit code', async () => {
type readdir = (path: string) => Promise<string[]>
const readdirMock = fs.readdir as unknown as jest.MockedFunction<readdir>
readdirMock.mockResolvedValue(['Pods', 'MyProject', 'MyProject.xcodeproj', 'Podfile'])

const spawnMock = spawn as jest.MockedFunction<typeof spawn>
spawnMock.mockImplementation(() => {
const ee = new EventEmitter() as ChildProcess
process.nextTick(() => ee.emit('close', 1))
return ee
})

await expect(install('/example/dir', logger)).rejects.toThrow('Command exited with non-zero exit code (1) "pod install"')
expect(spawnMock).toHaveBeenCalledWith('pod', ['install'], { cwd: '/example/dir/ios', stdio: 'inherit' })
})

test('install(): unknown child process error', async () => {
type readdir = (path: string) => Promise<string[]>
const readdirMock = fs.readdir as unknown as jest.MockedFunction<readdir>
readdirMock.mockResolvedValue(['Pods', 'MyProject', 'MyProject.xcodeproj', 'Podfile'])

const spawnMock = spawn as jest.MockedFunction<typeof spawn>
spawnMock.mockImplementation(() => {
const ee = new EventEmitter() as ChildProcess
process.nextTick(() => ee.emit('error', new Error('uh oh')))
return ee
})

await expect(install('/example/dir', logger)).rejects.toThrow('uh oh')
expect(spawnMock).toHaveBeenCalledWith('pod', ['install'], { cwd: '/example/dir/ios', stdio: 'inherit' })
})

test('install(): unknown error', async () => {
type readdir = (path: string) => Promise<string[]>
const readdirMock = fs.readdir as unknown as jest.MockedFunction<readdir>
readdirMock.mockRejectedValue(new Error('uh oh'))

const spawnMock = spawn as jest.MockedFunction<typeof spawn>

await expect(install('/example/dir', logger)).rejects.toThrow('uh oh')
expect(spawnMock).not.toHaveBeenCalled()
})