Wed Dec 28 2016

A year or two ago, front end developers had this sudden realization: task runners like Grunt and Gulp are just wrappers for existing CLIs, and using those CLIs directly is actually easier than trying to configure a task runner. Since then, the trend is to call your build tasks using npm scripts defined in your package.json.

I love removing dependencies from my project, so I was sold on building with npm scripts right away. I was a tad disappointed, though, when I saw that there isn't built-in support for watching files for changes. If you're lucky enough to be using only dependencies that have their own watch support, that's great, but otherwise you'll need to come up with a file watching solution. I've tried npm-watch, watch, and nodemon. npm-watch has the best interface, but uses nodemon under the hood, which is crazy heavy and not built for this purpose. Watch is okay but requires an additional plugin like npm-run-all to parallelize multiple watchers (i.e. for separate JS and CSS build processes).

So I felt like there's unnecessary complexity around this file watching problem, and decided to see if I could just solve the problem myself using node's built-in fs.watch method. Turns out it's crazy easy, and took about 10 minutes to switch from npm-watch to a custom watcher.

package.json


...
"scripts": {
  "build": "./bin/build.sh",
  "scss": "./bin/css/stylelint.sh; ./bin/css/sass.sh; ./bin/css/autoprefix.sh",
  "js": "node bin/js/fusebox.js",
  "watch": "node bin/watch.js"
},
...

So I have a few npm scripts - one to build the whole project for deployment, one that lints and builds scss files, and one that just bundles my JavaScript files. Then there's the one that watches files for changes. Here's what that script looks like:

bin/watch.js


const fs = require('fs'),
    spawn = require('child_process').spawn;

fs.watch('src', {
    recursive: true // watch everything in the directory
}, (e, file) => {
    // Use the extension of the file as the npm script name
    const script = file.split('.').pop();

    if (['js', 'scss'].includes(script)) {
        // Spawn the process
        const p = spawn('npm', ['run', script], {
            stdio: 'inherit' // pipe output to the console
        });
        // Print something when the process completes
        p.on('close', code => {
            if (code === 1) {
                console.error(`✖ "npm run ${script}" failed.`);
            } else {
                console.log('watching for changes...');
            }
        });
    }
});

console.log('watching for changes...');

And that's it. Now I can run npm run watch, and then my css build runs when I change an scss file, and my js build runs when I change a JavaScript file. Admittedly, my implementation is a little tricky because it uses the extension of the changed file as the npm script name, but you could certainly use a switch statement to call other scripts based on what files were changed.