Node.js allows defining scripts in package.json. This feature often constitutes a major component of a developer’s workflow. For example, developers can define procedures such as building, testing, and deployment as scripts in package.json. If the project is intended to be cross-platform, the scripts should also be cross-platform.

In this post, we discuss guidelines and tips on how to create Node.js scripts that work on both Windows and POSIX systems (e.g., GNU/Linux, MacOS). The central tenet is using cross-platform Node.js features instead of POSIX/Windows-specific commands.

Use System Utility-Like npm Packages

POSIX/Windows-specific commands include cp and sed on a POSIX system, or copy and md on Windows. We should avoid using them directly. Instead, there are Node.js packages that provide command-line interface (CLI) utilities that perform what these system-level commands do. The table below contains an incomplete list of common POSIX/Windows commands and npm packages that perform similar tasks. All packages listed in the Node.js column are popularly used.

POSIXWindowsNode.js
cpcopyncp
mvren, renamemove-file-cli
rm, rmdirdel, rdrimraf
sed, awkN/Areplace-in-files-cli
cd, mkdir, echocd, mkdir, echoN/A

For example, let’s say we have a project that requires copying a file to the dist/ directory in the build step. Instead of using cp or copy in package.json like:

{
  "scripts": {
    "build": "cp raw-js.js dist/raw-js.js"
  }
}

use the ncp package (npm install --save-dev ncp) as listed in the table above:

{
  "scripts": {
    "build": "ncp raw-js.js dist/raw-js.js"
  }
}

The last row of the table lists two commands that are the same on POSIX systems and Windows. In this case, simply use them directly. However, there are subtle differences between them on different platforms; make sure you use them in a cross-platform way!

Use node --eval

Sometimes, we don’t have a popular npm package for some specific tasks, or we simply prefer not to introduce an extra dependency like the previous section. In some cases, we can use node --eval followed by cross-platform Node.js code.

For example, there is no portable way to create a directory recursively with mkdir. The POSIX command that recursively creates a directory, mkdir -p dir/subdir, would create two directories named -p and dir/subdir on Windows. In package.json, we can replace the POSIX command mkdir -p dir/subdir with

node --eval 'fs.mkdirSync("dir/subdir", {recursive: true})'

Save the above command as a script in the scripts field of package.json and use it instead of mkdir -p whenever we need to recursively create dir/subdir:

{
  "scripts": {
    "mkdir": "node --eval 'fs.mkdirSync(\"dir/subdir\", {recursive: true})'",
    "build": "npm run mkdir && echo done"
  }
}

Write Standalone Scripts for Complex Tasks

If the tasks in a script are complex, it is usually better to write a cross-platform standalone Node.js script and execute this script in the scripts field specified in package.json. The standalone script can use any cross-platform Node.js modules. Extracting complex logic into a standalone script also makes it easier to maintain and troubleshoot.

We can execute the script with the node executable or a TypeScript executor, such as ts-node and tsx. For example, if the standalone script is build.js that builds the package, we can add the following to package.json:

{
  "scripts": {
    "build": "node build.js"
  }
}

In case the script build.ts is written in TypeScript, after running npm install --save-dev tsx, we add:

{
  "scripts": {
    "build": "tsx build.ts"
  }
}

(Or use ts-node in place of tsx.)