Skip to content

Commit

Permalink
fix: do not print generic error message for expected errors (#110)
Browse files Browse the repository at this point in the history
* fix: do not print generic error message for expected errors

* extract cycli error cancel to consts and reuse
  • Loading branch information
km1chno authored Sep 19, 2024
1 parent ded3deb commit 46e637e
Show file tree
Hide file tree
Showing 11 changed files with 75 additions and 40 deletions.
21 changes: 12 additions & 9 deletions src/commands/setup-ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import detox from '../recipes/detox'
import maestro from '../recipes/maestro'
import isGitDirty from 'is-git-dirty'
import sequentialPromiseMap from '../utils/sequentialPromiseMap'
import { CycliRecipe, CycliToolbox, ProjectContext } from '../types'
import messageFromError from '../utils/messageFromError'
import { CycliError, CycliRecipe, CycliToolbox, ProjectContext } from '../types'
import intersection from 'lodash/intersection'
import {
CYCLI_COMMAND,
HELP_FLAG,
PRESET_FLAG,
REPOSITORY_FEATURES_HELP_URL,
} from '../constants'
import { isCycliError, messageFromError } from '../utils/errors'

const SKIP_GIT_CHECK_FLAG = 'skip-git-check'

Expand Down Expand Up @@ -55,7 +55,7 @@ const getSelectedOptions = async (toolbox: CycliToolbox): Promise<string[]> => {
const validationError = messageFromError(error)

// adding context to validation error reason (used in multiselect menu hint)
throw Error(
throw CycliError(
`Cannot generate ${recipe.meta.name} workflow in your project.\nReason: ${validationError}`
)
}
Expand Down Expand Up @@ -94,7 +94,7 @@ const runReactNativeCiCli = async (toolbox: CycliToolbox) => {
toolbox.interactive.intro(' Welcome to npx setup-ci! ')

if (isGitDirty() == null) {
throw Error('This is not a git repository.')
throw CycliError('This is not a git repository.')
}

if (isGitDirty()) {
Expand All @@ -104,7 +104,7 @@ const runReactNativeCiCli = async (toolbox: CycliToolbox) => {
)
} else {
if (toolbox.options.isPreset()) {
throw Error(
throw CycliError(
`You have to commit your changes before running with preset or use --${SKIP_GIT_CHECK_FLAG}.`
)
}
Expand Down Expand Up @@ -181,11 +181,14 @@ const run = async (toolbox: GluegunToolbox) => {
try {
await runReactNativeCiCli(toolbox as CycliToolbox)
} catch (error: unknown) {
const errMessage = messageFromError(error)
toolbox.interactive.vspace()
toolbox.interactive.error(
`Failed to execute ${CYCLI_COMMAND} with following error:\n${errMessage}`
)
let errMessage = messageFromError(error)

if (!isCycliError(error)) {
errMessage = `${CYCLI_COMMAND} failed with unexpected error:\n${errMessage}`
}

toolbox.interactive.error(errMessage)
} finally {
process.exit()
}
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { print } from 'gluegun'

export const CYCLI_COMMAND = 'setup-ci'

export const CYCLI_ERROR_NAME = 'CycliError'

export const COLORS = {
bold: print.colors.bold,
cyan: print.colors.cyan,
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { COLORS, CYCLI_COMMAND, REPOSITORY_ISSUES_URL } from '../constants'
import { CycliToolbox, ProjectContext } from '../types'
import messageFromError from '../utils/messageFromError'
import { join, sep } from 'path'
import { messageFromError } from '../utils/errors'

module.exports = (toolbox: CycliToolbox) => {
const { packageManager } = toolbox
Expand Down
11 changes: 7 additions & 4 deletions src/extensions/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ import {
S_UR,
S_VBAR,
} from '../constants'
import { CycliToolbox, MessageColor } from '../types'
import { CycliError, CycliToolbox, MessageColor } from '../types'

const CYCLI_ERROR_CANCEL = CycliError(
'The script execution has been canceled by the user.'
)
const DEFAULT_HEADER_WIDTH = 80

interface Spinner {
Expand Down Expand Up @@ -123,7 +126,7 @@ module.exports = (toolbox: CycliToolbox) => {
}).prompt()

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

Expand Down Expand Up @@ -213,7 +216,7 @@ module.exports = (toolbox: CycliToolbox) => {
}).prompt()

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

return Boolean(confirmed)
Expand Down Expand Up @@ -365,7 +368,7 @@ module.exports = (toolbox: CycliToolbox) => {
const selected = await multiselectPromise

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

return selected as string[]
Expand Down
10 changes: 8 additions & 2 deletions src/extensions/projectConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { AppJson, CycliToolbox, PackageJson, ProjectContext } from '../types'
import {
AppJson,
CycliError,
CycliToolbox,
PackageJson,
ProjectContext,
} from '../types'
import { join } from 'path'

const APP_JSON_FILES = ['app.json', 'app.config.json']
Expand All @@ -9,7 +15,7 @@ module.exports = (toolbox: CycliToolbox) => {

const packageJson = (): PackageJson => {
if (!filesystem.exists('package.json')) {
throw Error(
throw CycliError(
'No package.json found in current directory. Are you sure you are in a project directory?'
)
}
Expand Down
11 changes: 8 additions & 3 deletions src/extensions/projectContext.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { CycliToolbox, PackageManager, ProjectContext } from '../types'
import {
CycliError,
CycliToolbox,
PackageManager,
ProjectContext,
} from '../types'
import { LOCK_FILE_TO_MANAGER } from '../constants'
import { lookItUpSync } from 'look-it-up'
import { basename, join, relative } from 'path'
Expand All @@ -15,7 +20,7 @@ module.exports = (toolbox: CycliToolbox) => {
) || []

if (lockFiles.length == 0) {
throw Error(
throw CycliError(
[
'No lock file found in repository root directory. Are you sure you are in a project directory?',
'Make sure you generated lock file by installing project dependencies.',
Expand Down Expand Up @@ -46,7 +51,7 @@ module.exports = (toolbox: CycliToolbox) => {
const packageJson = toolbox.projectConfig.packageJson()

if (packageJson.workspaces) {
throw Error(
throw CycliError(
'The current directory is workspace root directory. Please run the script again from selected package root directory.'
)
}
Expand Down
4 changes: 2 additions & 2 deletions src/recipes/build.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CycliToolbox, Platform, ProjectContext } from '../types'
import { CycliError, CycliToolbox, Platform, ProjectContext } from '../types'
import { join } from 'path'

const BuildMode = {
Expand Down Expand Up @@ -121,7 +121,7 @@ export const createBuildWorkflows = async (
?.replace('.xcworkspace', '')

if (!iOSAppName) {
throw Error(
throw CycliError(
'Failed to obtain iOS app name. Perhaps your ios/ directory is missing .xcworkspace file.'
)
}
Expand Down
4 changes: 2 additions & 2 deletions src/recipes/eas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { REPOSITORY_SECRETS_HELP_URL } from '../constants'
import { CycliRecipe, CycliToolbox, ProjectContext } from '../types'
import { CycliError, CycliRecipe, CycliToolbox, ProjectContext } from '../types'
import { join } from 'path'
import { recursiveAssign } from '../utils/recursiveAssign'

Expand Down Expand Up @@ -126,7 +126,7 @@ const execute = async (

const validate = (toolbox: CycliToolbox): void => {
if (!toolbox.projectConfig.isExpo()) {
throw Error('only supported in expo projects')
throw CycliError('only supported in expo projects')
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import { ProjectContextExtension } from './extensions/projectContext'
import { ScriptsExtension } from './extensions/scripts'
import { WorkflowsExtension } from './extensions/workflows'
import { ProjectConfigExtension } from './extensions/projectConfig'
import { COLORS, LOCK_FILE_TO_MANAGER } from './constants'
import { COLORS, CYCLI_ERROR_NAME, LOCK_FILE_TO_MANAGER } from './constants'
import { DiffExtension } from './extensions/diff'
import { OptionsExtension } from './extensions/options'
import { FurtherActionsExtension } from './extensions/furtherActions'
import { ExpoExtension } from './extensions/expo'
import { PrettierExtension } from './extensions/prettier'

export const CycliError = (message: string): Error => {
const error = new Error(message)
error.name = CYCLI_ERROR_NAME
return error
}

export type MessageColor = keyof typeof COLORS

export interface PackageJson {
Expand Down
26 changes: 26 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CYCLI_ERROR_NAME } from '../constants'

export const isError = (e: unknown): boolean => {
return Boolean(
typeof e === 'object' &&
e != null &&
'message' in e &&
typeof e.message === 'string' &&
'name' in e &&
typeof e.name === 'string'
)
}

export const isCycliError = (e: unknown): boolean => {
return isError(e) && (e as Error).name === CYCLI_ERROR_NAME
}

export const messageFromError = (e: unknown): string => {
if (isError(e)) {
return (e as Error).message
}

// This should normally never be reached, but since you can throw anything
// and errors have unknown type, we have to handle every case.
return JSON.stringify(e)
}
16 changes: 0 additions & 16 deletions src/utils/messageFromError.ts

This file was deleted.

0 comments on commit 46e637e

Please sign in to comment.