VermontDepartmentOfHealth / docgov

some public facing standards, guidelines, and, well, documentation
https://docgov.netlify.app/
MIT License
3 stars 3 forks source link

make npm scripts work cross platform #89

Closed KyleMit closed 4 years ago

KyleMit commented 4 years ago

Ideally, the commands would just work on either linux or windows (most work is done against windows, but netlify builds on a linux machine)

Here are some examples of scripts used in the package.json:

# build 
set ELEVENTY_ENV=prod&& npx @11ty/eleventy

# serve
set ELEVENTY_ENV=dev&& npx @11ty/eleventy --serve

# clean
rm -rf _site

Problems

References

KyleMit commented 4 years ago

TLDR; Cross Platform Scripts Updates

"scripts": {
-    "build": "set ELEVENTY_ENV=prod&& npx @11ty/eleventy",
-    "serve": "set ELEVENTY_ENV=dev&& npx @11ty/eleventy --serve",
-    "clean": "rm -rf _site"
+    "build": "npx cross-env ELEVENTY_ENV=prod npx @11ty/eleventy",
+    "serve": "npx cross-env ELEVENTY_ENV=dev  npx @11ty/eleventy --serve",
+    "clean": "npx rimraf _site"
}

And if you don't want to use npx to invoke cross-env and rimraf, you can also install globally:

npm i rimraf -g
npm i cross-env -g

The Problem

The fundamental challenge is that whatever string ends up in the script section will get sent directly to whatever shell you've selected. Which means for true cross compatibility, the only safe scripts require syntaxes and commands that perfectly overlap in both cmd and bash - which is a small venn diagram.

Here's an outline of some of the commands we'd like to run and several different implementations across environments to see if we can find something that works universally.

1. Setting Environment Variables

BASH / Linux

MY_NAME=Kyle echo Hi, \'$MY_NAME\'
MY_NAME=Kyle && echo Hi, \'$MY_NAME\'

CMD / Windows

set MY_NAME=Kyle&& echo Hi '%MY_NAME%'

Note 1: You cannot have any space* between the value being set and the && command continuation character

*Note 2: For this particular example, the variable expansion will occur immediately, so in order to set and use the variable in the same line you'll need to wrap in cmd evaluation, pass in /V for delayed environment variable expansion, and delimit with ! instead of $. But it should be fine to consume the variable in a subsequent command.

Node.js

Attach directly to process.env

process.env.MY_NAME = 'Kyle';

.ENV file

You can create a file named .env

MY_NAME=Kyle

And then load into the process with a library like dotEnv like this

require('dotenv').config()

Build Machine / Netlify

You might also set some environment variables on your build server settings like in Netlify Environment Variables:

image

2. Delete Files / Folder

BASH / Linux

rm with -f force and -r recursive

rm -rf _site

CMD / Windows

The windows equivalent is rd remove directory with /s to delete all subfolders and /q to not prompt for y/n confirmation

rd /s /q _site

Node.js

Use fs.rmdir to remove entire directory as of Node v12.10.0

const fs = require('fs');
fs.rmdir("_site", { recursive: true });

CLI / NPX

RimRaf is a recursive delete package that has a CLI which can be brought in using npx

npx rimraf _site

3. Running Multiple Commands

BASH / Linux

How to run multiple bash commands - Docs on list of commands / control operators

Syntax Description
A ; B Run A and then run B
A && B Run A, if it succeeds then run B
A \|\| B Run A, if it fails then run B
echo hey && echo you

CMD / Windows

How to run two commands in one cmd line - Docs on redirection and conditional processing symbols

Syntax Description
A & B Run A and then run B
A && B Run A, if it succeeds then run B
A \|\| B Run A, if it fails then run B
echo hey && echo you

NPM

You can Run npm scripts sequentially or run npm scripts in parallel with a library called npm-run-all

If you had the following scripts in your package.json:

"scripts": {
    "lint":  "eslint src",
    "build": "babel src -o lib"
}

You could run them all like this:

$ npm-run-all -s lint build # sequentially
$ npm-run-all -p lint build # in parallel

Possible Solutions

Run Script

One escape hatch seems to be to to invoke a script contained in a node.js file - because node is a prerequisite anyway and is cross-OS compatible. However, this also seems pretty heavy handed for a 1 line function to delete a directory. This is similar to the approach taken by Create React App with react-scripts, but they have much more complexity to handle.

Another challenge to this approach is when calling CLI / binary commands like eleventy --serve. To do that we need to execute a command line binary with Node.js and also pipe the response to stdout

So it could work a little something like this:

File: Package.json:

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

File: Build.js

let { exec } = require('child_process');
let log = (err, stdout, stderr) => console.log(stdout)

exec("git config --global user.name", log); 

If an OS agnostic node command wouldn't work, you can also use require("os") to get "operating system-related utility methods and properties"

Environment Specific Script Names

As outlined in package.json with OS specific script, there's a library called run-script-os that helps automagically toggle between named script versions

So if we had the following scripts:

"scripts": {
    "build": "npx run-script-os",
    "build:windows": "SET ELEVENTY_ENV=prod&& npx @11ty/eleventy",
    "build:default": "export ELEVENTY_ENV=prod && npx @11ty/eleventy"
}

You could manually run a named instance:

$ npm run build:windows

Or you could let run-script-os decide for you:

$ npm run build

Use NPM CLI packages

From How to set environment variables in a cross-platform way, there's a package by Kent C. Dodds called cross-env that allows you to set environment variables across multiple platforms like this:

"scripts": {
    "build": "npx cross-env ELEVENTY_ENV=prod npx @11ty/eleventy",
}

At the potential cost of additional dependencies (none of which are felt by the end client), this probably gives us a solution that is both robust & terse as long as a CLI exists for what you're trying to do

!Spoiler! There's probably an NPM package for it