diff --git a/src/index.ts b/src/index.ts index d4130b9d..4bdf2f52 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import path from 'path' import prompts from 'prompts-ncu' import spawn from 'spawn-please' +import pkg from '../package.json' import { cliOptionsMap } from './cli-options' import { cacheClear } from './lib/cache' import chalk, { chalkInit } from './lib/chalk' @@ -28,12 +29,17 @@ if (process.env.INJECT_PROMPTS) { prompts.inject(JSON.parse(process.env.INJECT_PROMPTS)) } -// Exit with non-zero error code when there is an unhandled promise rejection. +/** Tracks the (first) unhandled rejection so the process can exit with an error code at the end. This allows other errors to be logged before the process exits. */ +let unhandledRejectionError = false + // Use `node --trace-uncaught ...` to show where the exception was thrown. // See: https://nodejs.org/api/process.html#event-unhandledrejection process.on('unhandledRejection', (reason: string | Error) => { // do not rethrow, as there may be other errors to print out console.error(reason) + + // ensure the process exits with a non-zero code at the end + unhandledRejectionError = true }) /** @@ -280,6 +286,14 @@ export async function run( ): Promise | void> { const options = await initOptions(runOptions, { cli }) + // ensure that the process exits with an error code if there was an unhandled rejection + const bugsUrl = pkg.bugs.url + process.on('exit', () => { + if (unhandledRejectionError) { + programError(options, `Unhandled Rejection! This is a bug and should be reported: ${bugsUrl}`) + } + }) + // chalk may already have been initialized in cli.ts, but when imported as a module // chalkInit is idempotent await chalkInit(options.color) diff --git a/src/lib/filterAndReject.ts b/src/lib/filterAndReject.ts index 65fb3542..b785287b 100644 --- a/src/lib/filterAndReject.ts +++ b/src/lib/filterAndReject.ts @@ -7,8 +7,9 @@ import { Maybe } from '../types/Maybe' import { VersionSpec } from '../types/VersionSpec' /** - * Creates a filter function from a given filter string. Supports - * strings, wildcards, comma-or-space-delimited lists, and regexes. + * Creates a filter function from a given filter string. + * Supports strings, wildcards, comma-or-space-delimited lists, and regexes. + * The filter function *may* throw an exception if the filter pattern is invalid. * * @param [filterPattern] * @returns @@ -65,8 +66,9 @@ function composeFilter(filterPattern: FilterPattern): (name: string, versionSpec // limit the arity to 1 to avoid passing the value return predicate } + /** - * Composes a filter function from filter, reject, filterVersion, and rejectVersion patterns. + * Composes a filter function from filter, reject, filterVersion, and rejectVersion patterns. The filter function *may* throw an exception if the filter pattern is invalid. * * @param [filter] * @param [reject] diff --git a/src/lib/filterObject.ts b/src/lib/filterObject.ts index 9a04e754..fb93f249 100644 --- a/src/lib/filterObject.ts +++ b/src/lib/filterObject.ts @@ -1,7 +1,7 @@ import { Index } from '../types/IndexType' import keyValueBy from './keyValueBy' -/** Filters an object by a predicate. */ +/** Filters an object by a predicate. Does not catch exceptions thrown by the predicate. */ const filterObject = (obj: Index, predicate: (key: string, value: T) => boolean) => keyValueBy(obj, (key, value) => (predicate(key, value) ? { [key]: value } : null)) diff --git a/src/lib/getCurrentDependencies.ts b/src/lib/getCurrentDependencies.ts index 5b57892f..3c3dd44d 100644 --- a/src/lib/getCurrentDependencies.ts +++ b/src/lib/getCurrentDependencies.ts @@ -6,6 +6,7 @@ import { VersionSpec } from '../types/VersionSpec' import filterAndReject from './filterAndReject' import filterObject from './filterObject' import { keyValueBy } from './keyValueBy' +import programError from './programError' import resolveDepSections from './resolveDepSections' /** Returns true if spec1 is greater than spec2, ignoring invalid version ranges. */ @@ -51,15 +52,20 @@ function getCurrentDependencies(pkgData: PackageFile = {}, options: Options = {} // filter & reject dependencies and versions const workspacePackageMap = keyValueBy(options.workspacePackages || []) - const filteredDependencies = filterObject( - filterObject(allDependencies, name => !workspacePackageMap[name]), - filterAndReject( - options.filter || null, - options.reject || null, - options.filterVersion || null, - options.rejectVersion || null, - ), - ) + let filteredDependencies: Index = {} + try { + filteredDependencies = filterObject( + filterObject(allDependencies, name => !workspacePackageMap[name]), + filterAndReject( + options.filter || null, + options.reject || null, + options.filterVersion || null, + options.rejectVersion || null, + ), + ) + } catch (err: any) { + programError(options, 'Invalid filter: ' + err.message || err) + } return filteredDependencies } diff --git a/src/lib/getInstalledPackages.ts b/src/lib/getInstalledPackages.ts index e859c961..95e84f0b 100644 --- a/src/lib/getInstalledPackages.ts +++ b/src/lib/getInstalledPackages.ts @@ -1,3 +1,4 @@ +import { Index } from '../types/IndexType' import { Options } from '../types/Options' import { Version } from '../types/Version' import { VersionSpec } from '../types/VersionSpec' @@ -29,10 +30,17 @@ async function getInstalledPackages(options: Options = {}) { // filter out undefined packages or those with a wildcard const filterFunction = filterAndReject(options.filter, options.reject, options.filterVersion, options.rejectVersion) - return filterObject( - packages, - (dep: VersionSpec, version: Version) => !!version && !isWildPart(version) && filterFunction(dep, version), - ) + let filteredPackages: Index = {} + try { + filteredPackages = filterObject( + packages, + (dep: VersionSpec, version: Version) => !!version && !isWildPart(version) && filterFunction(dep, version), + ) + } catch (err: any) { + programError(options, 'Invalid filter: ' + err.message || err) + } + + return filteredPackages } export default getInstalledPackages diff --git a/test/filter.test.ts b/test/filter.test.ts index 88995284..5dd674f0 100644 --- a/test/filter.test.ts +++ b/test/filter.test.ts @@ -191,7 +191,7 @@ describe('filter', () => { upgraded.should.have.property('fp-and-or') }) - it('trim and ignore empty filter', async () => { + it('trim and ignore empty array', async () => { const upgraded = (await ncu({ packageData: await fs.readFile(path.join(__dirname, 'test-data/ncu/package2.json'), 'utf-8'), filter: [], @@ -199,6 +199,14 @@ describe('filter', () => { upgraded.should.have.property('lodash.map') upgraded.should.have.property('lodash.filter') }) + + it('empty string is invalid', async () => { + const promise = ncu({ + packageData: await fs.readFile(path.join(__dirname, 'test-data/ncu/package2.json'), 'utf-8'), + filter: ',test', + }) + promise.should.eventually.be.rejectedWith('Invalid filter: Expected pattern to be a non-empty string') + }) }) describe('cli', () => { diff --git a/test/interactive.test.ts b/test/interactive.test.ts index f538796e..0b44e029 100644 --- a/test/interactive.test.ts +++ b/test/interactive.test.ts @@ -17,7 +17,8 @@ describe('--interactive', () => { 'ncu-test-v2': '2.0.0', 'ncu-test-tag': '1.1.0', 'ncu-test-return-version': '2.0.0', - 'modern-diacritics': '99.9.9', + // this must be a real version for --format repo to work + 'modern-diacritics': '2.0.0', }, { spawn: true }, )