fs/promises and fs.Dirent
Recursion is a functional heritage and so using it with functional style yields the best results. Here's an efficient dirs
program using Node's fast fs.Dirent objects and fs/promises
module. This approach allows you to skip wasteful fs.exist
or fs.stat
calls on every path -
import { readdir } from "fs/promises"
import { join } from "path"
async function* dirs (path = ".")
{ yield path
for (const dirent of await readdir(path, { withFileTypes: true }))
if (dirent.isDirectory())
yield* dirs(join(path, dirent.name))
}
async function* empty () {}
async function toArray (iter = empty())
{ let r = []
for await (const x of iter)
r.push(x)
return r
}
toArray(dirs(".")).then(console.log, console.error)
Let's get some files so we can see dirs
working -
$ yarn add immutable # (just some example package)
$ node main.js
[
'.',
'node_modules',
'node_modules/immutable',
'node_modules/immutable/contrib',
'node_modules/immutable/contrib/cursor',
'node_modules/immutable/contrib/cursor/__tests__',
'node_modules/immutable/dist'
]
async generators
And because we're using async generators, we can intuitively stop iteration as soon as a matching file is found -
import { readdir } from "fs/promises"
import { join, basename } from "path"
async function* dirs // ...
async function* empty // ...
async function toArray // ...
async function search (iter = empty(), test = _ => false)
{ for await (const p of iter)
if (test(p))
return p // <-- iteration stops here
}
search(dirs("."), path => basename(path) === "contrib") // <-- search for contrib
.then(console.log, console.error)
search(dirs("."), path => basename(path) === "foobar") // <-- search for foobar
.then(console.log, console.error)
$ node main.js
node_modules/immutable/contrib # ("contrib" found)
undefined # ("foobar" not found)
invent your own convenience
Above search
is a higher-order function like Array.prototype.find
. We could write searchByName
which probably more comfortable to use -
import // ...
async function* dirs // ...
async function* empty // ...
async function toArray // ...
async function search // ...
async function searchByName (iter = empty(), query = "")
{ return search(iter, p => basename(p) === query) }
searchByName(dirs("."), "contrib")
.then(console.log, console.error)
searchByName(dirs("."), "foobar")
.then(console.log, console.error)
Output is the same -
$ node main.js
node_modules/immutable/contrib # ("contrib" found)
undefined # ("foobar" not found)
make it a module
A practice that isn't emphasised enough. By making a module, we create a place to separate concerns and keep complexity from overwhelming the rest of our program -
// FsExtensions.js
import { readdir } from "fs/promises" // <-- import only what you need
import { join, basename } from "path"
async function* dirs // ...
async function* empty // ...
async function search // ...
async function searchByName // ...
async function toArray // ...
// ...
export { dirs, search, searchByName, toArray } // <-- you control what to export
// Main.js
import { dirs, searchByName } from './FsExtensions' // <-- import only what's used
searchByName(dirs("."), "contrib")
.then(console.log, console.error)
searchByName(dirs("."), "foobar")
.then(console.log, console.error)
limiting depth
dirs
is implemented using a recursive generator. We can easily restrict the depth of recursion by adding a parameter to our function. I'm using a default value of Infinity
but you can choose whatever you like -
async function* dirs (path = ".", depth = Infinity)
{ if (depth < 1) return // stop if depth limit is reached
yield path
for (const dirent of await readdir(path, { withFileTypes: true }))
if (dirent.isDirectory())
yield* dirs(join(path, dirent.name), depth - 1)
}
When dirs
is called with a second argument, the depth of recursion is limited -
toArray(dirs(".", 1)).then(console.log, console.error)
// [ '.' ]
toArray(dirs(".", 2)).then(console.log, console.error)
// [ '.', 'node_modules' ]
toArray(dirs(".", 3)).then(console.log, console.error)
// [ '.', 'node_modules', 'node_modules/immutable' ]
toArray(dirs(".", 4)).then(console.log, console.error)
// [ '.',
// 'node_modules',
// 'node_modules/immutable',
// 'node_modules/immutable/contrib',
// 'node_modules/immutable/dist'
// ]
searchByName(dirs(".", 1), "contrib").then(console.log, console.error)
// undefined
searchByName(dirs(".", 2), "contrib").then(console.log, console.error)
// undefined
searchByName(dirs(".", 3), "contrib").then(console.log, console.error)
// undefined
searchByName(dirs(".", 4), "contrib").then(console.log, console.error)
// node_modules/immutable/contrib
searchByName(dirs("."), "contrib").then(console.log, console.error)
// node_modules/immutable/contrib
searchByName(dirs("."), "foobar").then(console.log, console.error)
// undefined