From adb148fe0b9cbdab10d29ded784788ac319ab981 Mon Sep 17 00:00:00 2001 From: Ryan Zimmerman Date: Fri, 12 Sep 2025 13:02:48 -0400 Subject: [PATCH 1/2] Fix UnhandledPromiseRejectionWarning in copy Fixes #1056 --- lib/copy/copy.js | 31 ++++++++++++------------------- lib/util/async.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 lib/util/async.js diff --git a/lib/copy/copy.js b/lib/copy/copy.js index 47fd9836..40652f47 100644 --- a/lib/copy/copy.js +++ b/lib/copy/copy.js @@ -6,6 +6,7 @@ const { mkdirs } = require('../mkdirs') const { pathExists } = require('../path-exists') const { utimesMillis } = require('../util/utimes') const stat = require('../util/stat') +const { asyncIteratorConcurrentProcess } = require('../util/async') async function copy (src, dest, opts = {}) { if (typeof opts === 'function') { @@ -113,28 +114,20 @@ async function onDir (srcStat, destStat, src, dest, opts) { await fs.mkdir(dest) } - const promises = [] - - // loop through the files in the current directory to copy everything - for await (const item of await fs.opendir(src)) { + // iterate through the files in the current directory to copy everything + await asyncIteratorConcurrentProcess(await fs.opendir(src), async (item) => { const srcItem = path.join(src, item.name) const destItem = path.join(dest, item.name) - promises.push( - runFilter(srcItem, destItem, opts).then(include => { - if (include) { - // only copy the item if it matches the filter function - return stat.checkPaths(srcItem, destItem, 'copy', opts).then(({ destStat }) => { - // If the item is a copyable file, `getStatsAndPerformCopy` will copy it - // If the item is a directory, `getStatsAndPerformCopy` will call `onDir` recursively - return getStatsAndPerformCopy(destStat, srcItem, destItem, opts) - }) - } - }) - ) - } - - await Promise.all(promises) + const include = await runFilter(srcItem, destItem, opts) + // only copy the item if it matches the filter function + if (include) { + const { destStat } = await stat.checkPaths(srcItem, destItem, 'copy', opts) + // If the item is a copyable file, `getStatsAndPerformCopy` will copy it + // If the item is a directory, `getStatsAndPerformCopy` will call `onDir` recursively + await getStatsAndPerformCopy(destStat, srcItem, destItem, opts) + } + }) if (!destStat) { await fs.chmod(dest, srcStat.mode) diff --git a/lib/util/async.js b/lib/util/async.js new file mode 100644 index 00000000..181c27e9 --- /dev/null +++ b/lib/util/async.js @@ -0,0 +1,29 @@ +'use strict' + +// https://github.com/jprichardson/node-fs-extra/issues/1056 +// Performing parallel operations on each item of an async iterator is +// surprisingly hard; you need to have handlers in place to avoid getting an +// UnhandledPromiseRejectionWarning. +// NOTE: This function does not presently handle return values, only errors +async function asyncIteratorConcurrentProcess (iterator, fn) { + const promises = [] + for await (const item of iterator) { + promises.push( + fn(item).then( + () => null, + (err) => err ?? new Error('unknown error') + ) + ) + } + await Promise.all( + promises.map((promise) => + promise.then((possibleErr) => { + if (possibleErr != null) throw possibleErr + }) + ) + ) +} + +module.exports = { + asyncIteratorConcurrentProcess +} From d9c72646ea7b6efd25e6542b1697d9ec8428e40d Mon Sep 17 00:00:00 2001 From: Ryan Zimmerman Date: Mon, 15 Sep 2025 11:07:35 -0400 Subject: [PATCH 2/2] != -> !== --- lib/util/async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util/async.js b/lib/util/async.js index 181c27e9..3f6288df 100644 --- a/lib/util/async.js +++ b/lib/util/async.js @@ -18,7 +18,7 @@ async function asyncIteratorConcurrentProcess (iterator, fn) { await Promise.all( promises.map((promise) => promise.then((possibleErr) => { - if (possibleErr != null) throw possibleErr + if (possibleErr !== null) throw possibleErr }) ) )