Skip to content

Instantly share code, notes, and snippets.

@kethinov
Created September 22, 2013 09:04
Show Gist options
  • Save kethinov/6658166 to your computer and use it in GitHub Desktop.
Save kethinov/6658166 to your computer and use it in GitHub Desktop.
List all files in a directory in Node.js recursively in a synchronous fashion
// List all files in a directory in Node.js recursively in a synchronous fashion
var walkSync = function(dir, filelist) {
var fs = fs || require('fs'),
files = fs.readdirSync(dir);
filelist = filelist || [];
files.forEach(function(file) {
if (fs.statSync(dir + file).isDirectory()) {
filelist = walkSync(dir + file + '/', filelist);
}
else {
filelist.push(file);
}
});
return filelist;
};
@schadocalex
Copy link

schadocalex commented Oct 12, 2023

Node v20.1+

const filelist = await fs.readdir(dir, { recursive: true });

@weroro-sk
Copy link

weroro-sk commented Feb 26, 2025

TypeScript version with filters:

import {readdirSync, statSync} from "node:fs";
import * as path from "node:path";

/**
 * Options for scanning a directory.
 *
 * @template PathType - The type of the relative directory path.
 * @interface DirectoryScanOptions
 * @property {string | PathType} [baseDir=""] - The base directory for relative paths.
 * @property {string[]} [fileExtensions=[]] - An array of file extensions to filter.
 * @property {"all" | "dir" | "file"} [collectionType="all"] - Specifies what to collect:
 *   - "all": collect both files and directories
 *   - "dir": collect only directories
 *   - "file": collect only files
 */
interface DirectoryScanOptions<PathType extends string = ""> {
    /**
     * Optional relative directory path
     *
     * The base directory for relative paths.
     *
     * @default ""
     */
    baseDir?: string | PathType;

    /**
     * Optional array of file extensions
     *
     * An array of file extensions to filter.
     * 
     * @default []
     */
    fileExtensions?: string[];

    /**
     * Optional collection type
     *
     * Specifies what to collect:
     *  - "all": collect both files and directories
     *  - "dir": collect only directories
     *  - "file": collect only files
     *
     *  @default "all"
     */
    collectionType?: "all" | "dir" | "file";
}

interface ScanDirectory {

    /**
     * Scans a directory and collects paths based on specified options.
     *
     * @template PathType - The type of the directory path.
     * @param {PathType} targetDir - The path of the directory to scan.
     * @returns {string[]} - An array of collected paths relative to the specified directory.
     *
     * @example
     * // Example usage of scanDirectory function
     * const collectedPaths = scanDirectory('/path/to/directory', {
     *     baseDir: '/path/to',
     *     fileExtensions: ['.js', '.ts'],
     *     collectionType: 'file'
     * });
     * console.log(collectedPaths); // Outputs an array of relative file paths
     */
    <PathType extends string>(targetDir: PathType): string[];

    /**
     * Scans a directory and collects paths based on specified options.
     *
     * @template PathType - The type of the directory path.
     * @param {PathType} targetDir - The path of the directory to scan.
     * @param {DirectoryScanOptions<PathType>} [options] - Options for scanning the directory.
     * @returns {string[]} - An array of collected paths relative to the specified directory.
     *
     * @example
     * // Example usage of scanDirectory function
     * const collectedPaths = scanDirectory('/path/to/directory', {
     *     baseDir: '/path/to',
     *     fileExtensions: ['.js', '.ts'],
     *     collectionType: 'file'
     * });
     * console.log(collectedPaths); // Outputs an array of relative file paths
     */
    <PathType extends string>(targetDir: PathType, options: DirectoryScanOptions<PathType>): string[];
}

/**
 * Scans a directory and collects paths based on specified options.
 *
 * @template PathType - The type of the directory path.
 * @param {PathType} targetDir - The path of the directory to scan.
 * @param {DirectoryScanOptions<PathType>} [options] - Options for scanning the directory.
 * @returns {string[]} - An array of collected paths relative to the specified directory.
 *
 * @example
 * // Example usage of scanDirectory function
 * const collectedPaths = scanDirectory('/path/to/directory', {
 *     baseDir: '/path/to',
 *     fileExtensions: ['.js', '.ts'],
 *     collectionType: 'file'
 * });
 * console.log(collectedPaths); // Outputs an array of relative file paths
 */
export const scanDirectory: ScanDirectory = <PathType extends string>(
    targetDir: PathType, // The directory path to scan
    options?: DirectoryScanOptions<PathType> // Options for scanning a directory.
): string[] => {

    const {
        baseDir = "", // The base directory for relative paths
        fileExtensions = [], // Default to an empty array if no extensions are provided
        collectionType = "all" // Default to collecting all types
    } = options || {};

    // Type alias for the collection options
    type CollectionArray = Array<typeof collectionType>;

    // Array to hold the scanned paths
    const collectedPaths: string[] = [];
    // Read directory contents
    const directoryContents = readdirSync(targetDir, {withFileTypes: true, recursive: true});
    // Check if any extensions are provided
    const hasExtensions = !!fileExtensions?.length;

    // Iterate over each file/directory in the scanned directory
    for (const directoryEntry of directoryContents) {
        // Construct the full file path
        const fullPath = path.join(directoryEntry.parentPath, directoryEntry.name);
        // Get the file statistics
        const fileStats = statSync(fullPath);
        // Get the relative path
        const relativePath = path.relative(baseDir, fullPath);

        // Check if we should collect files
        if ((["all", "file"] as CollectionArray).includes(collectionType) && fileStats.isFile()) {
            // If extensions are specified, check if the file matches
            if (hasExtensions && !fileExtensions.includes(path.extname(fullPath))) {
                continue; // Skip this file if it doesn't match the extensions
            }
            // Add the relative file path to the collected array
            collectedPaths.push(relativePath);
        }
        // Check if we should collect directories
        else if ((["all", "dir"] as CollectionArray).includes(collectionType) && fileStats.isDirectory()) {
            // Add the relative directory path to the collected array
            collectedPaths.push(relativePath);
        }
    }

    // Return the collected paths
    return collectedPaths;
};

Vanilla JS version if you don't use TypeScript:

import {readdirSync, statSync} from "node:fs";
import * as path from "node:path";

/**
 * Scans a directory and collects paths based on specified options.
 *
 * @template PathType - The type of the directory path.
 * @param {PathType} targetDir - The path of the directory to scan.
 * @param {DirectoryScanOptions<PathType>} [options] - Options for scanning the directory.
 * @returns {string[]} - An array of collected paths relative to the specified directory.
 *
 * @example
 * // Example usage of scanDirectory function
 * const collectedPaths = scanDirectory('/path/to/directory', {
 *     baseDir: '/path/to',
 *     fileExtensions: ['.js', '.ts'],
 *     collectionType: 'file'
 * });
 * console.log(collectedPaths); // Outputs an array of relative file paths
 */
export const scanDirectory = (
    targetDir, // The directory path to scan
    options // Options for scanning a directory.
) => {
    const {
        baseDir = "", // The base directory for relative paths
        fileExtensions = [], // Default to an empty array if no extensions are provided
        collectionType = "all" // Default to collecting all types
    } = options || {};

    // Array to hold the scanned paths
    /** @type {string[]} */
    const collectedPaths = [];
    // Read directory contents
    /** @type {Dirent[]} */
    const directoryContents = readdirSync(targetDir, {withFileTypes: true, recursive: true});
    // Check if any extensions are provided
    /** @type {boolean} */
    const hasExtensions = !!fileExtensions?.length;

    // Iterate over each file/directory in the scanned directory
    for (const directoryEntry of directoryContents) {
        // Construct the full file path
        /** @type {string} */
        const fullPath = path.join(directoryEntry.parentPath, directoryEntry.name);
        // Get the file statistics
        /** @type {Stats} */
        const fileStats = statSync(fullPath);
        // Get the relative path
        /** @type {string} */
        const relativePath = path.relative(baseDir, fullPath);
        // Check if we should collect files
        if (["all", "file"].includes(collectionType) && fileStats.isFile()) {
            // If extensions are specified, check if the file matches
            if (hasExtensions && !fileExtensions.includes(path.extname(fullPath))) {
                continue; // Skip this file if it doesn't match the extensions
            }
            // Add the relative file path to the collected array
            collectedPaths.push(relativePath);
        }
        // Check if we should collect directories
        else if (["all", "dir"].includes(collectionType) && fileStats.isDirectory()) {
            // Add the relative directory path to the collected array
            collectedPaths.push(relativePath);
        }
    }
    // Return the collected paths
    return collectedPaths;
};

// JSODC types

/**
 * Options for scanning a directory.
 *
 * @template PathType - The type of the relative directory path.
 * @interface DirectoryScanOptions
 * @property {string | PathType} [baseDir=""] - The base directory for relative paths.
 * @property {string[]} [fileExtensions=[]] - An array of file extensions to filter.
 * @property {"all" | "dir" | "file"} [collectionType="all"] - Specifies what to collect:
 *   - "all": collect both files and directories
 *   - "dir": collect only directories
 *   - "file": collect only files
 */

@snowinmars-debug
Copy link

$ yarn add glob rxjs

/* --- */

import {Observable, of} from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import * as path from 'path';
import {glob, type GlobOptionsWithFileTypesUnset} from "glob";

const listFiles = (root: string, globMask = '*', ignoredFolders: string[] = []): Observable<string> => {
  const ignore = ignoredFolders.map(x => path.join(root, x));
  const options: GlobOptionsWithFileTypesUnset = {
    cwd: root,
    nodir: true,
    ignore: ignore,
  };

  // do not use path.join as it uses upper slash instead of lower slash, and it does not work
  return of(globMask).pipe(
    mergeMap(x=> glob(x, options)),
    mergeMap(x => x),
  );
}

/* --- */

const topLevel = listFiles('/dev/sdb', '*.ts', []).subscribe(x => { console.log(x); });
const recurse = listFiles('/dev/sdb', '**/*.ts', []).subscribe(x => { console.log(x); });

@meowsus
Copy link

meowsus commented Mar 12, 2025

I just want to say that every time someone comes up with another solution to this problem it makes my day :)

@ghostfreak3000
Copy link

wow.. been receiving notifications for this thing for 9 years!.. it's the most consistent item in my dev career

@Skhmt
Copy link

Skhmt commented Mar 13, 2025

There are only 3 hard problems in computer science... cache invalidation, naming things, off by one errors, and directory walking,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment