Incredibly easy NPM only build step

Advantages of npm as a build tool

The biggest advantage for me is that you have one less dependency chain. You don’t need to download and learn a build tool. With npm, which you already use, you use simple cli commands or node scripts.

The downsides of npm

Npm scripts are not quite as plug and play as gulp or grunt. Tutorial for the typical use cases make the entrance into a build tool easy. Only when you have more advanced requirements, will it become a hassle.
As of now there are far viewer tutorials and recipes on npm build steps compared to other build tools.

  • The pipe (|) streams one commands output to the next commands input
  • The redirection (>) operator redirects the output to a file

Let’s build it

All our work will be within package.json. We will accomplish the following:

  • running a node server that watches for file changes
  • compile sass to css and revision the files
  • compile typescript to js and revision files
  • move some node_modules into the public folder
  • watch for changes in *.scss and *.ts files and recompile

Running a node server

I am currently using supervisor for development because it is dead simple. You could easily replace it with ts-node, forever, nodemon or any other server.

// package.json"scripts": {
"supervisor": "supervisor -w app,resources/templates --extensions node,js,hbs app.js"
}

Building our css

We want to compile Sass into css, remove old compile files and revision the new files. If we change our Sass files we want it to be recompiled. We will split the steps into individual scripts and compose them together. This makes it easier to read and reason about.

Compiling Sass files

All we need to install is the node-sass package. In my case the app.scss file that loads all other files is in resources/css/. The converted app.css file is saved to public/css/ as well as the source map. Check out the github project page to understand all flags and options.

"scripts": {
…,
"css:compile": "node-sass --source-map public/css/app.css.map --output-style compressed -o public/css/ resources/css/app.scss"
}

Removing old files

While it is possible to check which files changed and only update those, I opted for the simple option. With the speed at which Sass compiles there is no reason to invest any time in micro optimisations. This means we can remove all css files with a simple `rm`.

// package.json"scripts": {
…,
"css:clean": "rm -f public/css/*"

Revisioning files

I actually did not find any module that does revisioning the way I wanted it. Luckily with node it was easy to write the Node-file-rev module myself. After installing it, you provide the file(s) to the script and it creates the revisioned file as well as the manifest. You can specify the manifest directory and name with the --manifest flag. The --root flag allows you to specify the root directory to remove in the manifest (e.g. public/css/app.csscss/app.css).

// package.json"scripts": {
…,
"css:rev": "node-file-rev public/css/app.css --manifest=public/rev-manifest.json --root=public/"

Composing CSS scripts together

A new build:css script combines the clean, compile and rev script using the && operator. With a && the next command will only execute if the first command exits with 0, meaning it was successful.

// package.json"scripts": {
…,
"css:clean": "rm -f public/css/*",
"css:compile": "node-sass --source-map public/css/app.css.map --output-style compressed -o public/css/ resources/css/app.scss",
"css:rev": "node-file-rev public/css/app.css --manifest=public/rev-manifest.json --root=public/",
"build:css": "npm run css:clean && npm run css:compile && npm run css:rev",
"build:css:watch": "onchange -i 'resources/css/*.scss' 'resources/css/*/*.scss' -- npm run build:css"

Building our Javascript

To compile our Typescript files we need to replicate the same logic as we had for out Sass. We also want to move some files from node_modules into public/js.

Compiling Typescript files

I am using rollup, but you could use webpack or any other tool you need to convert your files. There is definitely a cli version available. Just add another script to the package.json named js:compile and run the needed cli call, for rollup it is rollup --config.

Removing old files & moving files

We use the same rm script to remove old javascript files.
The files we want to move are store in a variable in a config section of the package.json called moveFilesJs. You have to use a space separated list because arrays are not supported. In your script you can reference the files by using $npm_package_config_moveFilesJs. With this in place we can run a simple copy cp command.

// package.json"config": {
"moveFilesJs": "node_modules/@webcomponents/webcomponentsjs/bundles/webcomponents-sd-ce.js node_modules/@webcomponents/webcomponentsjs/bundles/webcomponents-sd-ce.js.map node_modules/fetch-inject/dist/fetch-inject.min.js"
},
"scripts": {
…,
"js:clean": "rm -f public/js/*",
"js:move": "cp $npm_package_config_moveFilesJs public/js/"

Revisioning files

For the revisioning we can use the same Node-file-rev module and replace the path. Instead of defining a single file, we define a “glob” by using public/js/*.js. This will revision all javascript files in the folder.

// package.json"scripts": {
…,
"js:rev": "node-file-rev public/js/*.js --manifest=public/rev-manifest.json --root=public/"

Composing JS scripts together

Like with the css, a build:js script combines the clean, compile, rev and move script using the && operator.

// package.json"scripts": {
…,
"build:js:watch": "onchange -i 'resources/js/*.ts' 'resources/ts/*/*.ts' -- npm run build:js",
"build:js": "npm run js:clean && npm run js:compile && npm run js:rev && npm run js:move",
"js:clean": "rm -f public/js/*",
"js:move": "cp $npm_package_config_moveFilesJs public/js/",
"js:compile": "rollup --config",
"js:rev": "node-file-rev public/js/*.js --manifest=public/rev-manifest.json --root=public/"

Composing it all together

Finally we can combine both build scripts into a single one called build. By combining build and supervisor into the start script we can run everything with npm start.

// package.json"scripts": {
…,
"build": "npm run build:js:watch & npm run build:css:watch",
"start": "node_modules/.bin/ttab -t 'Node Server' 'npm run supervisor' & node_modules/.bin/ttab -t 'Building assets' 'npm run build'"
}

Summary

Replacing a build tool like gulp with npm scripts is not as hard as it seems. While it can be a bit more inconvenient, it makes you less dependent on plugin authors. Scripts are less connected which lets you replace one part without touching the rest.
You can always write a node or bash script to do what you need and execute it via npm.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Lukas Oppermann

Lukas Oppermann

Product designer with a love for complex problems & data. Everything I post on Medium is a copy — the originals are on my own website: https://www.vea.re