Skip to content

Instantly share code, notes, and snippets.

@michael-ciniawsky
Last active January 21, 2018 16:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michael-ciniawsky/4c4427a185a1c33f065ea3069d144164 to your computer and use it in GitHub Desktop.
Save michael-ciniawsky/4c4427a185a1c33f065ea3069d144164 to your computer and use it in GitHub Desktop.
CSS Webpack Plugin
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