UNPKG

@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
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; } }