-
-
Save michael-ciniawsky/4c4427a185a1c33f065ea3069d144164 to your computer and use it in GitHub Desktop.
CSS Webpack Plugin
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 path = require('path') | |
const { getHashDigest } = require('loader-utils') | |
const validateOptions = require('schema-utils') | |
const postcss = require('postcss') | |
const schema = require('./options.json') | |
const filter = require('./lib/filter') | |
const reducer = require('./lib/reducer') | |
const SyntaxError = require('./Error') | |
const { | |
RawSource, | |
ConcatSource, | |
SourceMapSource | |
} = require('webpack-sources') | |
/** | |
* CSS Plugin | |
* | |
* @author webpack Contrib (@webpack-contrib) | |
* @version v1.0.0 | |
* @license MIT | |
* | |
* @requires postcss | |
* | |
* @requires schema-utils | |
* | |
* @requires webpack | |
* @requires webpack-sources | |
*/ | |
const name = 'CSSPlugin' | |
/** | |
* CSS Plugin Options | |
* | |
* @type {Object} | |
* | |
* @prop {String} name Filename | |
* @prop {Array} plugins PostCSS Plugins | |
* @prop {Boolean} sourceMap Source Map | |
*/ | |
const defaults = { | |
name: '[name].css', | |
plugins: [], | |
sourceMap: false, | |
} | |
class CSSPlugin { | |
constructor (options) { | |
options = Object.assign({}, defaults, options) | |
validateOptions(schema, options, 'CSS Plugin') | |
this.options = options | |
} | |
apply (compiler) { | |
const { output } = compiler.options | |
const { compilation } = compiler.hooks | |
compilation.tap(name, (compilation) => { | |
const { optimizeChunks, optimizeTree } = compilation.hooks | |
optimizeChunks.tap(name, (chunks) => { | |
chunks.forEach((chunk) => { | |
// TODO(michael-ciniawsky) | |
// revisit if adding a <link> | |
// and/or preloading is possible | |
// to implement (e.g via chunk.addModule(...)) | |
// or an option to extracted async CSS nevertheless | |
if (!chunk.isInitial()) { | |
return null | |
} | |
// Get CSS | |
const css = chunk | |
.getModules() | |
.filter(filter.css) | |
.sort() | |
.reduceRight((css, module) => { | |
// Remove the style-loader (if present) | |
if (module.request.includes('style-loader')) { | |
module.loaders.pop() | |
} | |
// Extract CSS | |
let source = module._source._value | |
.split('// CSS\n')[1] | |
.replace('export default ', '') | |
.replace(/`/g, '\n') | |
.trim() | |
// Add CSS File | |
source = `\n/* ./${path.basename(module.userRequest)} */\n${source}` | |
// Get Source Map (devtool: 'sourcemap') | |
const sourceMap = module.useSourceMap | |
? module._source._sourceMap | |
: null | |
// Get URL's | |
const urls = module.dependencies | |
.filter(filter.urls) | |
.reduce(reducer.urls, {}) | |
if (urls) { | |
// Add __webpack_public_path__ | |
const publicPath = output.publicPath || '/' | |
// Replace URL's (Placeholders) | |
Object.keys(urls).forEach((placeholder) => { | |
const url = urls[placeholder] | |
? urls[placeholder].includes('data:') | |
? `${urls[placeholder]}` | |
: `'${publicPath}${urls[placeholder]}'` | |
: '/* Loading URL failed */' | |
source = source.replace('${' + `${placeholder}` + '}', url) | |
}) | |
} | |
source = sourceMap | |
? new SourceMapSource(source, module.userRequest, sourceMap) | |
: new RawSource(source, module.userRequest) | |
// Concat CSS | |
css.add(source) | |
// Mark module as extracted (e.g for css-loader) | |
module.extracted = true | |
return css | |
}, new ConcatSource()) | |
// Process CSS (minify, prefix etc.) | |
if (css.size() > 0) { | |
let { output } = compiler.options | |
let { name, plugins, sourceMap } = this.options | |
const options = { | |
to: `${output.path}${`/${path.dirname(name)}` || ''}/${chunk.name}.css`, | |
from: `${output.path}/${chunk.name}.css` | |
} | |
options.map = sourceMap | |
? { | |
prev: css.map() || false, | |
inline: false, | |
// TODO(michael-ciniawsky) | |
// use webpack:// (DevTool Protocol) | |
annotation: true, | |
sourcesContent: true | |
} | |
: false | |
// TODO(michael-ciniawsky) | |
// Presistent Cache (Assets) | |
return postcss(plugins) | |
.process(css.source(), options) | |
.then(({ css, map }) => { | |
const asset = (name, map) => { | |
const HASH = /\[(?:(\w+):)?contenthash(?::([a-z]+\d*))?(?::(\d+))?\]/ig | |
return compilation | |
.getPath(name, { chunk }) | |
.replace(HASH, (name, type, digest, length) => { | |
return getHashDigest(css, type, digest, parseInt(length, 10)) | |
}) | |
} | |
compilation.assets[asset(name)] = new RawSource(css) | |
if (map) { | |
compilation.assets[`${asset(name)}.map`] = new RawSource( | |
JSON.stringify(map) | |
) | |
} | |
return null | |
}) | |
.catch((err) => { | |
err.name === 'CssSyntaxError' | |
? compilation.errors.push(new SyntaxError(err)) | |
: compilation.errors.push(err) | |
return null | |
}) | |
} | |
}) | |
}) | |
optimizeTree.tapAsync(name, (chunks, modules, cb) => { | |
modules | |
.filter(filter.extracted) | |
.reduce((rebuilded, module) => { | |
return new Promise((resolve, reject) => { | |
// Rebuild module to remove dependencies | |
compilation.rebuildModule(module, (err) => { | |
if (err) reject(err) | |
resolve() | |
}) | |
}) | |
}, Promise.resolve()) | |
.then(() => cb()) | |
.catch((err) => compilation.errors.push(err)) | |
}) | |
}) | |
} | |
} | |
module.exports = CSSPlugin |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment