Repository: jprichardson/node-fs-extra
Pull request #1021 (2023-10-19 15:37:19)

Refactor `copy` API in async/await
The PR is the first part of proposal #1020:

- The PR only migrates the `copy` API. Other APIs will be migrated in the following PRs
- The PR adds Promise-based implementations for some utility functions. The callback-based implementations are retained, but they will be removed once the rest of the library has migrated to Promise-based implementations.

SukkaW: @RyanZim The PR is ready to be reviewed now. `npm run lint` passes, and all test cases pass as well, check CI run here: https://github.com/SukkaW/node-fs-extra/actions/runs/6582727479
SukkaW: @RyanZim I've addressed all the code review suggestions. I've also universalized the related utility functions to reduce the duplicated implementations. The CI still passes after the latest changes: https://github.com/SukkaW/node-fs-extra/actions/runs/6598480115

Note that the `coprDir` has been heavily refactored (no more `items` array mutation!).

Patch:
diff --git a/lib/copy/copy.js b/lib/copy/copy.js
index bc188fb..5656021 100644
--- a/lib/copy/copy.js
+++ b/lib/copy/copy.js
@@ -1,23 +1,17 @@
 'use strict'
 
-const fs = require('graceful-fs')
+const fs = require('../fs')
 const path = require('path')
-const mkdirs = require('../mkdirs').mkdirs
-const pathExists = require('../path-exists').pathExists
-const utimesMillis = require('../util/utimes').utimesMillis
+const { mkdirs } = require('../mkdirs')
+const { pathExists } = require('../path-exists')
+const { utimesMillis } = require('../util/utimes')
 const stat = require('../util/stat')
 
-function copy (src, dest, opts, cb) {
-  if (typeof opts === 'function' && !cb) {
-    cb = opts
-    opts = {}
-  } else if (typeof opts === 'function') {
+async function copy (src, dest, opts = {}) {
+  if (typeof opts === 'function') {
     opts = { filter: opts }
   }
 
-  cb = cb || function () {}
-  opts = opts || {}
-
   opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
   opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber
 
@@ -30,209 +24,152 @@ function copy (src, dest, opts, cb) {
     )
   }
 
-  stat.checkPaths(src, dest, 'copy', opts, (err, stats) => {
-    if (err) return cb(err)
-    const { srcStat, destStat } = stats
-    stat.checkParentPaths(src, srcStat, dest, 'copy', err => {
-      if (err) return cb(err)
-      runFilter(src, dest, opts, (err, include) => {
-        if (err) return cb(err)
-        if (!include) return cb()
-
-        checkParentDir(destStat, src, dest, opts, cb)
-      })
-    })
-  })
-}
+  const { srcStat, destStat } = await stat.checkPaths(src, dest, 'copy', opts)
+
+  await stat.checkParentPaths(src, srcStat, dest, 'copy')
+
+  const include = await runFilter(src, dest, opts)
+
+  if (!include) return
 
-function checkParentDir (destStat, src, dest, opts, cb) {
+  // check if the parent of dest exists, and create it if it doesn't exist
   const destParent = path.dirname(dest)
-  pathExists(destParent, (err, dirExists) => {
-    if (err) return cb(err)
-    if (dirExists) return getStats(destStat, src, dest, opts, cb)
-    mkdirs(destParent, err => {
-      if (err) return cb(err)
-      return getStats(destStat, src, dest, opts, cb)
-    })
-  })
-}
+  const dirExists = await pathExists(destParent)
+  if (!dirExists) {
+    await mkdirs(destParent)
+  }
 
-function runFilter (src, dest, opts, cb) {
-  if (!opts.filter) return cb(null, true)
-  Promise.resolve(opts.filter(src, dest))
-    .then(include => cb(null, include), error => cb(error))
+  await getStatsAndPerformCopy(destStat, src, dest, opts)
 }
 
-function getStats (destStat, src, dest, opts, cb) {
-  const stat = opts.dereference ? fs.stat : fs.lstat
-  stat(src, (err, srcStat) => {
-    if (err) return cb(err)
-
-    if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts, cb)
-    else if (srcStat.isFile() ||
-             srcStat.isCharacterDevice() ||
-             srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts, cb)
-    else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts, cb)
-    else if (srcStat.isSocket()) return cb(new Error(`Cannot copy a socket file: ${src}`))
-    else if (srcStat.isFIFO()) return cb(new Error(`Cannot copy a FIFO pipe: ${src}`))
-    return cb(new Error(`Unknown file: ${src}`))
-  })
+async function runFilter (src, dest, opts) {
+  if (!opts.filter) return true
+  return opts.filter(src, dest)
 }
 
-function onFile (srcStat, destStat, src, dest, opts, cb) {
-  if (!destStat) return copyFile(srcStat, src, dest, opts, cb)
-  return mayCopyFile(srcStat, src, dest, opts, cb)
+async function getStatsAndPerformCopy (destStat, src, dest, opts) {
+  const statFn = opts.dereference ? fs.stat : fs.lstat
+  const srcStat = await statFn(src)
+
+  if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts)
+
+  if (
+    srcStat.isFile() ||
+    srcStat.isCharacterDevice() ||
+    srcStat.isBlockDevice()
+  ) return onFile(srcStat, destStat, src, dest, opts)
+
+  if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts)
+  if (srcStat.isSocket()) throw new Error(`Cannot copy a socket file: ${src}`)
+  if (srcStat.isFIFO()) throw new Error(`Cannot copy a FIFO pipe: ${src}`)
+  throw new Error(`Unknown file: ${src}`)
 }
 
-function mayCopyFile (srcStat, src, dest, opts, cb) {
+async function onFile (srcStat, destStat, src, dest, opts) {
+  if (!destStat) return copyFile(srcStat, src, dest, opts)
+
   if (opts.overwrite) {
-    fs.unlink(dest, err => {
-      if (err) return cb(err)
-      return copyFile(srcStat, src, dest, opts, cb)
-    })
-  } else if (opts.errorOnExist) {
-    return cb(new Error(`'${dest}' already exists`))
-  } else return cb()
+    await fs.unlink(dest)
+    return copyFile(srcStat, src, dest, opts)
+  }
+  if (opts.errorOnExist) {
+    throw new Error(`'${dest}' already exists`)
+  }
 }
 
-function copyFile (srcStat, src, dest, opts, cb) {
-  fs.copyFile(src, dest, err => {
-    if (err) return cb(err)
-    if (opts.preserveTimestamps) return handleTimestampsAndMode(srcStat.mode, src, dest, cb)
-    return setDestMode(dest, srcStat.mode, cb)
-  })
-}
+async function copyFile (srcStat, src, dest, opts) {
+  await fs.copyFile(src, dest)
+  if (opts.preserveTimestamps) {
+    // Make sure the file is writable before setting the timestamp
+    // otherwise open fails with EPERM when invoked with 'r+'
+    // (through utimes call)
+    if (fileIsNotWritable(srcStat.mode)) {
+      await makeFileWritable(dest, srcStat.mode)
+    }
 
-function handleTimestampsAndMode (srcMode, src, dest, cb) {
-  // Make sure the file is writable before setting the timestamp
-  // otherwise open fails with EPERM when invoked with 'r+'
-  // (through utimes call)
-  if (fileIsNotWritable(srcMode)) {
-    return makeFileWritable(dest, srcMode, err => {
-      if (err) return cb(err)
-      return setDestTimestampsAndMode(srcMode, src, dest, cb)
-    })
+    // Set timestamps and mode correspondingly
+
+    // Note that The initial srcStat.atime cannot be trusted
+    // because it is modified by the read(2) system call
+    // (See https://nodejs.org/api/fs.html#fs_stat_time_values)
+    const updatedSrcStat = await fs.stat(src)
+    await utimesMillis(dest, updatedSrcStat.atime, updatedSrcStat.mtime)
   }
-  return setDestTimestampsAndMode(srcMode, src, dest, cb)
+
+  return fs.chmod(dest, srcStat.mode)
 }
 
 function fileIsNotWritable (srcMode) {
   return (srcMode & 0o200) === 0
 }
 
-function makeFileWritable (dest, srcMode, cb) {
-  return setDestMode(dest, srcMode | 0o200, cb)
-}
-
-function setDestTimestampsAndMode (srcMode, src, dest, cb) {
-  setDestTimestamps(src, dest, err => {
-    if (err) return cb(err)
-    return setDestMode(dest, srcMode, cb)
-  })
+function makeFileWritable (dest, srcMode) {
+  return fs.chmod(dest, srcMode | 0o200)
 }
 
-function setDestMode (dest, srcMode, cb) {
-  return fs.chmod(dest, srcMode, cb)
-}
+async function onDir (srcStat, destStat, src, dest, opts) {
+  // the dest directory might not exist, create it
+  if (!destStat) {
+    await fs.mkdir(dest)
+  }
 
-function setDestTimestamps (src, dest, cb) {
-  // The initial srcStat.atime cannot be trusted
-  // because it is modified by the read(2) system call
-  // (See https://nodejs.org/api/fs.html#fs_stat_time_values)
-  fs.stat(src, (err, updatedSrcStat) => {
-    if (err) return cb(err)
-    return utimesMillis(dest, updatedSrcStat.atime, updatedSrcStat.mtime, cb)
-  })
-}
+  // loop through the files in the current directory to copy everything
+  for (const item of await fs.readdir(src)) {
+    const srcItem = path.join(src, item)
+    const destItem = path.join(dest, item)
 
-function onDir (srcStat, destStat, src, dest, opts, cb) {
-  if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts, cb)
-  return copyDir(src, dest, opts, cb)
-}
+    // skip the item if it is matches by the filter function
+    const include = await runFilter(srcItem, destItem, opts)
+    if (!include) continue
 
-function mkDirAndCopy (srcMode, src, dest, opts, cb) {
-  fs.mkdir(dest, err => {
-    if (err) return cb(err)
-    copyDir(src, dest, opts, err => {
-      if (err) return cb(err)
-      return setDestMode(dest, srcMode, cb)
-    })
-  })
-}
+    const { destStat } = await stat.checkPaths(srcItem, destItem, 'copy', opts)
 
-function copyDir (src, dest, opts, cb) {
-  fs.readdir(src, (err, items) => {
-    if (err) return cb(err)
-    return copyDirItems(items, src, dest, opts, cb)
-  })
-}
+    // 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)
+  }
 
-function copyDirItems (items, src, dest, opts, cb) {
-  const item = items.pop()
-  if (!item) return cb()
-  return copyDirItem(items, item, src, dest, opts, cb)
+  if (!destStat) {
+    await fs.chmod(dest, srcStat.mode)
+  }
 }
 
-function copyDirItem (items, item, src, dest, opts, cb) {
-  const srcItem = path.join(src, item)
-  const destItem = path.join(dest, item)
-  runFilter(srcItem, destItem, opts, (err, include) => {
-    if (err) return cb(err)
-    if (!include) return copyDirItems(items, src, dest, opts, cb)
-
-    stat.checkPaths(srcItem, destItem, 'copy', opts, (err, stats) => {
-      if (err) return cb(err)
-      const { destStat } = stats
-      getStats(destStat, srcItem, destItem, opts, err => {
-        if (err) return cb(err)
-        return copyDirItems(items, src, dest, opts, cb)
-      })
-    })
-  })
-}
+async function onLink (destStat, src, dest, opts) {
+  let resolvedSrc = await fs.readlink(src)
+  if (opts.dereference) {
+    resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
+  }
+  if (!destStat) {
+    return fs.symlink(resolvedSrc, dest)
+  }
 
-function onLink (destStat, src, dest, opts, cb) {
-  fs.readlink(src, (err, resolvedSrc) => {
-    if (err) return cb(err)
-    if (opts.dereference) {
-      resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
-    }
+  let resolvedDest = null
+  try {
+    resolvedDest = await fs.readlink(dest)
+  } catch (e) {
+    // dest exists and is a regular file or directory,
+    // Windows may throw UNKNOWN error. If dest already exists,
+    // fs throws error anyway, so no need to guard against it here.
+    if (e.code === 'EINVAL' || e.code === 'UNKNOWN') return fs.symlink(resolvedSrc, dest)
+    throw e
+  }
+  if (opts.dereference) {
+    resolvedDest = path.resolve(process.cwd(), resolvedDest)
+  }
+  if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) {
+    throw new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`)
+  }
 
-    if (!destStat) {
-      return fs.symlink(resolvedSrc, dest, cb)
-    } else {
-      fs.readlink(dest, (err, resolvedDest) => {
-        if (err) {
-          // dest exists and is a regular file or directory,
-          // Windows may throw UNKNOWN error. If dest already exists,
-          // fs throws error anyway, so no need to guard against it here.
-          if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlink(resolvedSrc, dest, cb)
-          return cb(err)
-        }
-        if (opts.dereference) {
-          resolvedDest = path.resolve(process.cwd(), resolvedDest)
-        }
-        if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) {
-          return cb(new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`))
-        }
-
-        // do not copy if src is a subdir of dest since unlinking
-        // dest in this case would result in removing src contents
-        // and therefore a broken symlink would be created.
-        if (stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
-          return cb(new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`))
-        }
-        return copyLink(resolvedSrc, dest, cb)
-      })
-    }
-  })
-}
+  // do not copy if src is a subdir of dest since unlinking
+  // dest in this case would result in removing src contents
+  // and therefore a broken symlink would be created.
+  if (stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
+    throw new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`)
+  }
 
-function copyLink (resolvedSrc, dest, cb) {
-  fs.unlink(dest, err => {
-    if (err) return cb(err)
-    return fs.symlink(resolvedSrc, dest, cb)
-  })
+  // copy the link
+  await fs.unlink(dest)
+  return fs.symlink(resolvedSrc, dest)
 }
 
 module.exports = copy
diff --git a/lib/copy/index.js b/lib/copy/index.js
index 45c07a2..2e31d27 100644
--- a/lib/copy/index.js
+++ b/lib/copy/index.js
@@ -1,6 +1,6 @@
 'use strict'
 
-const u = require('universalify').fromCallback
+const u = require('universalify').fromPromise
 module.exports = {
   copy: u(require('./copy')),
   copySync: require('./copy-sync')
diff --git a/lib/ensure/file.js b/lib/ensure/file.js
index 15cc473..a55c2d9 100644
--- a/lib/ensure/file.js
+++ b/lib/ensure/file.js
@@ -1,50 +1,47 @@
 'use strict'
 
-const u = require('universalify').fromCallback
+const u = require('universalify').fromPromise
 const path = require('path')
-const fs = require('graceful-fs')
+const fs = require('../fs')
 const mkdir = require('../mkdirs')
 
-function createFile (file, callback) {
-  function makeFile () {
-    fs.writeFile(file, '', err => {
-      if (err) return callback(err)
-      callback()
-    })
-  }
+async function createFile (file) {
+  let stats
+  try {
+    stats = await fs.stat(file)
+  } catch { }
+  if (stats && stats.isFile()) return
 
-  fs.stat(file, (err, stats) => { // eslint-disable-line handle-callback-err
-    if (!err && stats.isFile()) return callback()
-    const dir = path.dirname(file)
-    fs.stat(dir, (err, stats) => {
-      if (err) {
-        // if the directory doesn't exist, make it
-        if (err.code === 'ENOENT') {
-          return mkdir.mkdirs(dir, err => {
-            if (err) return callback(err)
-            makeFile()
-          })
-        }
-        return callback(err)
-      }
+  const dir = path.dirname(file)
 
-      if (stats.isDirectory()) makeFile()
-      else {
-        // parent is not a directory
-        // This is just to cause an internal ENOTDIR error to be thrown
-        fs.readdir(dir, err => {
-          if (err) return callback(err)
-        })
-      }
-    })
-  })
+  let dirStats = null
+  try {
+    dirStats = await fs.stat(dir)
+  } catch (err) {
+    // if the directory doesn't exist, make it
+    if (err.code === 'ENOENT') {
+      await mkdir.mkdirs(dir)
+      await fs.writeFile(file, '')
+      return
+    } else {
+      throw err
+    }
+  }
+
+  if (dirStats.isDirectory()) {
+    await fs.writeFile(file, '')
+  } else {
+    // parent is not a directory
+    // This is just to cause an internal ENOTDIR error to be thrown
+    await fs.readdir(dir)
+  }
 }
 
 function createFileSync (file) {
   let stats
   try {
     stats = fs.statSync(file)
-  } catch {}
+  } catch { }
   if (stats && stats.isFile()) return
 
   const dir = path.dirname(file)
diff --git a/lib/ensure/link.js b/lib/ensure/link.js
index f6d6748..c3d1c69 100644
--- a/lib/ensure/link.js
+++ b/lib/ensure/link.js
@@ -1,39 +1,39 @@
 'use strict'
 
-const u = require('universalify').fromCallback
+const u = require('universalify').fromPromise
 const path = require('path')
-const fs = require('graceful-fs')
+const fs = require('../fs')
 const mkdir = require('../mkdirs')
-const pathExists = require('../path-exists').pathExists
+const { pathExists } = require('../path-exists')
 const { areIdentical } = require('../util/stat')
 
-function createLink (srcpath, dstpath, callback) {
-  function makeLink (srcpath, dstpath) {
-    fs.link(srcpath, dstpath, err => {
-      if (err) return callback(err)
-      callback(null)
-    })
+async function createLink (srcpath, dstpath) {
+  let dstStat
+  try {
+    dstStat = await fs.lstat(dstpath)
+  } catch {
+    // ignore error
   }
 
-  fs.lstat(dstpath, (_, dstStat) => {
-    fs.lstat(srcpath, (err, srcStat) => {
-      if (err) {
-        err.message = err.message.replace('lstat', 'ensureLink')
-        return callback(err)
-      }
-      if (dstStat && areIdentical(srcStat, dstStat)) return callback(null)
+  let srcStat
+  try {
+    srcStat = await fs.lstat(srcpath)
+  } catch (err) {
+    err.message = err.message.replace('lstat', 'ensureLink')
+    throw err
+  }
+
+  if (dstStat && areIdentical(srcStat, dstStat)) return
+
+  const dir = path.dirname(dstpath)
+
+  const dirExists = await pathExists(dir)
+
+  if (!dirExists) {
+    await mkdir.mkdirs(dir)
+  }
 
-      const dir = path.dirname(dstpath)
-      pathExists(dir, (err, dirExists) => {
-        if (err) return callback(err)
-        if (dirExists) return makeLink(srcpath, dstpath)
-        mkdir.mkdirs(dir, err => {
-          if (err) return callback(err)
-          makeLink(srcpath, dstpath)
-        })
-      })
-    })
-  })
+  await fs.link(srcpath, dstpath)
 }
 
 function createLinkSync (srcpath, dstpath) {
diff --git a/lib/ensure/symlink-paths.js b/lib/ensure/symlink-paths.js
index 33cd760..85dda47 100644
--- a/lib/ensure/symlink-paths.js
+++ b/lib/ensure/symlink-paths.js
@@ -1,8 +1,10 @@
 'use strict'
 
 const path = require('path')
-const fs = require('graceful-fs')
-const pathExists = require('../path-exists').pathExists
+const fs = require('../fs')
+const { pathExists } = require('../path-exists')
+
+const u = require('universalify').fromPromise
 
 /**
  * Function that returns two types of paths, one relative to symlink, and one
@@ -26,74 +28,74 @@ const pathExists = require('../path-exists').pathExists
  * the ability to pass in `relative to current working direcotry` paths.
  */
 
-function symlinkPaths (srcpath, dstpath, callback) {
+async function symlinkPaths (srcpath, dstpath) {
   if (path.isAbsolute(srcpath)) {
-    return fs.lstat(srcpath, (err) => {
-      if (err) {
-        err.message = err.message.replace('lstat', 'ensureSymlink')
-        return callback(err)
-      }
-      return callback(null, {
-        toCwd: srcpath,
-        toDst: srcpath
-      })
-    })
-  } else {
-    const dstdir = path.dirname(dstpath)
-    const relativeToDst = path.join(dstdir, srcpath)
-    return pathExists(relativeToDst, (err, exists) => {
-      if (err) return callback(err)
-      if (exists) {
-        return callback(null, {
-          toCwd: relativeToDst,
-          toDst: srcpath
-        })
-      } else {
-        return fs.lstat(srcpath, (err) => {
-          if (err) {
-            err.message = err.message.replace('lstat', 'ensureSymlink')
-            return callback(err)
-          }
-          return callback(null, {
-            toCwd: srcpath,
-            toDst: path.relative(dstdir, srcpath)
-          })
-        })
-      }
-    })
+    try {
+      await fs.lstat(srcpath)
+    } catch (err) {
+      err.message = err.message.replace('lstat', 'ensureSymlink')
+      throw err
+    }
+
+    return {
+      toCwd: srcpath,
+      toDst: srcpath
+    }
+  }
+
+  const dstdir = path.dirname(dstpath)
+  const relativeToDst = path.join(dstdir, srcpath)
+
+  const exists = await pathExists(relativeToDst)
+  if (exists) {
+    return {
+      toCwd: relativeToDst,
+      toDst: srcpath
+    }
+  }
+
+  try {
+    await fs.lstat(srcpath)
+  } catch (err) {
+    err.message = err.message.replace('lstat', 'ensureSymlink')
+    throw err
+  }
+
+  return {
+    toCwd: srcpath,
+    toDst: path.relative(dstdir, srcpath)
   }
 }
 
 function symlinkPathsSync (srcpath, dstpath) {
-  let exists
   if (path.isAbsolute(srcpath)) {
-    exists = fs.existsSync(srcpath)
+    const exists = fs.existsSync(srcpath)
     if (!exists) throw new Error('absolute srcpath does not exist')
     return {
       toCwd: srcpath,
       toDst: srcpath
     }
-  } else {
-    const dstdir = path.dirname(dstpath)
-    const relativeToDst = path.join(dstdir, srcpath)
-    exists = fs.existsSync(relativeToDst)
-    if (exists) {
-      return {
-        toCwd: relativeToDst,
-        toDst: srcpath
-      }
-    } else {
-      exists = fs.existsSync(srcpath)
-      if (!exists) throw new Error('relative srcpath does not exist')
-      return {
-        toCwd: srcpath,
-        toDst: path.relative(dstdir, srcpath)
-      }
+  }
+
+  const dstdir = path.dirname(dstpath)
+  const relativeToDst = path.join(dstdir, srcpath)
+  const exists = fs.existsSync(relativeToDst)
+  if (exists) {
+    return {
+      toCwd: relativeToDst,
+      toDst: srcpath
     }
   }
+
+  const srcExists = fs.existsSync(srcpath)
+  if (!srcExists) throw new Error('relative srcpath does not exist')
+  return {
+    toCwd: srcpath,
+    toDst: path.relative(dstdir, srcpath)
+  }
 }
 
 module.exports = {
-  symlinkPaths,
+  symlinkPaths: u(symlinkPaths),
   symlinkPathsSync
 }
diff --git a/lib/ensure/symlink-type.js b/lib/ensure/symlink-type.js
index 42dc0ce..518558e 100644
--- a/lib/ensure/symlink-type.js
+++ b/lib/ensure/symlink-type.js
@@ -1,22 +1,25 @@
 'use strict'
 
-const fs = require('graceful-fs')
+const fs = require('../fs')
+const u = require('universalify').fromPromise
 
-function symlinkType (srcpath, type, callback) {
-  callback = (typeof type === 'function') ? type : callback
-  type = (typeof type === 'function') ? false : type
-  if (type) return callback(null, type)
-  fs.lstat(srcpath, (err, stats) => {
-    if (err) return callback(null, 'file')
-    type = (stats && stats.isDirectory()) ? 'dir' : 'file'
-    callback(null, type)
-  })
-}
+async function symlinkType (srcpath, type) {
+  if (type) return type
 
-function symlinkTypeSync (srcpath, type) {
   let stats
+  try {
+    stats = await fs.lstat(srcpath)
+  } catch {
+    return 'file'
+  }
 
+  return (stats && stats.isDirectory()) ? 'dir' : 'file'
+}
+
+function symlinkTypeSync (srcpath, type) {
   if (type) return type
+
+  let stats
   try {
     stats = fs.lstatSync(srcpath)
   } catch {
@@ -26,6 +29,6 @@ function symlinkTypeSync (srcpath, type) {
 }
 
 module.exports = {
-  symlinkType,
+  symlinkType: u(symlinkType),
   symlinkTypeSync
 }
diff --git a/lib/ensure/symlink.js b/lib/ensure/symlink.js
index 2b93052..a3d5f57 100644
--- a/lib/ensure/symlink.js
+++ b/lib/ensure/symlink.js
@@ -1,65 +1,50 @@
 'use strict'
 
-const u = require('universalify').fromCallback
+const u = require('universalify').fromPromise
 const path = require('path')
 const fs = require('../fs')
-const _mkdirs = require('../mkdirs')
-const mkdirs = _mkdirs.mkdirs
-const mkdirsSync = _mkdirs.mkdirsSync
 
-const _symlinkPaths = require('./symlink-paths')
-const symlinkPaths = _symlinkPaths.symlinkPaths
-const symlinkPathsSync = _symlinkPaths.symlinkPathsSync
+const { mkdirs, mkdirsSync } = require('../mkdirs')
 
-const _symlinkType = require('./symlink-type')
-const symlinkType = _symlinkType.symlinkType
-const symlinkTypeSync = _symlinkType.symlinkTypeSync
+const { symlinkPaths, symlinkPathsSync } = require('./symlink-paths')
+const { symlinkType, symlinkTypeSync } = require('./symlink-type')
 
-const pathExists = require('../path-exists').pathExists
+const { pathExists } = require('../path-exists')
 
 const { areIdentical } = require('../util/stat')
 
-function createSymlink (srcpath, dstpath, type, callback) {
-  callback = (typeof type === 'function') ? type : callback
-  type = (typeof type === 'function') ? false : type
+async function createSymlink (srcpath, dstpath, type) {
+  let stats
+  try {
+    stats = await fs.lstat(dstpath)
+  } catch { }
 
-  fs.lstat(dstpath, (err, stats) => {
-    if (!err && stats.isSymbolicLink()) {
-      Promise.all([
-        fs.stat(srcpath),
-        fs.stat(dstpath)
-      ]).then(([srcStat, dstStat]) => {
-        if (areIdentical(srcStat, dstStat)) return callback(null)
-        _createSymlink(srcpath, dstpath, type, callback)
-      })
-    } else _createSymlink(srcpath, dstpath, type, callback)
-  })
-}
+  if (stats && stats.isSymbolicLink()) {
+    const [srcStat, dstStat] = await Promise.all([
+      fs.stat(srcpath),
+      fs.stat(dstpath)
+    ])
+
+    if (areIdentical(srcStat, dstStat)) return
+  }
+
+  const relative = await symlinkPaths(srcpath, dstpath)
+  srcpath = relative.toDst
+  const toType = await symlinkType(relative.toCwd, type)
+  const dir = path.dirname(dstpath)
+
+  if (!(await pathExists(dir))) {
+    await mkdirs(dir)
+  }
 
-function _createSymlink (srcpath, dstpath, type, callback) {
-  symlinkPaths(srcpath, dstpath, (err, relative) => {
-    if (err) return callback(err)
-    srcpath = relative.toDst
-    symlinkType(relative.toCwd, type, (err, type) => {
-      if (err) return callback(err)
-      const dir = path.dirname(dstpath)
-      pathExists(dir, (err, dirExists) => {
-        if (err) return callback(err)
-        if (dirExists) return fs.symlink(srcpath, dstpath, type, callback)
-        mkdirs(dir, err => {
-          if (err) return callback(err)
-          fs.symlink(srcpath, dstpath, type, callback)
-        })
-      })
-    })
-  })
+  return fs.symlink(srcpath, dstpath, toType)
 }
 
 function createSymlinkSync (srcpath, dstpath, type) {
   let stats
   try {
     stats = fs.lstatSync(dstpath)
-  } catch {}
+  } catch { }
   if (stats && stats.isSymbolicLink()) {
     const srcStat = fs.statSync(srcpath)
     const dstStat = fs.statSync(dstpath)
diff --git a/lib/output-file/index.js b/lib/output-file/index.js
index 92297ca..a42d943 100644
--- a/lib/output-file/index.js
+++ b/lib/output-file/index.js
@@ -1,36 +1,27 @@
 'use strict'
 
-const u = require('universalify').fromCallback
-const fs = require('graceful-fs')
+const u = require('universalify').fromPromise
+const fs = require('../fs')
 const path = require('path')
 const mkdir = require('../mkdirs')
 const pathExists = require('../path-exists').pathExists
 
-function outputFile (file, data, encoding, callback) {
-  if (typeof encoding === 'function') {
-    callback = encoding
-    encoding = 'utf8'
-  }
-
+async function outputFile (file, data, encoding = 'utf-8') {
   const dir = path.dirname(file)
-  pathExists(dir, (err, itDoes) => {
-    if (err) return callback(err)
-    if (itDoes) return fs.writeFile(file, data, encoding, callback)
 
-    mkdir.mkdirs(dir, err => {
-      if (err) return callback(err)
+  if (!(await pathExists(dir))) {
+    await mkdir.mkdirs(dir)
+  }
 
-      fs.writeFile(file, data, encoding, callback)
-    })
-  })
+  return fs.writeFile(file, data, encoding)
 }
 
 function outputFileSync (file, ...args) {
   const dir = path.dirname(file)
-  if (fs.existsSync(dir)) {
-    return fs.writeFileSync(file, ...args)
+  if (!fs.existsSync(dir)) {
+    mkdir.mkdirsSync(dir)
   }
-  mkdir.mkdirsSync(dir)
+
   fs.writeFileSync(file, ...args)
 }
 
diff --git a/lib/util/stat.js b/lib/util/stat.js
index 2efd1c3..dfd37d9 100644
--- a/lib/util/stat.js
+++ b/lib/util/stat.js
@@ -2,7 +2,7 @@
 
 const fs = require('../fs')
 const path = require('path')
-const util = require('util')
+const u = require('universalify').fromPromise
 
 function getStats (src, dest, opts) {
   const statFunc = opts.dereference
@@ -32,35 +32,32 @@ function getStatsSync (src, dest, opts) {
   return { srcStat, destStat }
 }
 
-function checkPaths (src, dest, funcName, opts, cb) {
-  util.callbackify(getStats)(src, dest, opts, (err, stats) => {
-    if (err) return cb(err)
-    const { srcStat, destStat } = stats
-
-    if (destStat) {
-      if (areIdentical(srcStat, destStat)) {
-        const srcBaseName = path.basename(src)
-        const destBaseName = path.basename(dest)
-        if (funcName === 'move' &&
-          srcBaseName !== destBaseName &&
-          srcBaseName.toLowerCase() === destBaseName.toLowerCase()) {
-          return cb(null, { srcStat, destStat, isChangingCase: true })
-        }
-        return cb(new Error('Source and destination must not be the same.'))
-      }
-      if (srcStat.isDirectory() && !destStat.isDirectory()) {
-        return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`))
-      }
-      if (!srcStat.isDirectory() && destStat.isDirectory()) {
-        return cb(new Error(`Cannot overwrite directory '${dest}' with non-directory '${src}'.`))
+async function checkPaths (src, dest, funcName, opts) {
+  const { srcStat, destStat } = await getStats(src, dest, opts)
+  if (destStat) {
+    if (areIdentical(srcStat, destStat)) {
+      const srcBaseName = path.basename(src)
+      const destBaseName = path.basename(dest)
+      if (funcName === 'move' &&
+        srcBaseName !== destBaseName &&
+        srcBaseName.toLowerCase() === destBaseName.toLowerCase()) {
+        return { srcStat, destStat, isChangingCase: true }
       }
+      throw new Error('Source and destination must not be the same.')
     }
-
-    if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
-      return cb(new Error(errMsg(src, dest, funcName)))
+    if (srcStat.isDirectory() && !destStat.isDirectory()) {
+      throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)
+    }
+    if (!srcStat.isDirectory() && destStat.isDirectory()) {
+      throw new Error(`Cannot overwrite directory '${dest}' with non-directory '${src}'.`)
     }
-    return cb(null, { srcStat, destStat })
-  })
+  }
+
+  if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
+    throw new Error(errMsg(src, dest, funcName))
+  }
+
+  return { srcStat, destStat }
 }
 
 function checkPathsSync (src, dest, funcName, opts) {
@@ -95,20 +92,24 @@ function checkPathsSync (src, dest, funcName, opts) {
 // It works for all file types including symlinks since it
 // checks the src and dest inodes. It starts from the deepest
 // parent and stops once it reaches the src parent or the root path.
-function checkParentPaths (src, srcStat, dest, funcName, cb) {
+async function checkParentPaths (src, srcStat, dest, funcName) {
   const srcParent = path.resolve(path.dirname(src))
   const destParent = path.resolve(path.dirname(dest))
-  if (destParent === srcParent || destParent === path.parse(destParent).root) return cb()
-  fs.stat(destParent, { bigint: true }, (err, destStat) => {
-    if (err) {
-      if (err.code === 'ENOENT') return cb()
-      return cb(err)
-    }
-    if (areIdentical(srcStat, destStat)) {
-      return cb(new Error(errMsg(src, dest, funcName)))
-    }
-    return checkParentPaths(src, srcStat, destParent, funcName, cb)
-  })
+  if (destParent === srcParent || destParent === path.parse(destParent).root) return
+
+  let destStat
+  try {
+    destStat = await fs.stat(destParent, { bigint: true })
+  } catch (err) {
+    if (err.code === 'ENOENT') return
+    throw err
+  }
+
+  if (areIdentical(srcStat, destStat)) {
+    throw new Error(errMsg(src, dest, funcName))
+  }
+
+  return checkParentPaths(src, srcStat, destParent, funcName)
 }
 
 function checkParentPathsSync (src, srcStat, dest, funcName) {
@@ -145,10 +146,13 @@ function errMsg (src, dest, funcName) {
 }
 
 module.exports = {
-  checkPaths,
+  // checkPaths
+  checkPaths: u(checkPaths),
   checkPathsSync,
-  checkParentPaths,
+  // checkParent
+  checkParentPaths: u(checkParentPaths),
   checkParentPathsSync,
+  // Misc
   isSrcSubdir,
   areIdentical
 }
diff --git a/lib/util/utimes.js b/lib/util/utimes.js
index 75395de..87f4588 100644
--- a/lib/util/utimes.js
+++ b/lib/util/utimes.js
@@ -1,17 +1,27 @@
 'use strict'
 
-const fs = require('graceful-fs')
+const fs = require('../fs')
+const u = require('universalify').fromPromise
 
-function utimesMillis (path, atime, mtime, callback) {
+async function utimesMillis (path, atime, mtime) {
   // if (!HAS_MILLIS_RES) return fs.utimes(path, atime, mtime, callback)
-  fs.open(path, 'r+', (err, fd) => {
-    if (err) return callback(err)
-    fs.futimes(fd, atime, mtime, futimesErr => {
-      fs.close(fd, closeErr => {
-        if (callback) callback(futimesErr || closeErr)
-      })
-    })
-  })
+  const fd = await fs.open(path, 'r+')
+
+  let closeErr = null
+
+  try {
+    await fs.futimes(fd, atime, mtime)
+  } finally {
+    try {
+      await fs.close(fd)
+    } catch (e) {
+      closeErr = e
+    }
+  }
+
+  if (closeErr) {
+    throw closeErr
+  }
 }
 
 function utimesMillisSync (path, atime, mtime) {
@@ -21,6 +31,6 @@ function utimesMillisSync (path, atime, mtime) {
 }
 
 module.exports = {
-  utimesMillis,
+  utimesMillis: u(utimesMillis),
   utimesMillisSync
 }


Test patch:
diff --git a/lib/copy/__tests__/copy-broken-symlink.test.js b/lib/copy/__tests__/copy-broken-symlink.test.js
index 81e46de..68d989d 100644
--- a/lib/copy/__tests__/copy-broken-symlink.test.js
+++ b/lib/copy/__tests__/copy-broken-symlink.test.js
@@ -5,7 +5,7 @@ const os = require('os')
 const fse = require('../..')
 const path = require('path')
 const assert = require('assert')
-const copy = require('../copy')
+const { copy } = require('../')
 
 /* global afterEach, beforeEach, describe, it */
 
diff --git a/lib/copy/__tests__/copy-preserve-timestamp.test.js b/lib/copy/__tests__/copy-preserve-timestamp.test.js
index 6f1336f..64ee5a3 100644
--- a/lib/copy/__tests__/copy-preserve-timestamp.test.js
+++ b/lib/copy/__tests__/copy-preserve-timestamp.test.js
@@ -3,7 +3,7 @@
 const fs = require('../../')
 const os = require('os')
 const path = require('path')
-const copy = require('../copy')
+const { copy } = require('../')
 const utimesSync = require('../../util/utimes').utimesMillisSync
 const assert = require('assert')
 
diff --git a/lib/copy/__tests__/ncp/broken-symlink.test.js b/lib/copy/__tests__/ncp/broken-symlink.test.js
index 956daa8..7a8db69 100644
--- a/lib/copy/__tests__/ncp/broken-symlink.test.js
+++ b/lib/copy/__tests__/ncp/broken-symlink.test.js
@@ -3,7 +3,7 @@
 const fs = require('fs')
 const os = require('os')
 const fse = require('../../..')
-const ncp = require('../../copy')
+const { copy: ncp } = require('../../')
 const path = require('path')
 const assert = require('assert')
 
diff --git a/lib/copy/__tests__/ncp/ncp-error-perm.test.js b/lib/copy/__tests__/ncp/ncp-error-perm.test.js
index 34b6af1..563e0ab 100644
--- a/lib/copy/__tests__/ncp/ncp-error-perm.test.js
+++ b/lib/copy/__tests__/ncp/ncp-error-perm.test.js
@@ -5,7 +5,7 @@
 const fs = require('fs')
 const os = require('os')
 const fse = require('../../..')
-const ncp = require('../../copy')
+const { copy: ncp } = require('../../')
 const path = require('path')
 const assert = require('assert')
 
diff --git a/lib/copy/__tests__/ncp/ncp.test.js b/lib/copy/__tests__/ncp/ncp.test.js
index 2b85437..5de57c0 100644
--- a/lib/copy/__tests__/ncp/ncp.test.js
+++ b/lib/copy/__tests__/ncp/ncp.test.js
@@ -1,7 +1,7 @@
 'use strict'
 
 const fs = require('fs')
-const ncp = require('../../copy')
+const { copy: ncp } = require('../../')
 const path = require('path')
 const rimraf = require('rimraf')
 const assert = require('assert')
diff --git a/lib/copy/__tests__/ncp/symlink.test.js b/lib/copy/__tests__/ncp/symlink.test.js
index 5c188b6..6845ce3 100644
--- a/lib/copy/__tests__/ncp/symlink.test.js
+++ b/lib/copy/__tests__/ncp/symlink.test.js
@@ -3,7 +3,7 @@
 const fs = require('fs')
 const os = require('os')
 const fse = require('../../..')
-const ncp = require('../../copy')
+const { copy: ncp } = require('../../')
 const path = require('path')
 const assert = require('assert')
 
diff --git a/lib/util/__tests__/utimes.test.js b/lib/util/__tests__/utimes.test.js
index d28ece1..02c65d1 100644
--- a/lib/util/__tests__/utimes.test.js
+++ b/lib/util/__tests__/utimes.test.js
@@ -6,6 +6,8 @@ const fse = require('../..')
 const path = require('path')
 const assert = require('assert')
 const proxyquire = require('proxyquire')
+const u = require('universalify').fromCallback
+
 let gracefulFsStub
 let utimes
 
@@ -33,7 +35,7 @@ describe('utimes', () => {
     fse.emptyDir(TEST_DIR, done)
     // reset stubs
     gracefulFsStub = {}
-    utimes = proxyquire('../utimes', { 'graceful-fs': gracefulFsStub })
+    utimes = proxyquire('../utimes', { '../fs': gracefulFsStub })
   })
 
   describe('utimesMillis()', () => {
@@ -68,25 +70,25 @@ describe('utimes', () => {
     it('should close open file desciptors after encountering an error', done => {
       const fakeFd = Math.random()
 
-      gracefulFsStub.open = (pathIgnored, flagsIgnored, modeIgnored, callback) => {
+      gracefulFsStub.open = u((pathIgnored, flagsIgnored, modeIgnored, callback) => {
         if (typeof modeIgnored === 'function') callback = modeIgnored
         process.nextTick(() => callback(null, fakeFd))
-      }
+      })
 
       let closeCalled = false
-      gracefulFsStub.close = (fd, callback) => {
+      gracefulFsStub.close = u((fd, callback) => {
         assert.strictEqual(fd, fakeFd)
         closeCalled = true
         if (callback) process.nextTick(callback)
-      }
+      })
 
       let testError
-      gracefulFsStub.futimes = (fd, atimeIgnored, mtimeIgnored, callback) => {
+      gracefulFsStub.futimes = u((fd, atimeIgnored, mtimeIgnored, callback) => {
         process.nextTick(() => {
           testError = new Error('A test error')
           callback(testError)
         })
-      }
+      })
 
       utimes.utimesMillis('ignored', 'ignored', 'ignored', err => {
         assert.strictEqual(err, testError)