Skip to content

Instantly share code, notes, and snippets.

@jeremiegirault
Last active November 20, 2017 18:10
Show Gist options
  • Save jeremiegirault/becadbb6baf2f066e54ebef0e1ec8154 to your computer and use it in GitHub Desktop.
Save jeremiegirault/becadbb6baf2f066e54ebef0e1ec8154 to your computer and use it in GitHub Desktop.
Webpack css micro loader. Generates a smaller payload than traditional methods.
const { stringifyRequest } = require('loader-utils')
const postcss = require('postcss')
const cssModules = require('postcss-modules')
const cssNano = require('cssnano')
const cssNext = require('postcss-cssnext')
const _ = require('lodash')
// Why this loader ?
// This loaded is inteded to load css with a much smaller footprint than traditional css-loader / style-loader
// css-loader and styles-loader have an overhead of ~5kb of runtime when compiled and minified (growing with number of css imports)
// in our use-case (a library embedding css in js) we don't want so much space taken by dependencies
// * css-loader -> css modules to avoid collision with clients + cssnano to minimize
// -- css-loaders share a single state with it's root runtime and each imported module
// -- this causes bug when importing two css modules from js (overwritten export.locals)
// -- provides a runtime with unused functions, output is coupled to style-loader anyway
// * styles-loader -> inserts css to document.head when imported and return css module mapping
// -- provides a runtime to load css content to <style> tags.
// -- runtime provides lot of customization (transform etc.) which is performed client side.
// -- relatively coupled to css-loader output so if you want 'styles-loader', you pretty much want 'css-loader'
// micro-style-loader claims size used by both runtimes back by providing a smaller runtime,
// -- more work is done on compile side / runtime is optimised on our specific use-case.
// -- it could use options and/or specific runtimes, contributors ?
// it is not battle-tested with all css import features but should work on common cases & have a wayyyy smaller footprint on final payload
// /!\ style-loader is still prefereable in developement because it provides HMR
function camelize(obj) {
return _.mapKeys(obj, (value, key) => _.camelCase(key))
}
function processCss(source, sourceMap, callback) {
let moduleMap = null
const pipeline = postcss([
cssNext({
// cssnext displays a duplicate warning because cssnano also using autoprefixer
warnForDuplicates: false
}),
cssModules({
getJSON (filename, json) {
moduleMap = camelize(json)
}
}),
cssNano({ preset: 'default' })
])
const options = {}
if (sourceMap) {
options.map = {
prev: sourceMap,
sourcesContent: true,
inline: false,
annotations: false
}
}
return pipeline.process(source, options)
.then(result => {
return {
moduleMap,
source: result.css,
sourceMap: result.map ? result.map.toJSON() : null
}
})
}
module.exports = function (content, map) {
const runtime = stringifyRequest(this, require.resolve('./micro-style-loader-runtime'))
const callback = this.async()
processCss(content, map)
.then(result => {
const css = JSON.stringify(result.source)
const map = JSON.stringify(result.sourceMap)
const source = [
`var runtime = require(${runtime});`,
`runtime.use(module.id, ${css}, ${map});`,
`module.exports = ${JSON.stringify(result.moduleMap)};`
].join('\n')
callback(null, source, null)
})
.catch(err => {
callback(err)
})
}
/* micro-style-loader runtime */
function createStyleTag () {
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
head.appendChild(style);
return style;
}
function removeTag (tag) {
if (tag.parentNode) {
tag.parentNode.removeChild(tag);
}
}
// multi-tag mode
function insertMultiple (source, map) {
var style = createStyleTag();
var css = source;
if (map) {
var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(map))));
css += '\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64 + ' */';
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
return function () { removeTag(style) };
}
var singleTag = null;
var uniqueIndex = 0;
var textList = {};
function insertSingle (css) { // single tag mode does not support source maps
// initialize the single tag if needed
if (!singleTag) { singleTag = createStyleTag(); }
function recomputeText () {
var sortedKeys = Object.keys(textList)
.sort(function(a,b) { return a - b; })
.map(function (key) { return textList[key] })
.join('\n')
}
if (singleTag.styleSheet) {
// update textlist
var currentIndex = uniqueIndex;
uniqueIndex += 1;
textList[currentIndex] = css;
// update css text
singleTag.styleSheet.cssText = recomputeText();
return function () {
delete textList[currentIndex];
singleTag.styleSheet.cssText = recomputeText();
}
} else {
// append a text node to the style tag
var textNode = document.createTextNode(css);
singleTag.appendChild(textNode);
return function () { remove(textNode); }
}
}
var useSingleTag = false;
var insert = useSingleTag ? insertSingle : insertMultiple;
var modules = {};
function use (moduleId, source, map) {
if (modules[moduleId]) { // module is already inserted
if (modules[moduleId].source !== source) {
modules[moduleId].remove(); // source changed: remove the module (and reinsert after)
} else {
return; // same source, nothing to do
}
}
var removeStyle = insert(source, map);
// ensure removeStyle is only called once a & when style is presented
function remove () {
if (modules[moduleId]) {
removeStyle();
modules[moduleId] = null;
}
}
var newModule = { source: source, map: map, remove: remove };
modules[moduleId] = newModule;
return remove;
}
module.exports = {
modules: modules,
use: use
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment