fs.walk for Node.js in 2021

Many years ago I wrote npmjs.com/package/walk as a loose port of Python’s os.walk, but using node’s EventEmitter for flair - because that seemed like the thing to do at the time (the node v0.x days).

However, times have changed. Since Node v10.10 fs.readdir(pathname, { withFileTypes: true }) is now a much nicer way to read directories and… no one actually writes JavaScript anymore (being that the new “ECMAScript” language has quite literally replaced every aspect of the language - from variable declarations to for loops and beyond).

A Modern fs.walk() for Node.js in (ES) 2021

I wanted a modern, no bloat, no dependency alternative to the original walk, so today I wrote @root/walk as a more direct port of Go’s filepath.walk.

npm install --save @root/walk@latest

ECMAScript (ES2021) Usage:

import Walk from '@root/walk';
import path from 'path';

await Walk.walk('./', async (err, pathname, dirent) => {
    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);
        return;
    }

    // return false to skip a directory
    // (ex: skipping "dot files")
    if (dirent.isDirectory() && dirent.name.startsWith('.')) {
        return false;
    }

    // fs.Dirent is a slimmed-down, faster version of fs.Stat
    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());
});

console.log('Done');

VanillaJS (ES5) Usage:

var Walk = require('@root/walk');
var path = require('path');

Walk.walk('./', function walkFunc(err, pathname, dirent) {
    if (err) {
        throw err;
    }

    if (dirent.isDirectory() && dirent.name.startsWith('.')) {
        return Promise.resolve(false);
    }
    console.log('name:', dirent.name, 'in', path.dirname(pathname));

    return Promise.resolve();
}).then(function () {
    console.log('Done');
});

Walk in < 50 Lines

If you’re like me and hate dependencies, this is almost exactly the same as what’s in the npm package, but without any options and in less than 50 lines of code.

// a port of Go's filepath.Walk
async function walk(pathname, walkFunc, dirent) {
    const fs = require('fs').promises;
    const path = require('path');
    const _pass = (err) => err;

    let err;

    // special case: walk the very first file or folder
    if (!dirent) {
        let filename = path.basename(path.resolve(pathname));
        dirent = await fs.lstat(pathname).catch(_pass);
        if (dirent instanceof Error) {
            err = dirent;
        } else {
            dirent.name = filename;
        }
    }

    // run the user-supplied function and either skip, bail, or continue
    err = await walkFunc(err, pathname, dirent).catch(_pass);
    if (false === err) {
        // walkFunc can return false to skip
        return;
    }
    if (err instanceof Error) {
        // if walkFunc throws, we throw
        throw err;
    }

    // "walk does not follow symbolic links"
    // (doing so could cause infinite loops)
    if (!dirent.isDirectory()) {
        return;
    }
    let result = await fs
        .readdir(pathname, { withFileTypes: true })
        .catch(_pass);
    if (result instanceof Error) {
        // notify on directory read error
        return walkFunc(result, pathname, dirent);
    }
    for (let entity of result) {
        await walk(path.join(pathname, entity.name), walkFunc, entity);
    }
}

Copy/Paste Snippet’s License

I’ve licensed the ~50 line snippet above as Creative Commons Zero v1.0 Universal (CC0-1.0), which is simply a formal, legally recognized and SPDX-compatible way of releasing it into the Public Domain.