Upcoming Node.js Features Improve Experience for CLI App Authors
Node.js will ship a few features in the current release line (v10.x; see release schedule) which may be of interest to those writing command-line applications. Let's take a closer look-see.
fs.readdir()
optionally outputs file types
fs.readdir()
is simple; it outputs a list of filenames. Of course, those filenames may represent directories, symbolic links, sockets, devices, hot dogs, etc.
If you want to know, say, which of those filenames represent directories, you will have to call fs.stat()
. This is ultimately kind of silly, because the underlying method in libuv actually provides this information; Node.js simply discards it.
Silliness is forbidden, so Bryan English created nodejs/node#22020 to address this malfeasance. fs.readdir()
, fs.readdirSync()
, and fs.promises.readdir()
(experimental) will now accept a new option, withFileTypes
. Use it like this:
// Reads contents of directory `/some/dir`, providing an `Array` of
// `fs.Dirent` objects (`entries`)
fs.readdir('/some/dir', { withFileTypes: true }, (err, entries) => {
if (err) throw err;
entries.filter((entry) => entry.isDirectory())
.forEach((entry) => {
console.log(`${entry.name} is a directory`);
});
});
// or
const entries = await fs.promises.readdir('/some/dir', {
withFileTypes: true
});
// or
const entries = fs.readdirSync('/some/dir', { withFileTypes: true });
Above, entries
is an array of fs.Dirent
objects, which are similar to fs.Stats
objects. These objects contain the same methods as fs.Stats
objects, but no other properties except name
. You cannot get information about file’s size from an fs.Dirent
object, for example.
The withFileTypes
feature provides a more performant and convenient API to work with if you need file type information after reading a directory. Since it’s optional behavior, it introduces no breaking changes.
fs.mkdir()
optionally creates directories recursively
This is mkdir -p
. Over the years of Node.js’ existence, the mkdirp
module and its ilk have courageously provided this functionality from userland. In fact, mkdirp
has become ubiquitous, leading some to wonder why it’s not part of the core API. This author wondered that as well!
The short answer is that Node.js has a “small core” philosophy. Whether you agree with that philosophy or not, we can all agree mkdirp
is a wildly popular module which provides a common filesystem operation. It’s proven its necessity, and that’s why Node.js merged the Benjamin Coe-created PR nodejs/node#21875.
fs.mkdir()
, fs.mkdirSync
and fs.promises.mkdir()
will now support the recursive
option. Use it like this:
// Creates /tmp/a/apple, regardless of whether `/tmp`
// and /tmp/a exist.
fs.mkdir('/tmp/a/apple', { recursive: true }, (err) => {
if (err) throw err;
});
// or
await fs.promises.mkdir('/tmp/a/apple', { recursive: true });
// or
fs.mkdirSync('/tmp/a/apple', { recursive: true });
Note: A recursive fs.mkdir()
is not an atomic operation. It’s…unlikely…to fail halfway through, but it could.
Another Note: There's an open PR concerning how to support feature detection here.
process.allowedNodeEnvironmentFlags
: Queryable & Iterable Flags
NODE_OPTIONS
is an environment variable supported as of Node.js v8.0.0. If present, it works just like flags passed to the node
executable. The flags allowed in NODE_OPTIONS
have a unique property: they do not fundamentally alter the default behavior of the node
executable. What does that mean, exactly?
Flags which don’t fundamentally alter node
’s behavior retain two properties:
- Executing
node --some-flag
will open a REPL - Executing
node --some-flag file.js
will runfile.js
A handful of flags are excluded fromNODE_OPTIONS
, such as--preserve-symlinks
, due to security concerns.
This means flags like --help
, --version
, --check
, etc., aren’t supported by NODE_OPTIONS
.
Now, if you’re using node
with flags in production, you’re probably using flags supported by NODE_OPTIONS
. And you probably have some tests. Assuming you do have tests, if you’re using a test runner which wraps the node
executable (such as Mocha), you will need to pass those same flags to the test runner’s executable. For example:
# production app
$ node --experimental-modules ./app.js
# test runner
$ mocha --experimental-modules "test/**/*.spec.js"
That’s a nice user experience, and it’s fine and good as long as the test runner supports the flags you want to use. But it also means the test runner must add support for any given flag in NODE_OPTIONS
and pass it along to Node.js. This is more manual labor to maintain than you’d expect; many flags are considered “temporary,” and only some flags support swapping any dash (-
) for an underscore (_
) or vice-versa.
This author created PR nodejs/node#19335 which adds process.allowedNodeEnvironmentFlags
. Using this, test runners and other CLI apps needing to wrap the node
executable won’t need to manually track new flags as they are added to (and removed from) Node.js.
To detect whether or not a flag as provided in process.argv
is a NODE_OPTIONS
flag—and thus a useful one, for our purposes—we can do this:
// cli.js
const command = ['cli2'];
process.argv.slice(2).forEach((arg) => {
if (process.allowedNodeEnvironmentFlags.has(arg)) {
command.unshift(arg);
} else {
command.push(arg);
}
});
command.unshift(process.execPath);
// `command` looks like:
// ['node', '--node-flag', 'cli2', '--cli2-flag']
process.allowedNodeEnvironmentFlags
is a Set-like object. It can't be mutated—add()
, delete()
and clear()
operations will silently fail—and its has()
method will return true
for any allowed permutation of a NODE_OPTIONS
flag. For example:
process.allowedNodeEnvironmentFlags.has('--stack-trace-limit') // true
process.allowedNodeEnvironmentFlags.has('--stack_trace_limit') // true
process.allowedNodeEnvironmentFlags.has('--stack_trace-limit') // true
has()
will also return true
for a disallowed (but convenient) permutation: omitted leading dashes. This means:
process.allowedNodeEnvironmentFlags.has('--experimental-modules') // true
process.allowedNodeEnvironmentFlags.has('experimental-modules') // true
// careful with your underscores!
process.allowedNodeEnvironmentFlags.has('--experimental_modules') // false
Since it’s a Set
-like object, you can iterate over it using forEach
and other typical methods:
process.allowedNodeEnvironmentFlags.forEach((flag) => {
// --enable-fips
// --experimental-modules
// --experimental-repl-await
// etc.
});
Only the canonical format (as shown in node --help
) of each flag will appear when iterated over; it won’t contain any duplicates.
This addition enables tooling authors to more easily wrap the node
executable, and provides runtime insight into the nature of flags in process.argv
.
Horn-Tooting
If you’re interested in helping make Node.js a better experience for authors of command-line tools, click on these links, dammit:
- Join our new tooling group!
- Join this Slack for Node.js tooling authors!!!
- Participate in a Node.js user feedback tooling group session!!!!!!!