next.js/packages/next/src/lib/recursive-readdir.ts
recursive-readdir.ts185 lines5.3 KB
import fs from 'fs/promises'
import path from 'path'

type Filter = (pathname: string) => boolean

type Result = {
  directories: string[]
  pathnames: string[]
  links: string[]
}

export type RecursiveReadDirOptions = {
  /**
   * Filter to ignore files with absolute pathnames, false to ignore.
   */
  pathnameFilter?: Filter

  /**
   * Filter to ignore files and directories with absolute pathnames, false to
   * ignore.
   */
  ignoreFilter?: Filter

  /**
   * Filter to ignore files and directories with the pathname part, false to
   * ignore.
   */
  ignorePartFilter?: Filter

  /**
   * Whether to sort the results, true by default.
   */
  sortPathnames?: boolean

  /**
   * Whether to return relative pathnames, true by default.
   */
  relativePathnames?: boolean
}

/**
 * Recursively reads a directory and returns the list of pathnames.
 *
 * @param rootDirectory the directory to read
 * @param options options to control the behavior of the recursive read
 * @returns the list of pathnames
 */
export async function recursiveReadDir(
  rootDirectory: string,
  options: RecursiveReadDirOptions = {}
): Promise<string[]> {
  // Grab our options.
  const {
    pathnameFilter,
    ignoreFilter,
    ignorePartFilter,
    sortPathnames = true,
    relativePathnames = true,
  } = options

  // The list of pathnames to return.
  const pathnames: string[] = []

  /**
   * Coerces the pathname to be relative if requested.
   */
  const coerce = relativePathnames
    ? (pathname: string) => pathname.replace(rootDirectory, '')
    : (pathname: string) => pathname

  // The queue of directories to scan.
  let directories: string[] = [rootDirectory]

  while (directories.length > 0) {
    // Load all the files in each directory at the same time.
    const results = await Promise.all(
      directories.map(async (directory) => {
        const result: Result = { directories: [], pathnames: [], links: [] }

        try {
          const dir = await fs.readdir(directory, { withFileTypes: true })
          for (const file of dir) {
            // If enabled, ignore the file if it matches the ignore filter.
            if (ignorePartFilter && ignorePartFilter(file.name)) {
              continue
            }

            // Handle each file.
            const absolutePathname = path.join(directory, file.name)

            // If enabled, ignore the file if it matches the ignore filter.
            if (ignoreFilter && ignoreFilter(absolutePathname)) {
              continue
            }

            // If the file is a directory, then add it to the list of directories,
            // they'll be scanned on a later pass.
            if (file.isDirectory()) {
              result.directories.push(absolutePathname)
            } else if (file.isSymbolicLink()) {
              result.links.push(absolutePathname)
            } else if (!pathnameFilter || pathnameFilter(absolutePathname)) {
              result.pathnames.push(coerce(absolutePathname))
            }
          }
        } catch (err: any) {
          // This can only happen when the underlying directory was removed. If
          // anything other than this error occurs, re-throw it.
          // if (err.code !== 'ENOENT') throw err
          if (err.code !== 'ENOENT' || directory === rootDirectory) throw err

          // The error occurred, so abandon reading this directory.
          return null
        }

        return result
      })
    )

    // Empty the directories, we'll fill it later if some of the files are
    // directories.
    directories = []

    // Keep track of any symbolic links we find, we'll resolve them later.
    const links = []

    // For each result of directory scans...
    for (const result of results) {
      // If the directory was removed, then skip it.
      if (!result) continue

      // Add any directories to the list of directories to scan.
      directories.push(...result.directories)

      // Add any symbolic links to the list of symbolic links to resolve.
      links.push(...result.links)

      // Add any file pathnames to the list of pathnames.
      pathnames.push(...result.pathnames)
    }

    // Resolve all the symbolic links we found if any.
    if (links.length > 0) {
      const resolved = await Promise.all(
        links.map(async (absolutePathname) => {
          try {
            return await fs.stat(absolutePathname)
          } catch (err: any) {
            // This can only happen when the underlying link was removed. If
            // anything other than this error occurs, re-throw it.
            if (err.code !== 'ENOENT') throw err

            // The error occurred, so abandon reading this directory.
            return null
          }
        })
      )

      for (let i = 0; i < links.length; i++) {
        const stats = resolved[i]

        // If the link was removed, then skip it.
        if (!stats) continue

        // We would have already ignored the file if it matched the ignore
        // filter, so we don't need to check it again.
        const absolutePathname = links[i]

        if (stats.isDirectory()) {
          directories.push(absolutePathname)
        } else if (!pathnameFilter || pathnameFilter(absolutePathname)) {
          pathnames.push(coerce(absolutePathname))
        }
      }
    }
  }

  // Sort the pathnames in place if requested.
  if (sortPathnames) {
    pathnames.sort()
  }

  return pathnames
}
Quest for Codev2.0.0
/
SIGN IN