/* IMPORT */ import fs from 'node:fs'; import path from 'node:path'; import {isFunction, makeCounterPromise} from './utils.js'; import type {Options, ResultDirectory, ResultDirectories, Result} from './types.js'; /* MAIN */ //TODO: Streamline the type of dirnmaps const readdir = ( rootPath: string, options?: Options ): Promise => { const followSymlinks = options?.followSymlinks ?? false; const maxDepth = options?.depth ?? Infinity; const maxPaths = options?.limit ?? Infinity; const ignore = options?.ignore ?? (() => false); const isIgnored = isFunction ( ignore ) ? ignore : ( targetPath: string ) => ignore.test ( targetPath ); const signal = options?.signal ?? { aborted: false }; const directories: string[] = []; const directoriesNames: Set = new Set (); const directoriesNamesToPaths: Record = {}; const files: string[] = []; const filesNames: Set = new Set (); const filesNamesToPaths: Record = {}; const symlinks: string[] = []; const symlinksNames: Set = new Set (); const symlinksNamesToPaths: Record = {}; const map: ResultDirectories = {}; const visited = new Set (); const resultEmpty: Result = { directories: [], directoriesNames: new Set (), directoriesNamesToPaths: {}, files: [], filesNames: new Set (), filesNamesToPaths: {}, symlinks: [], symlinksNames: new Set (), symlinksNamesToPaths: {}, map: {} }; const result: Result = { directories, directoriesNames, directoriesNamesToPaths, files, filesNames, filesNamesToPaths, symlinks, symlinksNames, symlinksNamesToPaths, map }; const {promise, increment, decrement} = makeCounterPromise (); let foundPaths = 0; const handleDirectory = ( dirmap: ResultDirectory, subPath: string, name: string, depth: number ): void => { 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: ResultDirectory, subPath: string, name: string ): void => { 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: ResultDirectory, subPath: string, name: string, depth: number ): void => { 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: ResultDirectory, rootPath: string, name: string, stat: fs.Stats, depth: number ): void => { 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: ResultDirectory, rootPath: string, dirent: fs.Dirent, depth: number ): void => { 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: ResultDirectory, rootPath: string, dirents: fs.Dirent[], depth: number ): void => { for ( let i = 0, l = dirents.length; i < l; i++ ) { handleDirent ( dirmap, rootPath, dirents[i], depth ); } }; const populateResultFromPath = ( rootPath: string, depth: number ): void => { 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 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 = async ( rootPath: string, depth: number ): Promise => { increment (); fs.realpath ( rootPath, ( error, realPath ) => { if ( error ) return decrement (); if ( signal.aborted ) return decrement (); fs.stat ( realPath, async ( 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: string, depth: number = 1 ): Promise => { 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;