/* IMPORT */ import fs from 'node:fs'; import path from 'node:path'; import makeCounterPromise from 'promise-make-counter'; import { NOOP_PROMISE_LIKE } from './constants.js'; import { castArray, isFunction } from './utils.js'; /* MAIN */ //TODO: Streamline the type of dirmaps const readdir = (rootPath, options) => { const followSymlinks = options?.followSymlinks ?? false; const maxDepth = options?.depth ?? Infinity; const maxPaths = options?.limit ?? Infinity; const ignore = options?.ignore ?? []; const ignores = castArray(ignore).map(ignore => isFunction(ignore) ? ignore : (targetPath) => ignore.test(targetPath)); const isIgnored = (targetPath) => ignores.some(ignore => ignore(targetPath)); const signal = options?.signal ?? { aborted: false }; const onDirents = options?.onDirents || (() => { }); const directories = []; const directoriesNames = new Set(); const directoriesNamesToPaths = {}; const files = []; const filesNames = new Set(); const filesNamesToPaths = {}; const symlinks = []; const symlinksNames = new Set(); const symlinksNamesToPaths = {}; const map = {}; const visited = new Set(); const resultEmpty = { directories: [], directoriesNames: new Set(), directoriesNamesToPaths: {}, files: [], filesNames: new Set(), filesNamesToPaths: {}, symlinks: [], symlinksNames: new Set(), symlinksNamesToPaths: {}, map: {} }; const result = { directories, directoriesNames, directoriesNamesToPaths, files, filesNames, filesNamesToPaths, symlinks, symlinksNames, symlinksNamesToPaths, map }; const { promise, increment, decrement } = makeCounterPromise(); let foundPaths = 0; const handleDirectory = (dirmap, subPath, name, depth) => { if (visited.has(subPath)) return; if (foundPaths >= maxPaths) return; foundPaths += 1; dirmap.directories.push(subPath); dirmap.directoriesNames.add(name); // dirmap.directoriesNamesToPaths.propertyIsEnumerable(name) || ( dirmap.directoriesNamesToPaths[name] = [] ); // dirmap.directoriesNamesToPaths[name].push ( subPath ); directories.push(subPath); directoriesNames.add(name); directoriesNamesToPaths.propertyIsEnumerable(name) || (directoriesNamesToPaths[name] = []); directoriesNamesToPaths[name].push(subPath); visited.add(subPath); if (depth >= maxDepth) return; if (foundPaths >= maxPaths) return; populateResultFromPath(subPath, depth + 1); }; const handleFile = (dirmap, subPath, name) => { if (visited.has(subPath)) return; if (foundPaths >= maxPaths) return; foundPaths += 1; dirmap.files.push(subPath); dirmap.filesNames.add(name); // dirmap.filesNamesToPaths.propertyIsEnumerable(name) || ( dirmap.filesNamesToPaths[name] = [] ); // dirmap.filesNamesToPaths[name].push ( subPath ); files.push(subPath); filesNames.add(name); filesNamesToPaths.propertyIsEnumerable(name) || (filesNamesToPaths[name] = []); filesNamesToPaths[name].push(subPath); visited.add(subPath); }; const handleSymlink = (dirmap, subPath, name, depth) => { if (visited.has(subPath)) return; if (foundPaths >= maxPaths) return; foundPaths += 1; dirmap.symlinks.push(subPath); dirmap.symlinksNames.add(name); // dirmap.symlinksNamesToPaths.propertyIsEnumerable(name) || ( dirmap.symlinksNamesToPaths[name] = [] ); // dirmap.symlinksNamesToPaths[name].push ( subPath ); symlinks.push(subPath); symlinksNames.add(name); symlinksNamesToPaths.propertyIsEnumerable(name) || (symlinksNamesToPaths[name] = []); symlinksNamesToPaths[name].push(subPath); visited.add(subPath); if (!followSymlinks) return; if (depth >= maxDepth) return; if (foundPaths >= maxPaths) return; populateResultFromSymlink(subPath, depth + 1); }; const handleStat = (dirmap, rootPath, name, stat, depth) => { if (signal.aborted) return; if (isIgnored(rootPath)) return; if (stat.isDirectory()) { handleDirectory(dirmap, rootPath, name, depth); } else if (stat.isFile()) { handleFile(dirmap, rootPath, name); } else if (stat.isSymbolicLink()) { handleSymlink(dirmap, rootPath, name, depth); } }; const handleDirent = (dirmap, rootPath, dirent, depth) => { if (signal.aborted) return; const separator = (rootPath === path.sep) ? '' : path.sep; const name = dirent.name; const subPath = `${rootPath}${separator}${name}`; if (isIgnored(subPath)) return; if (dirent.isDirectory()) { handleDirectory(dirmap, subPath, name, depth); } else if (dirent.isFile()) { handleFile(dirmap, subPath, name); } else if (dirent.isSymbolicLink()) { handleSymlink(dirmap, subPath, name, depth); } }; const handleDirents = (dirmap, rootPath, dirents, depth) => { for (let i = 0, l = dirents.length; i < l; i++) { handleDirent(dirmap, rootPath, dirents[i], depth); } }; const populateResultFromPath = (rootPath, depth) => { if (signal.aborted) return; if (depth > maxDepth) return; if (foundPaths >= maxPaths) return; increment(); fs.readdir(rootPath, { withFileTypes: true }, (error, dirents) => { if (error) return decrement(); if (signal.aborted) return decrement(); if (!dirents.length) return decrement(); const promise = onDirents(dirents) || NOOP_PROMISE_LIKE; promise.then(() => { const dirmap = map[rootPath] = { directories: [], directoriesNames: new Set(), directoriesNamesToPaths: {}, files: [], filesNames: new Set(), filesNamesToPaths: {}, symlinks: [], symlinksNames: new Set(), symlinksNamesToPaths: {} }; handleDirents(dirmap, rootPath, dirents, depth); decrement(); }); }); }; const populateResultFromSymlink = (rootPath, depth) => { increment(); fs.realpath(rootPath, (error, realPath) => { if (error) return decrement(); if (signal.aborted) return decrement(); fs.stat(realPath, (error, stat) => { if (error) return decrement(); if (signal.aborted) return decrement(); const name = path.basename(realPath); const dirmap = map[rootPath] = { directories: [], directoriesNames: new Set(), directoriesNamesToPaths: {}, files: [], filesNames: new Set(), filesNamesToPaths: {}, symlinks: [], symlinksNames: new Set(), symlinksNamesToPaths: {} }; handleStat(dirmap, realPath, name, stat, depth); decrement(); }); }); }; const getResult = async (rootPath, depth = 1) => { rootPath = path.normalize(rootPath); visited.add(rootPath); populateResultFromPath(rootPath, depth); await promise; if (signal.aborted) return resultEmpty; return result; }; return getResult(rootPath); }; /* EXPORT */ export default readdir;