Using NPM Scripts to Run Node-Sass
The way most front-end developers have been doing this in recent times is with tools like Grunt and Gulp — both of which offer to solve the same problem but in slightly different flavours. It’s chocolate or strawberry. But maybe it’s time to reconsider plain old vanilla.
Both Grunt and Gulp are built on Node.js and are available on NPM — the package manager for Node.js. Once installed, Grunt and Gulp have a slew of their own NPM modules available to accomplish various tasks.
But which one to use?
The short answer is…neither. Let’s backtrack for a moment. As of writing, the current version of Sass is 3.4.21. The most common compiler of Sass is LibSass, which is written in C++ as opposed to Ruby, which the original Sass compiler is built upon. LibSass is many times quicker than Ruby Sass, in fact more often than not the Ruby compiler is too slow for large projects, making LibSass the only viable option.
Fortunately for us, both Grunt-Sass and Gulp-Sass use Node-Sass, a Node.js Sass compiler, which uses LibSass (confused yet?).
Let’s visualise this for a moment.
By the time we get round to using Grunt/Gulp Sass we are already 3 levels deep in terms of dependencies. If Sass ever gets an update and we are using Grunt or Gulp, we must first wait for LibSass to receive the update, then wait for Node-Sass to receive LibSass’s update, and then finally wait for our Grunt/Gulp wrapper to receive the update. At any given time this could mean the version of Sass available to you and your project is lagging behind what’s actually possible.
If, like me, you are someone who enjoys experimenting with bleeding edge features and technologies, this just isn’t good enough. This foresight isn’t just theoretical speculation that could happen, it is very real. As of writing, my app will compile with the latest release of Node-Sass, but not the respective Grunt-Sass wrapper. Why? Because the last release of Node-Sass was 2 days ago (and prior to that, 7 days ago). The last release of Grunt-Sass was last year, and comes with a version of Sass incompatible with my project’s code.
This leaves me with 3 choices:
- I simply don’t use bleeding edge features
- I hold off all work and wait until I can use bleeding edge features
- I remove a layer of dependency and use bleeding edge features today
I could go with option 1 or 2 and call it a day, and leave you to read the next article, or I can continue to exploit the various tools available to me until I get what I want.
So how would we go about this? It’s probably a lot easier than you might think.
Setting up a project to use pure NPM scripts instead of Grunt/Gulp plugins is more or less the same. You start by creating your package.json file:
Except instead of adding a load of ‘grunt-*’ (or ‘gulp-*’) plugins, we simply add the core NPM modules which the Grunt/Gulp plugins wrap.
Using NPM Scripts:
I’m already feeling less anxious. But of course this is just the beginning, we still need to configure our tasks. If using Grunt/Gulp, along with our package.json file we would also have a Gruntfile.js/Gulpfile.js.
The drawback of having neither of these as a dependency does mean we need to make up for any lost benefits, and this does include creating a separate file for each task we want to use. So let’s create a ‘tasks’ directory and keep them there, neato.
Each of the files in the ‘tasks’ directory is what would contain the scripts to be run by Node.js. So to compile our project’s Sass, we would run the ‘./tasks/sass.js’ file in our Node.js environment.
On your command line, you would type:
$ node ./tasks/sass.js
Let’s add a ‘scripts’ section to our package.json file, so we can create a shortcut to running the above script.
"sass": "node ./tasks/sass.js",
"sass-lint": "node ./tasks/sass-lint.js",
"postcss": "node ./tasks/postcss.js",
We can now simply type:
$ npm run sass
To run the ‘sass’ script.
To see a more complete example, checkout this package.json sample.
Now for the hard part — writing the scripts to execute specific tasks. Whilst each task will likely only require a few lines of code, unless you have a lot of Node.js experience already (or the NPM module you are using has extensive documentation), it may be difficult to know what to write. Most modules should have at least a basic Node.js example to work off.
The example given on the Node-Sass documentation is:
‘Wtf does this mean, and how do I use it?’ Was my first thought. Well let’s fast forward to an actual working example. The below code is the contents of the final ./tasks/sass.js file:
Turns out the reality is not quite as simple as the docs made out, as is evident from the additional ‘fs’ and ‘mkdirp’ modules we are requiring (which I believe are used to perform operations of system files and that sort of stuff — if you are a real Node.js developer I’m sure you know more about it). In short, the example provided by the node-sass documentation shows you how to only render the compiled sass/scss. To actually create a new file and write the rendered CSS to it we need assistance from other node modules, i.e. fs-extra and mkdirp, which need to be added to package.json.
Getting from the former example to the latter took a lot of reading, as I have found documentation to be lacking in this area. If you are not one for reading and just want to get started, you may find this boilerplate of interest.
In anycase, running ‘npm run sass’ will now create 2 CSS files, 1 expanded and 1 compressed (assuming our source scss files exist). Hoorah!
The beauty of this approach is that you have total control over your project’s build processes, as it’s all handled by scripts you write. This does of course add a layer of complexity, but as a wise man once said, ‘a grand don’t come for free’. It is ultimately worth the benefits in my opinion — more control, less dependencies. If you remember my goal here was to be able to use bleeding-edge features of Sass in a modern workflow. Mission accomplished! An additional bonus is that I have also future-proofed my project — the more tools like Grunt and Gulp go out of fashion, the longer it takes for their respective tools to receive updates, making my project more prone to issues. Updates to my build processes can now be made quicker, and safer.