In previous post, I have briefly mentioned that systemjs/builder has a great support of extensibiity by providing a plug-in mechanism. In this post, I will show you how we can leverage this and make loading/bundling LESS files work on top of the systemjs loading pipeline. We are essentially aiming for two goals:
- During development time, we should be able to save and refresh to see the results of LESS file changes
- During producing time, we should be able to compile and bundle the generated CSS into the bundle file
The github repository of this plug-in and its usage can be found from here https://github.com/jack4it/system-less.
A brief word of LESS
According to its official website: Less is a CSS pre-processor, meaning that it extends the CSS language, adding features that allow variables, mixins, functions and many other techniques that allow you to make CSS that is more maintainable, themable and extendable.
LESS can run in multiple different environments, most importantly, in browser and node.js. These are the two exact environments that our plug-in will need to support. However, unlike the usuall cases, we will invoke LESS API programmatically, instead of running the node.js CLI or using a <script /> to include it on a web page.
The entry point of LESS API looks likes below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
less.render(lessInput, options) | |
.then(function(output) { | |
// output.css = string of css | |
// output.map = string of sourcemap | |
// output.imports = array of string filenames of the imports referenced | |
}, | |
function(error) { | |
}); |
A quick overview of the plug-in mechanim of systemjs
According to systemjs documentation:
A plugin is just a set of overrides for the loader hooks of the ES6 module specification. The hooks plugins can override are locate
, fetch
, translate
and instantiate
.
The behavior of the hooks is:
- Locate: Overrides the location of the plugin resource
- Fetch: Called with third argument representing default fetch function, has full control of fetch output.
- Translate: Returns the translated source from
load.source
, can also setload.metadata.sourceMap
for full source maps support. - Instantiate: Providing this hook as a promise or function allows the plugin to hook instantiate. Any return value becomes the defined custom module object for the plugin call.
In our case, we are going to override the Translate hook and another undocumented but obviously necessary one for the bundling scenario. It’s called bundle.
The implementation of system-less, a LESS plug-in for systemjs
Our first goal is to be able to load LESS files and apply the generated CSS styles on the fly during development time. We implement this by overriding the Translate hook like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
exports.translate = function (load) { | |
return System.import("less/lib/less-browser") | |
.then(function (lesscWrapper) { | |
return lesscWrapper(window, { | |
async: true, | |
errorReporting: "Console" | |
}); | |
}) | |
.then(function (lessc) { | |
return lessc.render(load.source, { | |
filename: load.name.replace(/^file:(\/+)?/i, '') | |
}); | |
}) | |
.then(function (output) { | |
// output.css = string of css | |
// output.map = string of sourcemap | |
// output.imports = array of string filenames of the imports referenced | |
var style = document.createElement('style'); | |
style.setAttribute('type', 'text/css'); | |
style.textContent = output.css; | |
document.getElementsByTagName('head')[0].appendChild(style); | |
load.metadata.format = 'defined'; | |
}); | |
}; |
There are three major parts of this implementation. First, we import the LESS browser compilation module less/lib/less-browser. This module is a wrapper of the core LESS logic. Second, we call the render method to compile the loaded LESS file content. Notice that the file content is already loaded by the systemjs pipeline, so that we don’t need to worry about the network loading part of it. Third, once we get the compiled results, the CSS styles, we need to inject them to the DOM, so that the browser will be able to pick them up and render the related markups with the new styles.
It’s a fairly straightforward logic to compile and apply LESS files in browsers.
Now it comes to the second goal of being able to compile and bundle LESS into the bundle file. This is a must-have goal for today’s web landscape. We can’t afford to load and compile LESS on the fly for a production system. That would be a kill of perfromance. Unlike loading LESS in browser, bundling via systemjs-builder happens in node.js environment. So the logic will be a bit different. Here is what it looks like:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var cssInject = "(function(c){var d=document,a='appendChild',i='styleSheet',s=d.createElement('style');s.type='text/css';d.getElementsByTagName('head')[0][a](s);s[i]?s[i].cssText=c:s[a](d.createTextNode(c));})"; | |
var escape = function (source) { | |
return source | |
.replace(/(["\\])/g, '\\$1') | |
.replace(/[\f]/g, '\\f') | |
.replace(/[\b]/g, '\\b') | |
.replace(/[\n]/g, '\\n') | |
.replace(/[\t]/g, '\\t') | |
.replace(/[\r]/g, '\\r') | |
.replace(/[\ufeff]/g, '') | |
.replace(/[\u2028]/g, '\\u2028') | |
.replace(/[\u2029]/g, '\\u2029'); | |
}; | |
exports.translate = function (load) { | |
load.metadata.format = 'defined'; | |
}; | |
exports.bundle = function (loads, compileOpts, outputOpts) { | |
var stubDefines = loads.map(function (load) { | |
return (compileOpts.systemGlobal || "System") + ".register('" + load.name + "', [], false, function() {});"; | |
}).join('\n'); | |
var lessc = System._nodeRequire("less"); | |
var compilePromise = function (load) { | |
return lessc.render(load.source, { | |
filename: load.name.replace(/^file:(\/+)?/i, '') | |
}) | |
.then(function (output) { | |
// output.css = string of css | |
// output.map = string of sourcemap | |
// output.imports = array of string filenames of the imports referenced | |
return output.css; | |
}) | |
}; | |
var cssOptimize = outputOpts.minify && outputOpts.cssOptimize !== false; | |
var CleanCSS = System._nodeRequire("clean-css"); | |
var cleaner = new CleanCSS({ | |
advanced: cssOptimize, | |
agressiveMerging: cssOptimize, | |
mediaMerging: cssOptimize, | |
restructuring: cssOptimize, | |
shorthandCompacting: cssOptimize, | |
////sourceMap: !!outputOpts.sourceMaps, | |
////sourceMapInlineSources: outputOpts.sourceMapContents | |
}); | |
return Promise.all(loads.map(compilePromise)) | |
.then(function (cssResults) { | |
var all = cssResults.join(""); | |
var minified = cleaner.minify(all).styles; | |
return [stubDefines, cssInject, "('" + escape(minified) + "');"].join('\n'); | |
}); | |
}; |
There a few different things to notice from this implementation. First, we have a minified version of the injection logic which will be inlined into the bundle. It is to be called to inject the CSS styles when systemjs loads the bundles. Second, now we have stubs of system.register for each of the LESS/CSS files. This will be interpretated correctly by systemjs during the load time. Third, optionally for this post but a must-have for a real plug-in, we use clean-css to optimize the generated CSS styles. With this implementation, during producing time, systemjs-builder will be able to figure out the LESS files and compile and bundle them into the bundle file together with other resources.
Summary
In this post, I walked through the process of developing a systemjs/builder plug-in for LESS resources. This plug-in mechanism is a powerful tool to extend the systemjs/builder functionality. In fact, there are already quite a few great plug-ins developed and can be used directly in your project. With these plug-ins, we can easily set up a seamless workflow that easily save and refresh for the development time and optimize the loading performance for production time using bundling.
Hope this helps,
-Jack