Joshua's Docs - NodeJS General Notes

Resources

What & Link Type
nodejs.org Official website
--> Docs Official Docs
node.green Compatibility table for NodeJS versions & JS versions

Upgrading Node itself version

  • On Unix, macOS
    • My favorite option is managing node versions through asdf, since it can be used for managing other runtimes as well
    • Another option is to use "N"
      # Just needed once
      npm install -g n
      # Now upgrade
      sudo n stable
  • On windows
    • You can download new installer (MSI) and just install over current
    • You can now use "NVM" (node version manager) for both Unix and Windows

NVM

Listing Versions

node -v

Note: Node also has "ABI" version number. regular version number is like 10.##.# (LTS or non-lts). ABI version is regular integer

node -p "process.versions.modules"

Or to get all version info:

node -p "process.versions"

Copying to the Clipboard

On Windows, here is a one-liner you can use:

require('child_process').spawn('clip').stdin.end(formattedHtml);

In general, you are probably better off using the cross-OS package clipboardy.

Paths, paths, paths

Current directory

Often in Node scripts, you will need to reference something either by absolute or relative path, which might require knowing the full path of where the script is running.

  • __dirname is a magic global that holds the string path of where the script resides - regardless from where it got called
    • Does not have trailing slash
    • ESM equivalent (S/O):
      import { dirname } from 'path';
      import { fileURLToPath } from 'url';
      
      const __dirname = dirname(fileURLToPath(import.meta.url));
  • process.cwd() returns the absolute path from where you invoked the script process - e.g. where you ran your command from

Normalizing paths (POSIX vs non-POSIX, aka Windows)

There is a really great native library for Node called path which has all kinds of methods for cleaning up and parsing paths. For example, if I want to create a path based on current directory, and then normalize it because I'm not sure which OS it is going to be running on, I might use something like this:

const path = require('path');
const myFilePath = path.normalize(__dirname + '../subdir/myFile.js');

However, that only normalizes it for the OS you are on. If you want to force a standard, across any env, that takes more work. Quick hackish example:

currFilePath = currFilePath.replace(/[\/\\]{1,2}/gm,'/');

Read more about path, here

Get list of node flags

node --help

Or here.

Using the Node CLI / REPL

You can use the -e argument to evaluate (eval) raw code string, or -p to both evaluate and "print" the output.

node -p "Math.min(24,2);"
  • Results in "2" being printed to console.

You can use the -i or --interactive flag to force node into REPL mode, even if stdin is not a terminal.

  • This is important to note if you are trying to do something like recursively call node from within node via child_process or something like that (without the flag, it won't flush stdout until stdin is explicitly closed via .end()).

Running a CLI command / system / bash / shell commands from within a node script

Recommended way is with child_process.exec.

const childProc = require('child_process');
var lsResults = childProc.execSync('ls');
// Note - exec returns buffer, so need to convert
var lsResultsString = lsResults.toString();

Calling Node From Node Via Child_Process

This is a pretty specific scenario, but if you end up trying to call node from within node, via exec or spawn:

  • You might want to call it with --interactive to force interactive mode (or call stdin.end() to flush at the end)
  • You will need to send EOL characters after sending input to the CLI
  • You might want to read this comment summarizing some approaches

Changing directory

If you want to use cd, you need to use it with the command you want to run at the same time - e.g. a single line input to child_process.exec. Otherwise, the change of directory will not persist between commands. For example:

const childProc = require('child_process');

// Right:
childProc.execSync('cd foo && ls');

// Wrong:
chldProc.execSync('cd foo');
childProc.execSync('ls');

A recommended alternative is to pass the directory you want to execute the command in through the cwd (working directory) option:

const childProc = require('child_process');
childProc.execSync('ls', {
	cwd: 'foo'
});

Receiving CLI arguments

Access through process.argv. It should follow the following syntax:

if (Array.isArray(process.argv)){
	// 0 = path to nodejs - {string}
	let nodePath = process.argv[0];
	// 1 = path to current executing file - {string}
	let currFile = process.argv[1];
	// 2, 3, etc. = arguments
	let argsArr = process.argv.slice(2);
}

CLI Argument Parsing

It's generally best to defer to a argument parsing library instead of rolling your own parsing code.

I'm partial to libraries that support strong static-typing and type-inference, as that solves a lot of headaches around building a CLI app. Some example libraries that fit into this category are privatenumber/type-flag and Schniz/cmd-ts.

Starting with Node 18.3, there is now a built-in tool for arg parsing - parseArgs from node:util. It is not as full-featured as a dedicated CLI library like cmd-ts, but is good enough to work for quite a few more simple use-cases.

For more details, see my other cheatsheet section - CLI Framework Libraries

Detecting when a script / file is being run via CLI

There is a really handy trick for, in your code, to detect if it is being run directly versus imported by other code. You can use:

if (require.main === module) {
	// This code will only execute if the file is called *directly* (e.g. via CLI)
}

Or, for ESM code

import esMain from 'es-main';

if (esMain(import.meta)) {
  // This code will only execute if the file is called *directly* (e.g. via CLI)
}

Also, see "Accessing the main module"

Read in package.json within script file

const packageInfo = require('./package.json');
console.log('Version = ' + packageInfo.version);

Also see "JS Modules - How to Import JSON?"

For most robust approach, stick to filesystem methods. fs-extra even has a readJson method.

Setting globals

There are not many good reasons to do this, but if for some reason you need to set a true global (not as in file global or top of closure global, but as in pollutes every file once imported), here is how:

global.findMyAnywhere = 'Hello';
// Or...
globalThis.findMyAnywhere = 'Hello';

Details

If you want TypeScript to understand the type of these globals across your codebase, you can follow the steps outlined here

Console EOF / process.stdin.on('end')

On Unix, pressing CTRL+D usually results in the system returning an EOF (end-of-file) to whatever is listening to the terminal. In node, you often see this listened to as:

process.stdin.on('end', ()=>{
	// Do something
}

However, this flat out does not work on Windows. Futhermore, pressing CTRL+C, since it sends the exit command, does not give that listener a chance to execute. The workaround is to use the process signal event SIGINT listener:

// Redirect windows CTRL+C to stdin-end
process.on('SIGINT',function(){
	// Do whatever you want here - finish up stuff, etc.
	// ...
	// Emit EOF / end event
	// https://nodejs.org/api/stream.html#stream_event_end
	process.stdin.emit('end');
});

Debugging, profiling, etc.

This is a great answer on S/O that lists a bunch of commonly used tools for debugging, profiling, and more.

Debugging

Call node --inspect-brk {nodeScriptFilePath} {args}

node --inspect is the same as above, but without breaking first

If you can't pass flags directly to node, you can also set them via a NODE_OPTIONS environment variable - e.g., NODE_OPTIONS="--inspect-brk" {MY_CMD}

Make sure you either have auto-attach on in your IDE settings, or start a debug session before running.

If you are using VSCode, you can also manually attach to running NodeJS processes, by using Command Palette -> Attach to Node Process

Profiling

For a plug-n-play solution for analyzing performance, check out 0x, or flamebearer.

Clinic.js is a nifty suite of profiling tools,, all wrapped into one easy to install and use package.

Throwing errors

The recommended way to throw errors in Node is with the explicit error constructor.

throw new Error('Computer says "no"...');

Good read: Flavio Copes - Node Exceptions


Environment Variables

Reading values

From your CLI, the fastest way to view your environment variables is node -p process.env.

From within code that is running with NodeJS, you can easily access any environment variable by picking it off the process global variable object. It is super common to use this as a way to avoid putting credentials in code, like so:

const myApiClient = new ApiClient({
	id: process.env.API_CLIENT_ID,
	superSecretPass: process.env.API_CLIENT_PASS
});

Setting values

There are multiple ways to set env values, with varying levels of setup required.

From the CLI

Since process.env is basically just a map of your OS's environment variables, setting values for it depends on your OS and even what CLI you use:

  • BASH, or "bash-like" CLIs: {KEY}={VAL}
  • Windows CMD: SETX {KEY} "{VAL}" (or, temporary, SET instead of SETX)
    • Example: SETX API_KEY "123"&& node -p process.env.API_KEY ---> results in 123 printed to console
    • Example SET port=3001 && node server.js
  • Windows PowerShell: $env:{KEY}="{VAL}"
  • NodeJS REPL: node -e "process.env.{KEY} = {VAL}"

For all of the above options, be careful about quotes / escaping.

From a .env File

Since it can get tedious setting and checking variables from the command line, most devs prefer to keep these values stored in a file, and have Node read the values out when executing. This also has the added benefit of keeping those values out of your OS variables.

However, unlike how it reads OS variables, mapping values from a file to process.env is not baked into Node, so you will need to use a dependency to add that ability. The most popular is probably dotenv.

You can read the docs for how to use it, but its pretty simple:

  • Add a .env file to the root of your project, with key pair values
    • These shoule be written as KEY=VAL
  • Run npm install dotenv - to add it as a dependency
  • Add require('dotenv').config() as early as possible in your code, which will cause dotenv to map the contents of the file to process.env
    • After this point, process.env.MY_KEY will contain the value defined in .env if you have the pair MY_KEY={something} in the file

WARNING: Be careful about sharing your .env file. If it contains "secrets" (API keys, credentials, etc.) you probably want to add it to your .gitignore, and create a example.env which contains the same keys as .env, but with empty values for a dev to fill in with their own credentials.

From within Code

From within your code that is running on Node, you can override existing values, or set new ones, simply by treating process.env as a regular object. For example:

process.env.API_KEY = 'ABC123';

Note: this only sets the value for Node's process and any child processes; this doesn't actually change your OS environment variable value after the process exits! See responses and linked answers to this StackOverflow for details.

Node.js - Parallelization Options / Concurrency Options

This is a really good rundown of the state of parallelization techniques in Node.js and some useful libraries - https://snyk.io/blog/node-js-multithreading-worker-threads-pros-cons/

In Node.js, you have three primary built-in options for parallelization:

  • Clusters (multiprocessing)
    • True multi-processing
  • Manual use of child_process
  • Worker Threads (multi-threading)

You also have the option of just wrapping your entire app in a multi-process spawner / manager, like PM2, if it is already amenable to being run in multiple parallel instances (example results).

Node.js - Parallel Processing Libraries / Abstractions

Library Worker Threads Cluster / Multiprocess Includes Type Definitions
poolifier
workerpool ✅ 🍴
tinypool
piscina
synckit
throng
threads.js

🍴 = Using child_process instead of cluster

Cross-Platform Development

Detecting Platform

You can use os.platform() or process.platform to get the platform string (e.g., win32, darwin, etc.). These are functionally equivalent.

Markdown Source Last Updated:
Sun Sep 15 2024 01:54:05 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Sat Oct 24 2020 19:34:44 GMT+0000 (Coordinated Universal Time)
© 2024 Joshua Tzucker, Built with Gatsby
Feedback