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

feat: support detox in bare react native projects #66

Merged
merged 6 commits into from
Aug 26, 2024
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
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ The following are **feature flags** that can be used with `--preset` flag (they
</tr>
<tr>
<td style="vertical-align: middle;">--detox</td>
<td style="vertical-align: middle;">Generate workflow to run Detox e2e tests on every PR (Expo projects only)</td>
<td style="vertical-align: middle;">Generate workflow to run Detox e2e tests on every PR</td>
</tr>
</table>

Expand Down
15 changes: 2 additions & 13 deletions src/commands/react-native-ci-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ const runReactNativeCiCli = async (toolbox: CycliToolbox) => {
[
`It is advised to commit all your changes before running ${COMMAND}.`,
'Running the script with uncommitted changes may have destructive consequences.',
'Do you want to proceed anyway?',
'Do you want to proceed anyway?\n',
].join('\n'),
'warning'
{ type: 'warning' }
)

if (!proceed) {
Expand All @@ -76,17 +76,6 @@ const runReactNativeCiCli = async (toolbox: CycliToolbox) => {
const easUpdateExecutor = await easUpdate.run(toolbox, context)
const detoxExecutor = await detox.run(toolbox, context)

// Detox and EAS Update recipes are currently supported only for Expo projects
if (
!toolbox.projectConfig.isExpo() &&
(context.selectedOptions.includes(detox.meta.flag) ||
context.selectedOptions.includes(easUpdate.meta.flag))
) {
throw Error(
'Detox and EAS Update workflows are supported only for Expo projects.'
)
}

const executors = [
lintExecutor,
jestExecutor,
Expand Down
3 changes: 2 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { print } from 'gluegun'
export const COLORS = {
bold: print.colors.bold,
cyan: print.colors.cyan,
magenta: print.colors.magenta,
green: print.colors.green,
yellow: print.colors.yellow,
gray: print.colors.gray,
Expand All @@ -16,7 +17,7 @@ export const COLORS = {
export const S_STEP_WARNING = COLORS.yellow('▲')
export const S_STEP_ERROR = COLORS.red('■')
export const S_STEP_SUCCESS = COLORS.green('◇')
export const S_SELECT = COLORS.cyan('◆')
export const S_CONFIRM = COLORS.magenta('◆')
export const S_ACTION = COLORS.cyan('▼')
export const S_ACTION_BULLET = COLORS.cyan('►')
export const S_BAR = '│'
Expand Down
99 changes: 89 additions & 10 deletions src/extensions/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ import {
log as clackLog,
} from '@clack/prompts'
import { CycliToolbox, MessageColor } from '../types'
import { ConfirmPrompt } from '@clack/core'
import { ConfirmPrompt, SelectPrompt } from '@clack/core'
import { spawn } from 'child_process'
import {
COLORS,
S_ACTION,
S_BAR,
S_BAR_END,
S_CONFIRM,
S_DL,
S_DR,
S_RADIO_ACTIVE,
S_RADIO_INACTIVE,
S_SELECT,
S_STEP_ERROR,
S_STEP_SUCCESS,
S_STEP_WARNING,
Expand All @@ -32,19 +33,90 @@ interface Spinner {
const DEFAULT_HEADER_WIDTH = 80

module.exports = (toolbox: CycliToolbox) => {
const { bold, cyan, yellow, gray, inverse, dim, strikethrough } = COLORS
const { bold, cyan, magenta, yellow, gray, inverse, dim, strikethrough } =
COLORS

const withNewlinePrefix = (message: string, prefix: string): string =>
message.split('\n').join(`\n${prefix} `)

const actionPrompt = async (message: string): Promise<void> => {
const opt = (
option: { value: string; label?: string; hint?: string },
state: 'inactive' | 'active' | 'selected' | 'cancelled'
) => {
const label = option.label ?? String(option.value)
switch (state) {
case 'selected':
return `${dim(label)}`
case 'active':
return `${cyan(S_RADIO_ACTIVE)} ${label} ${
option.hint ? dim(`(${option.hint})`) : ''
}`
case 'cancelled':
return `${strikethrough(dim(label))}`
default:
return `${dim(S_RADIO_INACTIVE)} ${dim(label)}`
}
}

const title = `${gray(S_BAR)}\n${S_ACTION} ${cyan(
withNewlinePrefix(message, S_BAR)
)}\n`

const titleSubmitted = `${gray(S_BAR)}\n${S_ACTION} ${withNewlinePrefix(
message
.split('\n')
.map((line) => cyan(line))
.join('\n'),
dim(S_BAR)
)}\n`

const confirmed = await new SelectPrompt({
options: [
{
value: 'continue',
label: 'Press enter to continue...',
},
],
initialValue: 'continue',
render() {
switch (this.state) {
case 'submit':
return `${titleSubmitted}${gray(S_BAR)} ${opt(
this.options[0],
'selected'
)}`
case 'cancel':
return `${titleSubmitted}${gray(S_BAR)} ${opt(
this.options[0],
'cancelled'
)}\n${gray(S_BAR)}`
default: {
return `${title}${cyan(S_BAR)} ${opt(
this.options[0],
'active'
)}\n${cyan(S_BAR_END)}`
}
}
},
}).prompt()

if (isCancel(confirmed)) {
throw Error('The script execution has been canceled by the user.')
}
}

const confirm = async (
message: string,
type: 'normal' | 'warning' = 'normal'
{ type }: { type: 'normal' | 'warning' }
): Promise<boolean> => {
const active = 'Yes'
const inactive = 'No'

const title = () => {
switch (type) {
case 'normal':
return `${gray(S_BAR)}\n${S_SELECT} ${message
return `${gray(S_BAR)}\n${S_CONFIRM} ${message
.split('\n')
.join(`\n${S_BAR} `)}\n`
case 'warning':
Expand All @@ -56,15 +128,17 @@ module.exports = (toolbox: CycliToolbox) => {

const titleSubmitted = () => {
switch (type) {
case 'normal':
return `${gray(S_BAR)} \n${S_SELECT} ${message
case 'normal': {
return `${gray(S_BAR)} \n${S_CONFIRM} ${message
.split('\n')
.join(`\n${gray(S_BAR)} `)}\n`
case 'warning':
}
case 'warning': {
return `${gray(S_BAR)} \n${S_STEP_WARNING} ${message
.split('\n')
.map((line) => yellow(line))
.join(`\n${gray(S_BAR)} `)}\n`
}
}
}

Expand All @@ -82,7 +156,7 @@ module.exports = (toolbox: CycliToolbox) => {
const typeColor = (msg: string): string => {
switch (type) {
case 'normal':
return cyan(msg)
return magenta(msg)
case 'warning':
return yellow(msg)
}
Expand Down Expand Up @@ -246,6 +320,7 @@ module.exports = (toolbox: CycliToolbox) => {
}

toolbox.interactive = {
actionPrompt,
confirm,
surveyStep,
surveyWarning,
Expand All @@ -266,7 +341,11 @@ module.exports = (toolbox: CycliToolbox) => {

export interface InteractiveExtension {
interactive: {
confirm: (message: string, type?: 'normal' | 'warning') => Promise<boolean>
actionPrompt: (message: string) => Promise<void>
confirm: (
message: string,
options: { type: 'normal' | 'warning' }
) => Promise<boolean>
surveyStep: (message: string) => void
surveyWarning: (message: string) => void
info: (message: string, color?: MessageColor) => void
Expand Down
98 changes: 53 additions & 45 deletions src/recipes/build-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,49 @@ import { join } from 'path'

const createReleaseBuildWorkflowAndroid = async (
toolbox: CycliToolbox,
context: ProjectContext
context: ProjectContext,
{ expo }: { expo: boolean }
) => {
await toolbox.scripts.add(
'build:release:android',
[
`npx expo prebuild --${context.packageManager}`,
'cd android',
'./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
].join(' && ')
)
let script =
'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release'

if (expo) {
script = `npx expo prebuild --${context.packageManager} && ${script}`
}

await toolbox.scripts.add('build:release:android', script)

await toolbox.workflows.generate(
join('build-release', 'build-release-android.ejf'),
context
)

toolbox.interactive.step('Created Android release build workflow for Expo.')
toolbox.interactive.step('Created Android release build workflow.')
}

const createReleaseBuildWorkflowIOs = async (
toolbox: CycliToolbox,
context: ProjectContext
context: ProjectContext,
{ iOSAppName, expo }: { iOSAppName: string; expo: boolean }
) => {
const iOSAppName = toolbox.filesystem
.list('ios')
?.find((file) => file.endsWith('.xcworkspace'))
?.replace('.xcworkspace', '')
let script = [
'xcodebuild ONLY_ACTIVE_ARCH=YES',
`-workspace ios/${iOSAppName}.xcworkspace`,
'-UseNewBuildSystem=YES',
`-scheme ${iOSAppName}`,
'-configuration Release',
'-sdk iphonesimulator',
'-derivedDataPath ios/build',
'-quiet',
].join(' ')

if (!iOSAppName) {
throw Error(
[
'Failed to obtain iOS app name. Is there a ios/ directory without .xcworkspace file in your project?',
'If so, try running "npx expo prebuild --clean" manually and try again.',
].join('\n')
)
if (expo) {
script = `npx expo prebuild --${context.packageManager} && ${script}`
} else {
script = `cd ios && pod install && cd .. && ${script}`
}

await toolbox.scripts.add(
'build:release:ios',
[
`npx expo prebuild --${context.packageManager} &&`,
'xcodebuild ONLY_ACTIVE_ARCH=YES',
`-workspace ios/${iOSAppName}.xcworkspace`,
'-UseNewBuildSystem=YES',
`-scheme ${iOSAppName}`,
'-configuration Release',
'-sdk iphonesimulator',
'-derivedDataPath ios/build',
'-quiet',
].join(' ')
)
await toolbox.scripts.add('build:release:ios', script)

await toolbox.workflows.generate(
join('build-release', 'build-release-ios.ejf'),
Expand All @@ -63,17 +55,37 @@ const createReleaseBuildWorkflowIOs = async (
}
)

toolbox.interactive.step('Created iOS release build workflow for Expo.')
toolbox.interactive.step('Created iOS release build workflow.')
}

export const createReleaseBuildWorkflowsForExpo = async (
export const createReleaseBuildWorkflows = async (
toolbox: CycliToolbox,
context: ProjectContext,
platforms: Platform[]
{ platforms, expo }: { platforms: Platform[]; expo: boolean }
): Promise<void> => {
const existsAndroidDir = toolbox.filesystem.exists('android')
const existsIOsDir = toolbox.filesystem.exists('ios')

if (expo) {
toolbox.print.info('⚙️ Running expo prebuild to setup app.json properly.')
await toolbox.system.spawn(
`npx expo prebuild --${context.packageManager}`,
{
stdio: 'inherit',
}
)
}

const iOSAppName = toolbox.filesystem
.list('ios')
?.find((file) => file.endsWith('.xcworkspace'))
?.replace('.xcworkspace', '')

if (!iOSAppName) {
throw Error(
'Failed to obtain iOS app name. Perhaps your ios/ directory is missing .xcworkspace file.'
)
}
toolbox.print.info('⚙️ Running expo prebuild to setup app.json properly.')
await toolbox.interactive.spawnSubprocess(
'Expo prebuild',
Expand All @@ -82,17 +94,13 @@ export const createReleaseBuildWorkflowsForExpo = async (
)

if (platforms.includes('android')) {
await createReleaseBuildWorkflowAndroid(toolbox, context)
await createReleaseBuildWorkflowAndroid(toolbox, context, { expo })
}

if (platforms.includes('ios')) {
await createReleaseBuildWorkflowIOs(toolbox, context)
await createReleaseBuildWorkflowIOs(toolbox, context, { iOSAppName, expo })
}

const spinner = toolbox.print.spin('🧹 Cleaning up expo prebuild.')

if (!existsAndroidDir) toolbox.filesystem.remove('android')
if (!existsIOsDir) toolbox.filesystem.remove('ios')

spinner.stop()
}
Loading