Moving NPM scripts to JavaScript
Let's explore an alternative to NPM scripts, using plain JavaScript.
The initial NPM scripts
These are the package.json
scripts we're going to work with:
"scripts" : {
"start": "run-p dev:*",
"dev:server": "http-server",
"dev:wds": "webpack-dev-server",
"clean": "rimraf dist",
"lint": "eslint src",
"test": "jest",
"check-all": "run-s clean lint test",
"build": "webpack -p",
"upload": "upload-somewhere",
"deploy": "run-s check-all build upload"
}
The new NPM scripts
Let's create a run.js
file at the root of our project, and update package.json
with:
"scripts" : {
"start": "node run start",
"dev:server": "node run dev:server",
"dev:wds": "node run dev:wds",
"clean": "node run clean",
"lint": "node run lint",
"test": "node run test",
"check-all": "node run check-all",
"build": "node run build",
"upload": "node run upload",
"deploy": "node run deploy"
}
Here, we are simply using the node
binary to run the run.js
file with the additional argument of the script name.
spawnSync
In order to run a command in run.js
, and have its output streamed to the shell in real-time like a normal NPM Script, we need to use the native Node function spawnSync
from child_process
, with { shell: true, stdio: 'inherit' }
. Let's create a run
function at the top of our run.js
file:
const { spawnSync } = require('child_process')
const run = cmd => spawnSync(cmd, { shell: true, stdio: 'inherit' })
Feel free to add a console.log()
for a better experience:
const { spawnSync } = require('child_process')
const run = cmd => {
console.log(`\x1b[35mRunning\x1b[0m: ${cmd}`)
return spawnSync(cmd, { shell: true, stdio: 'inherit' })
}
Note: \x1b[35m
and \x1b[0m
are shell color symbols.
Declaring the scripts
Now, using a plain JavaScript object, we can bring back our scripts:
const scripts = {
// Dev
start: 'run-p dev:*',
'dev:server': 'http-server',
'dev:wds': 'webpack-dev-server',
// Code quality
clean: 'rimraf dist',
lint: 'eslint src',
test: 'jest',
'check-all': 'run-s clean lint test',
// Deployment
build: 'webpack -p',
upload: 'upload-somewhere',
deploy: 'run-s check-all build upload',
}
Note: We can use comments and newlines to organize our scripts. Mind-blowing.
Finally, when this file is executed, we want to launch the script name that corresponds to the second argument of the node
command, and exit with the right error code:
process.exitCode = run(scripts[process.argv[2]]).status
Packages and functions
Note that unlike NPM Scripts, we can use any NPM package, like dotenv to load environment variables. We can also construct commands with functions:
require('dotenv/config')
const httpServer = port => `http-server ${port ? `-p ${port}` : ''}`
httpServer(process.env.DEV_PORT) // 'http-server -p [your port from .env]'
Also, in this article, we are only using CLI binaries, but by using JavaScript, we can use programmatic Node APIs too if your packages offer that.
Complete code
And here is the final code of our run.js
file:
const { spawnSync } = require('child_process')
const run = cmd => {
console.log(`\x1b[35mRunning\x1b[0m: ${cmd}`)
return spawnSync(cmd, { shell: true, stdio: 'inherit' })
}
const scripts = {
// Dev
start: 'run-p dev:*',
'dev:server': 'http-server',
'dev:wds': 'webpack-dev-server',
// Code quality
clean: 'rimraf dist',
lint: 'eslint src',
test: 'jest',
'check-all': 'run-s clean lint test',
// Deployment
build: 'webpack -p',
upload: 'upload-somewhere',
deploy: 'run-s check-all build upload',
}
process.exitCode = run(scripts[process.argv[2]]).status