From 306fecee7c2ec718ea9f0ef5a2cea8251f265583 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Tue, 3 Apr 2018 10:50:00 -0400 Subject: [PATCH] Use modern JS features, add lots of comments. --- config/critters-webpack-plugin.js | 284 +++++++++++++++++------------- 1 file changed, 157 insertions(+), 127 deletions(-) diff --git a/config/critters-webpack-plugin.js b/config/critters-webpack-plugin.js index 6aa631f1..2d012446 100644 --- a/config/critters-webpack-plugin.js +++ b/config/critters-webpack-plugin.js @@ -5,94 +5,20 @@ const nwmatcher = require('nwmatcher'); const css = require('css'); const prettyBytes = require('pretty-bytes'); -const treeUtils = parse5.treeAdapters.htmlparser2; +const treeAdapter = parse5.treeAdapters.htmlparser2; const PLUGIN_NAME = 'critters-webpack-plugin'; const PARSE5_OPTS = { - treeAdapter: treeUtils -}; - -function defineProperties(obj, properties) { - for (const i in properties) { - const value = properties[i]; - Object.defineProperty(obj, i, typeof value === 'function' ? { value: value } : value); - } -} - -const ElementExtensions = { - nodeName: { - get: function() { - return this.tagName; - } - }, - insertBefore: function (child, referenceNode) { - if (!referenceNode) return this.appendChild(child); - treeUtils.insertBefore(this, child, referenceNode); - return child; - }, - appendChild: function (child) { - treeUtils.appendChild(this, child); - return child; - }, - removeChild: function (child) { - treeUtils.detachNode(child); - }, - setAttribute: function (name, value) { - if (this.attribs == null) this.attribs = {}; - if (value == null) value = ''; - this.attribs[name] = value; - }, - removeAttribute: function(name) { - if (this.attribs != null) { - delete this.attribs[name]; - } - }, - getAttribute: function (name) { - return this.attribs != null && this.attribs[name]; - }, - hasAttribute: function (name) { - return this.attribs != null && this.attribs[name] != null; - }, - getAttributeNode: function (name) { - const value = this.getAttribute(name); - if (value!=null) return { specified: true, value: value }; - }, - getElementsByTagName: getElementsByTagName -}; - -const DocumentExtensions = { - nodeType: { - get: function () { - return 11; - } - }, - createElement: function (name) { - return treeUtils.createElement(name, null, []); - }, - createTextNode: function (text) { - const scratch = this.$$scratchElement; - treeUtils.insertText(scratch, text); - const node = scratch.lastChild; - treeUtils.detachNode(node); - return node; - }, - querySelector: function (sel) { - return this.$match.first(sel); - }, - querySelectorAll: function (sel) { - return this.$match.select(sel); - }, - getElementsByTagName: getElementsByTagName, - // https://github.com/dperini/nwmatcher/blob/3edb471e12ce7f7d46dc1606c7f659ff45675a29/src/nwmatcher.js#L353 - addEventListener: Object + 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=true] If `false`, only already-inline stylesheets will be reduced to critical rules. + * @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 */ module.exports = class CrittersWebpackPlugin { constructor(options) { @@ -100,59 +26,60 @@ module.exports = class CrittersWebpackPlugin { } apply(compiler) { - const self = this; const outputPath = compiler.options.output.path; - compiler.hooks.compilation.tap(PLUGIN_NAME, function (compilation) { - compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(PLUGIN_NAME, function (htmlPluginData, callback) { + + // 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); - defineProperties(document, DocumentExtensions); - document.documentElement = document.childNodes[document.childNodes.length - 1]; + // `external:false` skips processing of external sheets + const externalSheets = this.options.external===false ? [] : document.querySelectorAll('link[rel="stylesheet"]'); - const scratch = document.$$scratchElement = document.createElement('div'); - const elementProto = Object.getPrototypeOf(scratch); - defineProperties(elementProto, ElementExtensions); - elementProto.ownerDocument = document; - - document.$match = nwmatcher({ document }); - document.$match.configure({ - CACHING: false, - USE_QSAPI: false, - USE_HTML5: false - }); - - const externalSheets = document.querySelectorAll('link[rel="stylesheet"]'); - - Promise.all(externalSheets.map(function(link) { - if (self.options.external===false) return; + 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(function (sheet) { + return promise.then(sheet => { + // the reduced critical CSS gets injected into a new