Skip to content

Commit

Permalink
Refactor .ncurc loading (#1457)
Browse files Browse the repository at this point in the history
  • Loading branch information
jftanner authored Sep 22, 2024
1 parent 8a5967a commit 2258ef7
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ async function runUpgrades(options: Options, timeout?: NodeJS.Timeout): Promise<
const packages = await previousPromise
// copy object to prevent share .ncurc options between different packageFile, to prevent unpredictable behavior
const rcResult = await getNcuRc({ packageFile: packageInfo.filepath, options })
let rcConfig = rcResult && rcResult.config ? rcResult.config : {}
let rcConfig = rcResult.config
if (options.mergeConfig && Object.keys(rcConfig).length) {
// Merge config options.
rcConfig = mergeOptions(options, rcConfig)
Expand Down
48 changes: 22 additions & 26 deletions src/lib/getNcuRc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'path'
import { rcFile } from 'rc-config-loader'
import { cliOptionsMap } from '../cli-options'
import { Options } from '../types/Options'
import { RcOptions } from '../types/RcOptions'
import programError from './programError'

/** Loads the .ncurc config file. */
Expand All @@ -23,52 +24,47 @@ async function getNcuRc({
const { default: chalkDefault, Chalk } = await import('chalk')
const chalk = options?.color ? new Chalk({ level: 1 }) : chalkDefault

const rawResult = rcFile('ncurc', {
const rawResult = rcFile<RcOptions>('ncurc', {
configFileName: configFileName || '.ncurc',
defaultExtension: ['.json', '.yml', '.js'],
cwd: configFilePath || (global ? os.homedir() : packageFile ? path.dirname(packageFile) : undefined),
})

if (configFileName && !rawResult?.filePath) {
// ensure a file was found if expected
const filePath = rawResult?.filePath
if (configFileName && !filePath) {
programError(options, `Config file ${configFileName} not found in ${configFilePath || process.cwd()}`)
}

// @ts-expect-error -- rawResult.config is not typed thus TypeScript does not know that it has a $schema property
const { $schema: _, ...rawConfigWithoutSchema } = rawResult?.config || {}

const result = {
filePath: rawResult?.filePath,
// Prevent the cli tool from choking because of an unknown option "$schema"
config: rawConfigWithoutSchema,
}
// convert the config to valid options by removing $schema and parsing format
const { $schema: _, ...rawConfig } = rawResult?.config || {}
const config: Options = rawConfig
if (typeof config.format === 'string') config.format = cliOptionsMap.format.parse!(config.format)

// validate arguments here to provide a better error message
const unknownOptions = Object.keys(result?.config || {}).filter(arg => !cliOptionsMap[arg])
const unknownOptions = Object.keys(config).filter(arg => !cliOptionsMap[arg])
if (unknownOptions.length > 0) {
console.error(
chalk.red(`Unknown option${unknownOptions.length === 1 ? '' : 's'} found in config file:`),
chalk.gray(unknownOptions.join(', ')),
)
console.info('Using config file ' + result!.filePath)
console.info('Using config file ' + filePath)
console.info(`You can change the config file path with ${chalk.blue('--configFilePath')}`)
}

// flatten config object into command line arguments to be read by commander
const args = result
? Object.entries(result.config).flatMap(([name, value]): any[] =>
// if a boolean option is true, include only the nullary option --${name}
// an option is considered boolean if its type is explicitly set to boolean, or if it is has a proper Javascript boolean value
value === true || (cliOptionsMap[name]?.type === 'boolean' && value)
? [`--${name}`]
: // if a boolean option is false, exclude it
value === false || (cliOptionsMap[name]?.type === 'boolean' && !value)
? []
: // otherwise render as a 2-tuple
[`--${name}`, value],
)
: []
const args = Object.entries(config).flatMap(([name, value]): any[] => {
// render boolean options as a single parameter
// an option is considered boolean if its type is explicitly set to boolean, or if it is has a proper Javascript boolean value
if (typeof value === 'boolean' || cliOptionsMap[name]?.type === 'boolean') {
// if the boolean option is true, include only the nullary option --${name}, otherwise exclude it
return value ? [`--${name}`] : []
}
// otherwise render as a 2-tuple with name and value
return [`--${name}`, value]
})

return result ? { ...result, args } : null
return { filePath, args, config }
}

export default getNcuRc
10 changes: 10 additions & 0 deletions src/types/RcOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { RunOptions } from './RunOptions'

/** Options that would make no sense in a .ncurc file */
type Nonsensical = 'configFileName' | 'configFilePath' | 'cwd' | 'packageData' | 'stdin'

/** Expected options that might be found in an .ncurc file. Since the config is external, this cannot be guaranteed */
export type RcOptions = Omit<RunOptions, Nonsensical> & {
$schema?: string
format?: string | string[] // Format is often set as a string, but needs to be an array
}

0 comments on commit 2258ef7

Please sign in to comment.