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..3f6288df --- /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 +}