walk.js/README.md

226 lines
5.6 KiB
Markdown
Raw Permalink Normal View History

2020-12-09 00:28:11 +00:00
# Walk.js (@root/walk)
2020-12-09 11:10:56 +00:00
Walk a directory recursively and handle each entity (files, directories, symlnks, etc).
2020-12-09 00:28:11 +00:00
2020-12-09 11:48:55 +00:00
(a port of Go's [`filepath.Walk`](https://golang.org/pkg/path/filepath/#Walk)
using Node.js v10+'s `fs.readdir`'s `withFileTypes` and ES 2021)
2020-12-09 00:28:11 +00:00
```js
2020-12-09 11:10:56 +00:00
await Walk.walk(pathname, walkFunc);
2020-12-09 22:58:53 +00:00
async function walkFunc(err, pathname, dirent) {
// err is failure to lstat a file or directory
// pathname is relative path, including the file or folder name
// dirent = { name, isDirectory(), isFile(), isSymbolicLink(), ... }
if (err) {
return false;
}
console.log(pathname);
2020-12-09 11:10:56 +00:00
}
2020-12-09 00:28:11 +00:00
```
2020-12-09 22:58:53 +00:00
# Table of Contents
- Install
- Usage
- CommonJS
- ES Modules
- API
- Walk.walk
- walkFunc
- Example: filter dotfiles
- Walk.create
- withFileStats
- sort (and filter)
- [Node walk in <50 Lines of Code](https://therootcompany.com/blog/fs-walk-for-node-js/)
- License (MPL-2.0)
# Install
```bash
npm install --save @root/walk
```
2020-12-09 11:10:56 +00:00
2020-12-09 22:58:53 +00:00
# Usage
2020-12-09 11:10:56 +00:00
2020-12-09 22:58:53 +00:00
You can use this with Node v12+ using Vanilla JS (CommonJS) or ES2021 (ES Modules).
2020-12-09 11:10:56 +00:00
2020-12-09 22:58:53 +00:00
## CommonJS (Vanilla JS / ES5)
2020-12-09 00:28:11 +00:00
```js
2020-12-09 22:58:53 +00:00
var Walk = require("@root/walk");
var path = require("path");
2020-12-09 00:28:11 +00:00
2020-12-09 22:58:53 +00:00
Walk.walk("./", walkFunc).then(function () {
console.log("Done");
});
// walkFunc must be async, or return a Promise
function walkFunc(err, pathname, dirent) {
2020-12-09 11:10:56 +00:00
if (err) {
// throw an error to stop walking
// (or return to ignore and keep going)
console.warn("fs stat error for %s: %s", pathname, err.message);
2020-12-09 22:58:53 +00:00
return Promise.resolve();
2020-12-09 11:10:56 +00:00
}
// return false to skip a directory
2020-12-09 22:58:53 +00:00
// (ex: skipping "dot file" directories)
2020-12-09 11:10:56 +00:00
if (dirent.isDirectory() && dirent.name.startsWith(".")) {
2020-12-09 22:58:53 +00:00
return Promise.resolve(false);
2020-12-09 11:10:56 +00:00
}
2020-12-09 22:58:53 +00:00
// fs.Dirent is a slimmed-down, faster version of fs.Stats
2020-12-09 11:10:56 +00:00
console.log("name:", dirent.name, "in", path.dirname(pathname));
// (only one of these will be true)
console.log("is file?", dirent.isFile());
console.log("is link?", dirent.isSymbolicLink());
2020-12-09 22:58:53 +00:00
return Promise.resolve();
}
2020-12-09 11:10:56 +00:00
```
2020-12-09 22:58:53 +00:00
## ECMAScript 2021 (ES Modules)
`@root/walk` can be used with async/await or Promises.
2020-12-09 11:10:56 +00:00
```js
2020-12-09 22:58:53 +00:00
import { walk } from "@root/walk";
import path from "path";
2020-12-09 11:10:56 +00:00
2020-12-09 22:58:53 +00:00
const walkFunc = async (err, pathname, dirent) => {
2020-12-09 00:28:11 +00:00
if (err) {
throw err;
}
2020-12-09 11:10:56 +00:00
if (dirent.isDirectory() && dirent.name.startsWith(".")) {
2020-12-09 22:58:53 +00:00
return false;
2020-12-09 00:28:11 +00:00
}
2020-12-09 22:58:53 +00:00
2020-12-09 11:10:56 +00:00
console.log("name:", dirent.name, "in", path.dirname(pathname));
2020-12-09 22:58:53 +00:00
};
2020-12-09 11:10:56 +00:00
2020-12-09 22:58:53 +00:00
await walk("./", walkFunc);
console.log("Done");
2020-12-09 00:28:11 +00:00
```
# API Documentation
2020-12-09 22:58:53 +00:00
## Walk.walk(pathname, walkFunc)
`Walk.walk` walks `pathname` (inclusive) and calls `walkFunc` for each file system entity.
2020-12-09 00:28:11 +00:00
It can be used with Promises:
```js
2020-12-09 11:10:56 +00:00
Walk.walk(pathname, promiseWalker).then(doMore);
2020-12-09 00:28:11 +00:00
```
Or with async / await:
```js
2020-12-09 11:10:56 +00:00
await Walk.walk(pathname, asyncWalker);
2020-12-09 00:28:11 +00:00
```
The behavior should exactly match Go's
2020-12-09 22:58:53 +00:00
[`filepath.Walk`](https://golang.org/pkg/path/filepath/#Walk) with a few exceptions:
2020-12-09 00:28:11 +00:00
- uses JavaScript Promises/async/await
2020-12-09 22:58:53 +00:00
- receives `dirent` rather than `lstat` (for performance, see `withFileStats`)
- optional parameters to change stat behavior and sort order
2020-12-09 00:28:11 +00:00
2020-12-09 22:58:53 +00:00
### walkFunc
2020-12-09 00:28:11 +00:00
Handles each directory entry
```js
2020-12-09 22:58:53 +00:00
async function walkFunc(err, pathname, dirent) {
2020-12-09 00:28:11 +00:00
// `err` is a file system stat error
// `pathname` is the full pathname, including the file name
// `dirent` is an fs.Dirent with a `name`, `isDirectory`, `isFile`, etc
return null;
}
```
2020-12-09 22:58:53 +00:00
## Walk.create(options)
2020-12-09 00:28:11 +00:00
Create a custom walker with these options:
2020-12-09 22:58:53 +00:00
- `withFileStats: true` walkFunc will receive fs.Stats[] from fs.lstat instead of fs.Dirent[]
- `sort: (entities) => entities.sort()` sort and/or filter entities before walking them
```js
const walk = Walk.create({
withFileStats: true,
sort: (entities) => entities.sort()),
});
```
## withFileStats
By default `walk` will use `fs.readdir(pathname, { withFileTypes: true })` which returns `fs.Dirent[]`,
which only has name and file type info, but is much faster when you don't need the complete `fs.Stats`.
Enable `withFileStats` to use get full `fs.Stats`. This will use `fs.readdir(pathname)` (returning `String[]`)
and then call `fs.lstat(pathname)` - including `mtime`, `birthtime`, `uid`, etc - right after.
```js
const walk = Walk.create({
withFileStats: true,
});
walk(".", async function (err, pathname, stat) {
console.log(stat.name, stat.uid, stat.birthtime, stat.isDirectory());
});
```
## sort (and filter)
Sometimes you want to give priority to walking certain directories first.
The `sort` option allows you to specify a funciton that modifies the `fs.Dirent[]` entities (default) or `String[]` filenames (`withFileStats: true`).
Since you must return the sorted array, you can also filter here if you'd prefer.
```js
const byNameWithoutDotFiles = (entities) => {
// sort by name
// filter dot files
return entities
.sort((a, b) => {
if (a.name > b.name) {
return 1;
}
if (a.name < b.name) {
return -1;
}
return 0;
})
.filter((ent) => !ent.name.startsWith("."));
};
const walk = Walk.create({ sort: byNameWithoutDotFiles });
walk(".", async function (err, pathname, stat) {
// each directories contents will be listed alphabetically
console.log(pathname);
});
```
Note: this gets the result of `fs.readdir()`. If `withFileStats` is `true` you will get a `String[]` of filenames - because this hapens BEFORE `fs.lstat()` is called - otherwise you will get `fs.Dirent[]`.
# node walk in 50 lines of code
If you're like me and you hate dependencies,
here's the bare minimum node fs walk function:
See [snippet.js](/snippet.js) or <https://therootcompany.com/blog/fs-walk-for-node-js/>.
# License
The main module, as published to NPM, is licensed the MPL-2.0.
The ~50 line snippet is licensed CC0-1.0 (Public Domain).