@igorskyflyer/recursive-readdir
Version: 
📖 Fast, type-safe recursive directory reader for Node.js with depth control, entry filtering, and sync/async APIs. 📁
205 lines (204 loc) • 6.25 kB
JavaScript
import { u } from '@igorskyflyer/upath';
import { readdirSync } from 'node:fs';
/** Used for maxDepth parameter
 * @readonly
 */
export var Depth;
(function (Depth) {
    /** All subdirectories */
    Depth[Depth["All"] = -1] = "All";
    /** Only the root directory */
    Depth[Depth["Root"] = 0] = "Root";
})(Depth || (Depth = {}));
/** Used for entries filtering parameter
 * @readonly
 */
export var Entry;
(function (Entry) {
    /** All directory entries - files and subdirectories */
    Entry[Entry["All"] = 2730] = "All";
    /** Only files */
    Entry[Entry["FilesOnly"] = 3003] = "FilesOnly";
    /** Only subdirectories */
    Entry[Entry["DirectoriesOnly"] = 3276] = "DirectoriesOnly";
})(Entry || (Entry = {}));
function defaultPredicate() {
    return true;
}
function shouldPush(isDirectory, entries) {
    if (isDirectory && entries !== Entry.FilesOnly) {
        return true;
    }
    else if (!isDirectory && entries !== Entry.DirectoriesOnly) {
        return true;
    }
    return false;
}
function createRecursiveFilterParams(path, isDirectory, wasSkipped) {
    return { path, isDirectory, wasSkipped };
}
function recursiveDirSync(directory, options = {}, depth = Depth.Root, files = []) {
    if (!directory) {
        return [];
    }
    const { filter = defaultPredicate, maxDepth = Depth.Root, entries = Entry.All, addTrailingSlash = false } = options;
    try {
        const dirEntries = readdirSync(directory, { withFileTypes: true });
        if (dirEntries.length > 0) {
            depth++;
        }
        for (const entry of dirEntries) {
            processEntry(entry, directory, depth);
        }
    }
    catch {
        return [];
    }
    return files;
    function processEntry(entry, baseDir, currentDepth) {
        const fullPath = u(`${baseDir}/${entry.name}`);
        let path = maxDepth === Depth.Root ? entry.name : fullPath;
        let isDir = false;
        try {
            isDir = entry.isDirectory();
            if (addTrailingSlash && isDir) {
                path = u(path, true);
            }
        }
        catch {
            pushIfAllowed(path, isDir, true);
            return;
        }
        if (shouldRecurse(isDir, currentDepth)) {
            pushIfAllowed(path, isDir, false);
            if (isDir) {
                recursiveDirSync(fullPath, options, currentDepth, files);
            }
        }
        else {
            pushIfAllowed(path, isDir, false);
        }
    }
    function pushIfAllowed(path, isDir, skipped) {
        if (filter(createRecursiveFilterParams(path, isDir, skipped)) &&
            shouldPush(isDir, entries)) {
            files.push(path);
        }
    }
    function shouldRecurse(isDir, currentDepth) {
        return isDir && (maxDepth === Depth.All || currentDepth < maxDepth);
    }
}
/**
 * Asynchronously gets files/directories inside the given directory.
 * @param {string} directory the directory whose files/directories should be listed
 * @param {RecursiveDirOptions} [options] additional options
 * @returns {Promise<string[]>} returns Promise\<string[]\>
 */
export async function readDir(directory, options) {
    return new Promise((resolve, reject) => {
        try {
            resolve(recursiveDirSync(directory, options));
        }
        catch (e) {
            reject(e);
        }
    });
}
/**
 * Synchronously gets files/directories inside the given directory.
 * @param {string} directory the directory whose files/directories should be listed
 * @param {RecursiveDirOptions} [options] additional options
 * @returns {string[]} returns string[]
 */
export function readDirSync(directory, options) {
    return recursiveDirSync(directory, options);
}
/**
 * RecursiveDir class
 * @class
 */
export class RecursiveDir {
    #options;
    constructor() {
        this.#options = {
            maxDepth: Depth.Root,
            filter: defaultPredicate,
            entries: Entry.All,
            addTrailingSlash: false
        };
    }
    /**
     * Synchronously gets files/directories inside the given directory.
     * @param {string} directory the directory whose files/directories should be listed
     * @returns {string[]} returns string[]
     */
    readDirSync(directory) {
        return recursiveDirSync(directory, this.#options);
    }
    /**
     * Asynchronously gets files/directories inside the given directory.
     * @param {string} directory the directory whose files/directories should be listed
     * @returns {Promise<string[]>} returns Promise\<string[]\>
     */
    async readDir(directory) {
        return new Promise((resolve, reject) => {
            try {
                resolve(recursiveDirSync(directory, this.#options));
            }
            catch (e) {
                reject(e);
            }
        });
    }
    /**
     * Sets the **entries** property which controls which entries to show, files-only, directories-only or all (**default**), use any of the following values,
     *
     * - `Entry.FilesOnly`,
     * - `Entry.DirectoriesOnly`,
     * - `Entry.All` (**default**),
     * @param {Entry} value
     * @returns {RecursiveDir}
     */
    entries(value) {
        this.#options.entries = value;
        return this;
    }
    /**
     * Sets **maxDepth** which controls how many child directories'
     * entries are being listed.
     *
     * Possible values:
     *
     * - `Depth.All`
     * - `Depth.Root` (**default**)
     * - any arbitrary value that conforms the condition `maxDepth >= 0`.
     * @param {Depth} value
     * @returns {RecursiveDir}
     */
    maxDepth(value) {
        if (value >= Depth.All) {
            this.#options.maxDepth = value;
        }
        return this;
    }
    /**
     * Sets **filter** predicate function used for filtering
     * directory entries (directories/files)
     * @param {FilterCallback} value
     * @returns {RecursiveDir}
     */
    filter(value) {
        this.#options.filter = value;
        return this;
    }
    /**
     * Sets whether a trailing slash should be added to directory entries.
     * @param {boolean} value
     * @returns {RecursiveDir}
     */
    addTrailingSlash(value) {
        this.#options.addTrailingSlash = value;
        return this;
    }
}