From 70377fe40f3f79cad8c1d95d62f87b43a5f8edf1 Mon Sep 17 00:00:00 2001 From: James Tanner-McLeod Date: Fri, 20 Sep 2024 22:11:34 -0400 Subject: [PATCH] Refactor .ncurc loading (Fixes #1452) --- src/index.ts | 2 +- src/lib/getNcuRc.ts | 48 +++++++++++++++++++----------------------- src/types/RcOptions.ts | 10 +++++++++ 3 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 src/types/RcOptions.ts diff --git a/src/index.ts b/src/index.ts index 4bdf2f52..bf7a7620 100755 --- a/src/index.ts +++ b/src/index.ts @@ -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) diff --git a/src/lib/getNcuRc.ts b/src/lib/getNcuRc.ts index ac096dd1..7ae6ce9a 100644 --- a/src/lib/getNcuRc.ts +++ b/src/lib/getNcuRc.ts @@ -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. */ @@ -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('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: _, format, ...rawConfig } = rawResult?.config || {} + const config: Options = rawConfig + if (typeof format === 'string') config.format = format.split(',') // 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 diff --git a/src/types/RcOptions.ts b/src/types/RcOptions.ts new file mode 100644 index 00000000..4c45c7d1 --- /dev/null +++ b/src/types/RcOptions.ts @@ -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 & { + $schema?: string + format?: string | string[] // Format is often set as a string, but needs to be an array +}