const fs = require('fs'); const { promisify } = require('util'); const path = require('path'); const parse5 = require('parse5'); const nwmatcher = require('nwmatcher'); const css = require('css'); const prettyBytes = require('pretty-bytes'); const readFile = promisify(fs.readFile); const treeAdapter = parse5.treeAdapters.htmlparser2; const PLUGIN_NAME = 'critters-webpack-plugin'; const PARSE5_OPTS = { treeAdapter }; /** Critters: Webpack Plugin Edition! * @class * @param {Object} options * @param {Boolean} [options.external=true] Fetch and inline critical styles from external stylesheets * @param {Boolean} [options.async=false] Convert critical-inlined external stylesheets to load asynchronously (via link rel="preload") * @param {Boolean} [options.minify=false] Minify resulting critical CSS using cssnano (note: this often doesn't result in further size reduction) */ module.exports = class CrittersWebpackPlugin { constructor(options) { this.options = options || {}; } apply(compiler) { const outputPath = compiler.options.output.path; // hook into the compiler to get a Compilation instance... compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { // ... which is how we get an "after" hook into html-webpack-plugin's HTML generation. compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(PLUGIN_NAME, (htmlPluginData, callback) => { // Parse the generated HTML in a DOM we can mutate const document = parse5.parse(htmlPluginData.html, PARSE5_OPTS); makeDomInteractive(document); // `external:false` skips processing of external sheets const externalSheets = this.options.external===false ? [] : document.querySelectorAll('link[rel="stylesheet"]'); Promise.all(externalSheets.map(link => { const href = link.getAttribute('href'); // skip network resources if (href.match(/^(https?:)?\/\//)) return Promise.resolve(); // path on disk const filename = path.resolve(outputPath, href.replace(/^\//, '')); // try to find a matching asset by filename in webpack's output (not yet written to disk) const asset = compilation.assets[path.relative(outputPath, filename).replace(/^\.\//, '')]; // wait for a disk read if we had to go to disk const promise = asset ? Promise.resolve(asset.source()) : readFile(filename); return promise.then(sheet => { // the reduced critical CSS gets injected into a new