diff --git a/config/html-webpack-inline-critical-css-plugin.js b/config/html-webpack-inline-critical-css-plugin.js
new file mode 100644
index 00000000..1bb0dd5b
--- /dev/null
+++ b/config/html-webpack-inline-critical-css-plugin.js
@@ -0,0 +1,406 @@
+const fs = require('fs');
+const path = require('path');
+// const jsdom = require('jsdom');
+// const cssTree = require('css-tree');
+const parse5 = require('parse5');
+const nwmatcher = require('nwmatcher');
+const css = require('css');
+
+const treeUtils = parse5.treeAdapters.htmlparser2;
+
+const PLUGIN_NAME = 'html-webpack-inline-critical-css-plugin';
+
+const PARSE5_OPTS = {
+ treeAdapter: treeUtils
+};
+
+function defineProperties(obj, properties) {
+ for (let i in properties) {
+ let value = properties[i];
+ Object.defineProperty(obj, i, typeof value === 'function' ? { value: value } : value);
+ }
+}
+
+const ElementExtensions = {
+ nodeName: {
+ get: function() {
+ return this.tagName;
+ }
+ },
+ 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) {
+ let scratch = this.$$scratchElement;
+ treeUtils.insertText(scratch, text);
+ let 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
+};
+
+module.exports = class HtmlWebpackInlineCriticalCssPlugin {
+ constructor(options) {
+ this.options = options || {};
+ }
+
+ 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) {
+ const document = parse5.parse(htmlPluginData.html, PARSE5_OPTS);
+
+ defineProperties(document, DocumentExtensions);
+ document.documentElement = document.childNodes[document.childNodes.length - 1];
+
+ let scratch = document.$$scratchElement = document.createElement('div');
+ let elementProto = Object.getPrototypeOf(scratch);
+ defineProperties(elementProto, ElementExtensions);
+ // Object.assign(elementProto, ElementExtensions);
+ elementProto.ownerDocument = document;
+
+ document.$match = nwmatcher({ document });
+ document.$match.configure({
+ CACHING: false,
+ USE_QSAPI: false,
+ USE_HTML5: false
+ });
+
+ // const head = document.$match.byTag('head');
+
+ let externalSheets;
+ externalSheets = document.querySelectorAll('link[rel="stylesheet"]');
+
+ Promise.all(externalSheets.map(function(link) {
+ const href = link.getAttribute('href');
+ if (href.match(/^(https?:)?\/\//)) return Promise.resolve();
+ const filename = path.resolve(outputPath, href.replace(/^\//, ''));
+ // console.log('>>>> '+ filename);
+ let asset = compilation.assets[path.relative(outputPath, filename).replace(/^\.\//, '')];
+ let promise = asset ? Promise.resolve(asset.source()) : readFile(filename);
+ return promise.then(function (sheet) {
+ const style = document.createElement('style');
+ style.$$name = href;
+ style.appendChild(document.createTextNode(sheet));
+ // head.appendChild(style);
+ link.parentNode.appendChild(style); // @TODO insertBefore
+ });
+ }))
+ .then(function() {
+ const styles = document.$match.byTag('style');
+ return Promise.all(styles.map(function (style) {
+ return self.processStyle(style, document);
+ }));
+ })
+ .then(function () {
+ const html = parse5.serialize(document, PARSE5_OPTS);
+ callback(null, { html });
+ })
+ .catch(function (err) {
+ callback(err);
+ });
+
+ // for (let i = 0; i < styles.length; i++) {
+ // this.processStyle(styles[i], document);
+ // }
+
+ // const html = parse5.serialize(document);
+ // return { html };
+
+ /*
+ const dom = new jsdom.JSDOM(htmlPluginData.html);
+ const styles = dom.window.document.querySelectorAll('style');
+ for (let i=0; i0 && style.childNodes.map(getNodeValue).join('\n');
+ if (!sheet) return done;
+
+ // let ast = cssTree.parse(sheet, {
+ // positions: false,
+ // // parseRulePrelude: false,
+ // // parseAtrulePrelude: false,
+ // parseValue: false
+ // });
+ // cssTree.walk(ast, {
+ // visit: 'Rule',
+ // enter(node, item, list) {
+ // console.log(this);
+ // if (node.prelude && node.prelude.type === 'SelectorList' && document.querySelector(cssTree.generate(node.prelude)) == null) {
+ // list.remove(item);
+ // }
+ // // if (node.type === 'Selector') {
+ // // }
+ // }
+ // });
+ // sheet = cssTree.generate(ast);
+
+ let ast = css.parse(sheet);
+
+ let before = sheet;
+
+ visit(ast, function (rule) {
+ if (rule.type==='rule') {
+ rule.selectors = rule.selectors.filter(function (sel) {
+ if (sel.match(/:(hover|focus|active)([.[#~&^:*]|\s|\n|$)/)) return false;
+ sel = sel.replace(/::?(?:[a-z-]+)([.[#~&^:*]|\s|\n|$)/gi, '$1');
+ return document.querySelector(sel, document) != null;
+ });
+ // If there are no matched selectors, replace the rule with a comment.
+ if (rule.selectors.length===0) {
+ rule.type = 'comment';
+ rule.comment = '';
+ delete rule.selectors;
+ delete rule.declarations;
+ }
+ }
+
+ if (rule.rules) {
+ // Filter out comments
+ rule.rules = rule.rules.filter(notComment);
+ // If there are no remaining rules, replace the parent rule with a comment.
+ if (rule.rules.length===0) {
+ rule.type = 'comment';
+ rule.comment = '';
+ delete rule.rules;
+ }
+ }
+ });
+
+ sheet = css.stringify(ast, { compress: true });
+
+ if (this.options.minimize || this.options.compress || this.options.minify) {
+ const cssnano = require('cssnano');
+ done = cssnano.process(sheet, {}, { preset: 'default' }).then(function (result) {
+ sheet = result.css;
+ });
+ }
+
+ return done.then(function () {
+ if (sheet.trim().length===0) {
+ // all rules were removed, get rid of the style element entirely
+ sheet.parentNode.removeChild(sheet);
+ }
+ else {
+ // replace the stylesheet inline
+ while (style.lastChild) {
+ style.removeChild(style.lastChild);
+ }
+ style.appendChild(document.createTextNode(sheet));
+ }
+ const name = style.$$name;
+ console.log('\u001b[32mInlined CSS Size' + (name ? (' ('+name+')') : '') + ': ' + (sheet.length / 1000).toPrecision(2) + 'kb (' + ((before.length - sheet.length) / before.length * 100 | 0) + '% of original ' + (before.length / 1000).toPrecision(2) + 'kb)\u001b[39m');
+ });
+
+ /*
+ console.dir(style);
+ if (style.sheet == null) return;
+
+ const document = style.ownerDocument;
+
+ // function serializeStyle(styleDeclaration) {
+ // let str = '';
+ // for (let i=0; i {
+ // if (keyframe.type == 'keyframe') {
+ // fn(keyframe, rule);
+ // }
+ // });
+ // return;
+ // }
+
+ // @charset, @import etc
+ // if (!rule.declarations && !rule.keyframes) return;
+
+ fn(rule);
+ });
+}
+
+
+function readFile(file) {
+ return new Promise(function (resolve, reject) {
+ fs.readFile(file, 'utf8', function (err, contents) {
+ if (err) reject(err);
+ else resolve(contents);
+ });
+ });
+}
+
+function getNodeValue(node) {
+ return node.nodeValue;
+}
+
+function notComment(rule) {
+ return rule.type !== 'comment';
+}
+
+function getElementsByTagName(tagName) {
+ let stack = [this];
+ let matches = [];
+ let isWildCard = tagName === '*';
+ let tagNameUpper = tagName.toUpperCase();
+ while (stack.length !== 0) {
+ let el = stack.pop();
+ let child = el.lastChild;
+ while (child) {
+ if (child.nodeType === 1) stack.push(child);
+ child = child.previousSibling;
+ }
+ if (isWildCard || (el.tagName != null && (el.tagName === tagNameUpper || el.tagName.toUpperCase() === tagNameUpper))) {
+ matches.push(el);
+ }
+ }
+ return matches;
+}
diff --git a/package.json b/package.json
index 5b032fe7..0e032910 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,8 @@
2,
2
],
- "react/prefer-stateless-function": 0
+ "object-shorthand": 0,
+ "prefer-arrow-callback": 0
}
},
"eslintIgnore": [
@@ -34,8 +35,10 @@
"babel-plugin-transform-react-remove-prop-types": "^0.4.13",
"babel-preset-env": "^1.6.1",
"babel-register": "^6.26.0",
+ "chalk": "^2.3.2",
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^4.5.1",
+ "css": "^2.2.1",
"css-loader": "^0.28.11",
"ejs-loader": "^0.3.1",
"eslint": "^4.18.2",
@@ -48,6 +51,8 @@
"jsdom": "^11.6.2",
"mini-css-extract-plugin": "^0.3.0",
"node-sass": "^4.7.2",
+ "nwmatcher": "^1.4.4",
+ "parse5": "^4.0.0",
"preact-render-to-string": "^3.7.0",
"preload-webpack-plugin": "github:GoogleChromeLabs/preload-webpack-plugin",
"progress-bar-webpack-plugin": "^1.11.0",
diff --git a/src/style/index.scss b/src/style/index.scss
index 2795b423..5d7073ee 100644
--- a/src/style/index.scss
+++ b/src/style/index.scss
@@ -1,7 +1,7 @@
// @import './material-icons.scss';
// @import 'material-components-web/material-components-web';
@import './reset.scss';
-@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
+// @import url('https://fonts.googleapis.com/icon?family=Material+Icons');
html, body {
height: 100%;
diff --git a/webpack.config.js b/webpack.config.js
index d2d18149..89df2021 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -9,6 +9,7 @@ const PreloadWebpackPlugin = require('preload-webpack-plugin');
const ReplacePlugin = require('webpack-plugin-replace');
const CopyPlugin = require('copy-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
+const HtmlInlineCssPlugin = require('./config/html-webpack-inline-critical-css-plugin');
const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
@@ -174,6 +175,8 @@ module.exports = function(_, env) {
// Inject for resources
isProd && new PreloadWebpackPlugin(),
+ isProd && new HtmlInlineCssPlugin(),
+
// Inline constants during build, so they can be folded by UglifyJS.
new webpack.DefinePlugin({