Last active
November 20, 2017 18:10
-
-
Save jeremiegirault/becadbb6baf2f066e54ebef0e1ec8154 to your computer and use it in GitHub Desktop.
Webpack css micro loader. Generates a smaller payload than traditional methods.
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
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) | |
}) | |
} |
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
/* 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