Compiling SASS and PostCSS with Angular CLI
Introduction
For newcomers to Angular 2, it can be daunting figuring out how to setup a project for the first time with everything you have to consider. Luckily, Angular CLI takes care of the complexity that comes with build and deploy pipelines, but this convenience comes at a price. Suddenly you are at the mercy of the CLI and have no idea how it works!
This tutorial will introduce some key concepts when it comes to customizing the Angular CLI build. By demonstrating a typical use case, compiling the output of a CSS preprocessor, we will learn how the Angular CLI build can be configured.
Getting Started
Follow the instructions in the README on the Angular CLI github page for starting a new project.
After you have installed all of the dependencies for the CLI to run, use this command in your Terminal to start a new project:
ng new project-name
replacing the name of the project with your own.
SASS and Angular CLI
Out of the box, Angular CLI will compile SASS that is loaded with a component, but it does not handle global CSS. This tutorial will help you configure the build to compile global CSS as well as allow you to use SASS variables declared globally inside component level SASS. You will also learn how to utilize PostCSS for further processing of CSS after SASS compiles including auto prefixing for older browsers and minification so your CSS will load even faster for your users.
Broccoli
If you've run ng serve
, you may have noticed the Angular CLI uses a package called BroccoliTypeScriptCompiler
. This package compiles the Typescript in your Angular 2 project to JavaScript and it belongs to a community of packages used with Broccoli.js.
For more information about Broccoli, visit the project's website.
In short, Broccoli is an build and deploy pipeline with a growing developer community. It is the basis for ember-cli, which angular-cli is derived from.
Since Angular CLI is built from Broccoli, we have access to all of the community's plugins. You can view all of them via npm.
These plugins can be used to customize the build of your Angular CLI project.
Customizing The Build
The angular-cli-build.js
file in the root directory of your project is where the build can be customized. When you start with a new project, this file looks like this:
/* global require, module */
var Angular2App = require('angular-cli/lib/broccoli/angular2-app');
module.exports = function(defaults) {
return new Angular2App(defaults, {
vendorNpmFiles: [
'systemjs/dist/system-polyfills.js',
'systemjs/dist/system.src.js',
'zone.js/dist/**/*.+(js|js.map)',
'es6-shim/es6-shim.js',
'reflect-metadata/**/*.+(js|js.map)',
'rxjs/**/*.+(js|js.map)',
'@angular/**/*.+(js|js.map)'
]
});
};
An instance of Angular2App
is returned in the module.exports
of angular-cli-build.js
.
Angular2App
is what is called a Broccoli tree. A tree can be any string representing a directory path or in this case, an Object
that conforms to the Broccoli.JS Plugin API specification. The result of this build, what gets returned in the module.exports, is the tree of files that makes up a boilerplate Angular 2 App. Notice how the Angular2App can be configured with options. The example above specifies an Array
of file globs that this app will load into the /dist/vendor
directory.
We can add another option to this build to configure the SASS compiler. Just before the vendorNpmFiles
property add another property called sassCompiler
like in the following example:
sassCompiler: {
includePaths: [
'src/style'
]
},
vendorNpmFiles: [
The sassCompiler takes the same arguments that node-sass does. A complete list of options in available on the node-sass github page.
I chose to use the option includePaths
and set the path to my global SASS: src/style
. This alone will not compile the global SASS, but it does configure the node-sass compiler to find files when using the @import
directive. Now you can import SASS from src/style
anywhere in your app.
This solves a critical problem with the CLI. By default you cannot reference SASS in another file with @import
. Now by configuring the SASS compiler, you should be able to.
Compile SASS
To compile SASS, we need to install some more packages from npm.
npm i broccoli-sass broccoli-merge-trees lodash glob --save
This command installs 4 tools we need to compile SASS.
- broccoli-sass is a plug-in that will compile SASS with node-sass.
- broccoli-merge-trees allows you to execute multiple build processes and then merge them back together in one single tree necessary for the module.exports.
- lodash and glob will be used as utilities to format our SASS files so they can be compiled.
Special thanks to @intellix on github for this method.
Once you install the dependencies, import them into angular-cli-build.js
like so:
const Angular2App = require('angular-cli/lib/broccoli/angular2-app');
const compileSass = require('broccoli-sass');
const mergeTrees = require('broccoli-merge-trees');
const _ = require('lodash');
const glob = require('glob');
Since we are writing ES6, I decided to change the var to const.
Now we have to slightly modify the module.exports so we can export multiple trees, not just the Angular2App.
module.exports = function(defaults) {
let appTree = new Angular2App(defaults, {
sassCompiler: {
includePaths: [
'src/style'
]
},
vendorNpmFiles: [
'systemjs/dist/system-polyfills.js',
'systemjs/dist/system.src.js',
'zone.js/dist/**/*.+(js|js.map)',
'es6-shim/es6-shim.js',
'reflect-metadata/**/*.+(js|js.map)',
'rxjs/**/*.+(js|js.map)',
'@angular/**/*.+(js|js.map)'
]
});
return mergeTrees([appTree], { overwrite: true });
};
Run ng serve
to test the build works.
Notice that all we did was instead of returning appTree
directly, we make it a variable using let
, then returned mergeTrees
. mergeTrees
first argument is an Array
, then optionally a second argument with an Object
of options. Set overwrite: true
because we want to override the default SASS compile for our new custom compile.
Add the following to the module.exports:
let sass = mergeTrees(_.map(glob.sync('src/**/*.scss'), function(sassFile) {
sassFile = sassFile.replace('src/', '');
return compileSass(['src'], sassFile, sassFile.replace(/.scss$/, '.css'));
}));
This snippet requires that your global SASS resides in the /src
folder, but as an added side effect it also compiles the rest of the component level SASS in the same directory.
Let's break it down a bit.
Remember how I said Broccoli trees can be file paths? In this case, we are using lodash
and glob
to fetch an Array of file paths that have the .scss
extension.
mergeTrees can take a second argument in the form of a function callback. If you were to log sassFile
you would find the build iterating over the file paths for each .scss
file in your project's /src
directory. We are going to transform the path slightly for the compileSass
plug-in, removing the src/
. compileSass takes the tree that needs to be compiled (in this case 'src'), the file path for the SASS file, and the file path of the destination CSS file. Since the files are being iterated, each file passes through this function and will be compiled into CSS.
Finally, let's add the sass tree to the output of the build.
return mergeTrees([appTree, sass], { overwrite: true });
Now the sass tree will overwrite the appTree's SASS compile, only touching the css and not the other files in our project.
Run ng serve
to test the build works.
PostCSS
Better yet, we can leverage the functionality of PostCSS to further augment our CSS after compile. To learn more about PostCSS, visit the project's website. Two typical tasks that PostCSS can handle are autoprefixing CSS and CSS minification, so let's go ahead and implement those in the build.
Install the following three dependencies via npm:
npm i broccoli-postcss postcss-cssnext cssnano --save
- broccoli-post-css is the plug-in for Broccoli that integrates the PostCSS pipeline
- postcss-cssnext is the PostCSS plug-in that handles autoprefixing
- cssnano is a CSS minification tool
Add these packages to the top of angular-cli-build.js
.
const compileCSS = require('broccoli-postcss');
const cssnext = require('postcss-cssnext');
const cssnano = require('cssnano');
PostCSS takes options via an object, which let's PostCSS know which plugins to load and how to use them.
var options = {
plugins: [
{
module: cssnext,
options: {
browsers: ['> 1%'],
warnForDuplicates: false
}
},
{
module: cssnano,
options: {
safe: true,
sourcemap: true
}
}
]
};
Here we are telling PostCSS to use two modules: cssnext
and cssnano
, and further configuring these plugins.
Inside the module.exports, create another variable called css
like so:
let css = compileCSS(sass, options);
Here we pass the SASS tree (which output the file paths of the .css
files) and the options we added above.
Now, add the css tree to the mergeTrees that are returned in module.exports like so:
return mergeTrees([appTree, sass, css], { overwrite: true });
It is important the trees are in the above order, so they override each other correctly.
Run ng serve
to test the build works.
You should see something similar to:
Build successful - 2592ms.
Slowest Trees | Total
----------------------------------------------+---------------------
BroccoliTypeScriptCompiler | 1047ms
vendor | 783ms
PostcssFilter | 374ms
Slowest Trees (cumulative) | Total (avg)
----------------------------------------------+---------------------
BroccoliTypeScriptCompiler (1) | 1047ms
vendor (1) | 783ms
PostcssFilter (1) | 374ms
SassCompiler (24) | 130ms (5 ms)
Conclusion
By configuring the Angular CLI build to compile SASS and integrate PostCSS, we've seen how to customize the build and run tasks in parallel using mergeTrees. This lesson could be taken a step further with a myriad of plug-ins available to the Broccoli community. Now that you've adopted the Angular CLI, welcome to the community and for help and more information check out the gitter channel for the CLI.
Here is the finished angular-cli-build.js
:
const Angular2App = require('angular-cli/lib/broccoli/angular2-app');
const compileSass = require('broccoli-sass');
const compileCSS = require('broccoli-postcss');
const cssnext = require('postcss-cssnext');
const cssnano = require('cssnano');
const mergeTrees = require('broccoli-merge-trees');
const _ = require('lodash');
const glob = require('glob');
var options = {
plugins: [
{
module: cssnext,
options: {
browsers: ['> 1%'],
warnForDuplicates: false
}
},
{
module: cssnano,
options: {
safe: true,
sourcemap: true
}
}
]
};
module.exports = function(defaults) {
let appTree = new Angular2App(defaults, {
sassCompiler: {
includePaths: [
'src/style'
]
},
vendorNpmFiles: [
'systemjs/dist/system-polyfills.js',
'systemjs/dist/system.src.js',
'zone.js/dist/**/*.+(js|js.map)',
'es6-shim/es6-shim.js',
'reflect-metadata/**/*.+(js|js.map)',
'rxjs/**/*.+(js|js.map)',
'@angular/**/*.+(js|js.map)'
]
});
let sass = mergeTrees(_.map(glob.sync('src/**/*.scss'), function(sassFile) {
sassFile = sassFile.replace('src/', '');
return compileSass(['src'], sassFile, sassFile.replace(/.scss$/, '.css'));
}));
let css = compileCSS(sass, options);
return mergeTrees([appTree, sass, css], { overwrite: true });
};
To everyone who has posted recently. This blog post is now out of date. It was based on early Beta of Angular CLI which used Broccoli NOT Webpack. Codementor does not allow editing of posts after publish otherwise I would put disclaimer on this post.
If you are frustrated with customizing Angular CLI I recommend making a custom build of your own so you understand how everything works. Out of the box solutions are probably not a best practice for build and deploy pipelines as every project varies in requirements and what happens when the build fails, do you fix it or wait for project to update?
Regards,
Steve
Hallo! I’m using the version 1.0.0-rc.1 of Angular CLI and in this version if you generate the project with this command:
ng new scss-project --style=scss
then you can use Sass (with SCSS syntax) not only in the component stylesheets but also in the global stylesheet.
You can take a look here:
https://github.com/angular/…
Ng cli is currently webpack based :
http://stackoverflow.com/qu…