Skip to content

Commit

Permalink
Merge pull request #1454 from raineorshine/unhandled-exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
raineorshine committed Sep 17, 2024
2 parents 2715643 + 2a6df8c commit b74fb5d
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 20 deletions.
16 changes: 15 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
})

/**
Expand Down Expand Up @@ -280,6 +286,14 @@ export async function run(
): Promise<PackageFile | Index<VersionSpec> | 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)
Expand Down
8 changes: 5 additions & 3 deletions src/lib/filterAndReject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion src/lib/filterObject.ts
Original file line number Diff line number Diff line change
@@ -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 = <T>(obj: Index<T>, predicate: (key: string, value: T) => boolean) =>
keyValueBy(obj, (key, value) => (predicate(key, value) ? { [key]: value } : null))

Expand Down
24 changes: 15 additions & 9 deletions src/lib/getCurrentDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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<VersionSpec> = {}
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
}
Expand Down
16 changes: 12 additions & 4 deletions src/lib/getInstalledPackages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Index } from '../types/IndexType'
import { Options } from '../types/Options'
import { Version } from '../types/Version'
import { VersionSpec } from '../types/VersionSpec'
Expand Down Expand Up @@ -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<VersionSpec> = {}
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
10 changes: 9 additions & 1 deletion test/filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,22 @@ 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: [],
})) as Index<string>
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', () => {
Expand Down
3 changes: 2 additions & 1 deletion test/interactive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
)
Expand Down

0 comments on commit b74fb5d

Please sign in to comment.