├── .gitignore ├── test ├── fixtures │ ├── unused │ │ ├── index.js │ │ └── index.html │ ├── raw │ │ ├── index.js │ │ └── index.html │ ├── basic │ │ ├── index.js │ │ └── index.html │ └── external │ │ ├── index.js │ │ ├── index.html │ │ └── style.css ├── standalone.test.js ├── __snapshots__ │ ├── standalone.test.js.snap │ └── index.test.js.snap ├── _helpers.js └── index.test.js ├── .editorconfig ├── CONTRIBUTING.md ├── src ├── css.js ├── dom.js └── index.js ├── package.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | dist 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /test/fixtures/unused/index.js: -------------------------------------------------------------------------------- 1 | console.log('empty file'); 2 | -------------------------------------------------------------------------------- /test/fixtures/raw/index.js: -------------------------------------------------------------------------------- 1 | import html from './index.html'; 2 | 3 | module.exports = html; 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/index.js: -------------------------------------------------------------------------------- 1 | document.body.appendChild(document.createTextNode('this counts as SSR')); 2 | -------------------------------------------------------------------------------- /test/fixtures/external/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | 3 | document.body.appendChild(document.createTextNode('this counts as SSR')); 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /test/fixtures/raw/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic Demo 5 | 11 | 16 | 17 | 18 |

Some HTML Here

19 | 20 | 21 | -------------------------------------------------------------------------------- /test/fixtures/unused/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic Demo 5 | 11 | 16 | 17 | 18 |

Some HTML Here

19 | 20 | 21 | -------------------------------------------------------------------------------- /test/fixtures/external/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | External CSS Demo 5 | 6 | 7 | 15 |

My first styled page

16 |

Welcome to my styled page!

17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/fixtures/external/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-left: 11em; 3 | font-family: "Times New Roman", times, serif; 4 | color: purple; 5 | background-color: #d8da3d; 6 | } 7 | ul.navbar { 8 | list-style-type: none; 9 | padding: 0; 10 | margin: 0; 11 | position: absolute; 12 | top: 2em; 13 | left: 1em; 14 | width: 9em; 15 | } 16 | h1 { 17 | font-family: helvetica, arial, sans-serif; 18 | } 19 | ul.navbar li { 20 | background: white; 21 | margin: 0.5em 0; 22 | padding: 0.3em; 23 | border-right: 1em solid black; 24 | } 25 | ul.navbar a { 26 | text-decoration: none; 27 | } 28 | a:link { 29 | color: blue; 30 | } 31 | a:visited { 32 | color: purple; 33 | } 34 | footer { 35 | margin-top: 1em; 36 | padding-top: 1em; 37 | border-top: thin dotted; 38 | } 39 | .extra-style { 40 | font-size: 200%; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /test/fixtures/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic Demo 5 | 48 | 49 | 50 | 58 |

My first styled page

59 |

Welcome to my styled page!

60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /test/standalone.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import { compile, compileToHtml, readFile } from './_helpers'; 18 | 19 | function configure (config) { 20 | config.module.rules.push( 21 | { 22 | test: /\.css$/, 23 | loader: 'css-loader' 24 | }, 25 | { 26 | test: /\.html$/, 27 | loader: 'file-loader?name=[name].[ext]' 28 | } 29 | ); 30 | } 31 | 32 | test('webpack compilation', async () => { 33 | const info = await compile('fixtures/raw/index.js', configure); 34 | expect(info.assets).toHaveLength(2); 35 | expect(await readFile('fixtures/basic/dist/index.html')).toMatchSnapshot(); 36 | }); 37 | 38 | describe('Usage without html-webpack-plugin', () => { 39 | let output; 40 | beforeAll(async () => { 41 | output = await compileToHtml('raw', configure); 42 | }); 43 | 44 | it('should process the first html asset', () => { 45 | const { html, document } = output; 46 | expect(document.querySelectorAll('style')).toHaveLength(1); 47 | expect(document.getElementById('unused')).toBeNull(); 48 | expect(document.getElementById('used')).not.toBeNull(); 49 | expect(document.getElementById('used').textContent).toMatchSnapshot(); 50 | expect(html).toMatchSnapshot(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/__snapshots__/standalone.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Usage without html-webpack-plugin should process the first html asset 1`] = ` 4 | "h1 { 5 | color: green; 6 | }" 7 | `; 8 | 9 | exports[`Usage without html-webpack-plugin should process the first html asset 2`] = ` 10 | " 11 | Basic Demo 12 | 13 | 16 | 17 | 18 |

Some HTML Here

19 | 20 | 21 | " 22 | `; 23 | 24 | exports[`webpack compilation 1`] = ` 25 | " 26 | Basic Demo 27 | 72 | 73 | 74 | 82 |

My first styled page

83 |

Welcome to my styled page!

84 | 85 | 86 | 87 | " 88 | `; 89 | -------------------------------------------------------------------------------- /src/css.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import css from 'css'; 18 | 19 | /** 20 | * Parse a textual CSS Stylesheet into a Stylesheet instance. 21 | * Stylesheet is a mutable ReworkCSS AST with format similar to CSSOM. 22 | * @see https://github.com/reworkcss/css 23 | * @private 24 | * @param {String} stylesheet 25 | * @returns {css.Stylesheet} ast 26 | */ 27 | export function parseStylesheet (stylesheet) { 28 | return css.parse(stylesheet); 29 | } 30 | 31 | /** 32 | * Serialize a ReworkCSS Stylesheet to a String of CSS. 33 | * @private 34 | * @param {css.Stylesheet} ast A Stylesheet to serialize, such as one returned from `parseStylesheet()` 35 | * @param {Object} options Options to pass to `css.stringify()` 36 | * @param {Boolean} [options.compress] Compress CSS output (removes comments, whitespace, etc) 37 | */ 38 | export function serializeStylesheet (ast, options) { 39 | return css.stringify(ast, options); 40 | } 41 | 42 | /** 43 | * Recursively walk all rules in a stylesheet. 44 | * @private 45 | * @param {css.Rule} node A Stylesheet or Rule to descend into. 46 | * @param {Function} iterator Invoked on each node in the tree. Return `false` to remove that node. 47 | */ 48 | export function walkStyleRules (node, iterator) { 49 | if (node.stylesheet) return walkStyleRules(node.stylesheet, iterator); 50 | 51 | node.rules = node.rules.filter(rule => { 52 | if (rule.rules) { 53 | walkStyleRules(rule, iterator); 54 | } 55 | return iterator(rule) !== false; 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "critters-webpack-plugin", 3 | "version": "1.0.0", 4 | "description": "Webpack plugin to inline critical CSS and lazy-load the rest.", 5 | "main": "dist/critters.js", 6 | "source": "src/index.js", 7 | "license": "Apache-2.0", 8 | "author": "The Chromium Authors", 9 | "contributors": [ 10 | { 11 | "name": "Jason Miller", 12 | "email": "developit@google.com" 13 | } 14 | ], 15 | "keywords": [ 16 | "critical css", 17 | "inline css", 18 | "critical", 19 | "critters", 20 | "webpack plugin", 21 | "performance" 22 | ], 23 | "repository": "GoogleChromeLabs/critters", 24 | "scripts": { 25 | "build": "microbundle -f cjs --no-compress --external all", 26 | "docs": "documentation readme -q --no-markdown-toc -a public -s Usage --sort-order alpha src", 27 | "test": "jest --coverage" 28 | }, 29 | "babel": { 30 | "presets": [ 31 | "env" 32 | ] 33 | }, 34 | "jest": { 35 | "testEnvironment": "jsdom", 36 | "coverageReporters": [ 37 | "text" 38 | ], 39 | "collectCoverageFrom": [ 40 | "src/**/*" 41 | ], 42 | "watchPathIgnorePatterns": [ 43 | "node_modules", 44 | "dist" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "babel-core": "^6.26.0", 49 | "babel-jest": "^22.4.3", 50 | "babel-preset-env": "^1.6.1", 51 | "css-loader": "^0.28.11", 52 | "documentation": "^6.3.2", 53 | "eslint": "^4.19.1", 54 | "eslint-config-standard": "^11.0.0", 55 | "eslint-plugin-import": "^2.11.0", 56 | "eslint-plugin-jest": "^21.15.1", 57 | "eslint-plugin-node": "^6.0.1", 58 | "eslint-plugin-promise": "^3.7.0", 59 | "eslint-plugin-standard": "^3.0.1", 60 | "file-loader": "^1.1.11", 61 | "html-webpack-plugin": "^3.2.0", 62 | "jest": "^22.4.3", 63 | "jsdom": "^11.9.0", 64 | "microbundle": "^0.4.4", 65 | "mini-css-extract-plugin": "^0.4.0", 66 | "webpack": "^4.6.0" 67 | }, 68 | "dependencies": { 69 | "css": "^2.2.1", 70 | "nwmatcher": "^1.4.4", 71 | "parse5": "^4.0.0", 72 | "pretty-bytes": "^4.0.2", 73 | "webpack-sources": "^1.1.0" 74 | }, 75 | "eslintConfig": { 76 | "extends": [ 77 | "standard", 78 | "plugin:jest/recommended" 79 | ], 80 | "rules": { 81 | "indent": [ 82 | 2, 83 | 2 84 | ], 85 | "semi": [ 86 | 2, 87 | "always" 88 | ], 89 | "prefer-const": 1 90 | }, 91 | "globals": { 92 | "document": 0, 93 | "DOMParser": 1 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/_helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import { promisify } from 'util'; 18 | import fs from 'fs'; 19 | import path from 'path'; 20 | import webpack from 'webpack'; 21 | import Critters from '../src'; 22 | 23 | // parse a string into a JSDOM Document 24 | export const parseDom = html => new DOMParser().parseFromString(html, 'text/html'); 25 | 26 | // returns a promise resolving to the contents of a file 27 | export const readFile = file => promisify(fs.readFile)(path.resolve(__dirname, file), 'utf8'); 28 | 29 | // invoke webpack on a given entry module, optionally mutating the default configuration 30 | export function compile (entry, configDecorator) { 31 | return new Promise((resolve, reject) => { 32 | const context = path.dirname(path.resolve(__dirname, entry)); 33 | entry = path.basename(entry); 34 | let config = { 35 | context, 36 | entry: path.resolve(context, entry), 37 | output: { 38 | path: path.resolve(__dirname, path.resolve(context, 'dist')), 39 | filename: 'bundle.js', 40 | chunkFilename: '[name].chunk.js' 41 | }, 42 | resolveLoader: { 43 | modules: [path.resolve(__dirname, '../node_modules')] 44 | }, 45 | module: { 46 | rules: [] 47 | }, 48 | plugins: [] 49 | }; 50 | if (configDecorator) { 51 | config = configDecorator(config) || config; 52 | } 53 | webpack(config, (err, stats) => { 54 | if (err) return reject(err); 55 | const info = stats.toJson(); 56 | if (stats.hasErrors()) return reject(info.errors.join('\n')); 57 | resolve(info); 58 | }); 59 | }); 60 | } 61 | 62 | // invoke webpack via compile(), applying Critters to inline CSS and injecting `html` and `document` properties into the webpack build info. 63 | export async function compileToHtml (fixture, configDecorator, crittersOptions = {}) { 64 | const info = await compile(`fixtures/${fixture}/index.js`, config => { 65 | config = configDecorator(config) || config; 66 | config.plugins.push( 67 | new Critters({ 68 | compress: false, 69 | ...crittersOptions 70 | }) 71 | ); 72 | }); 73 | info.html = await readFile(`fixtures/${fixture}/dist/index.html`); 74 | info.document = parseDom(info.html); 75 | return info; 76 | } 77 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 18 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 19 | import { compile, compileToHtml, readFile } from './_helpers'; 20 | 21 | function configure (config) { 22 | config.module.rules.push({ 23 | test: /\.css$/, 24 | use: [ 25 | MiniCssExtractPlugin.loader, 26 | 'css-loader' 27 | ] 28 | }); 29 | 30 | config.plugins.push( 31 | new MiniCssExtractPlugin({ 32 | filename: '[name].css', 33 | chunkFilename: '[name].chunk.css' 34 | }), 35 | new HtmlWebpackPlugin({ 36 | filename: 'index.html', 37 | template: 'index.html', 38 | inject: true, 39 | compile: true 40 | }) 41 | ); 42 | } 43 | 44 | test('webpack compilation', async () => { 45 | const info = await compile('fixtures/basic/index.js', configure); 46 | expect(info.assets).toHaveLength(2); 47 | 48 | const html = await readFile('fixtures/basic/dist/index.html'); 49 | expect(html).toMatchSnapshot(); 50 | 51 | expect(html).toMatch(/\.extra-style/); 52 | }); 53 | 54 | describe('Inline 51 | 52 | 60 |

My first styled page

61 |

Welcome to my styled page!

62 | 63 | 64 | 65 | " 66 | `; 67 | 68 | exports[`Inline 122 | 123 | 124 | 132 |

My first styled page

133 |

Welcome to my styled page!

134 | 135 | 136 | 137 | " 138 | `; 139 | 140 | exports[`options { async:true } should match snapshot 1`] = ` 141 | " 142 | External CSS Demo 143 | 188 | 189 | 197 |

My first styled page

198 |

Welcome to my styled page!

199 | 200 | 201 | 202 | " 203 | `; 204 | 205 | exports[`webpack compilation 1`] = ` 206 | " 207 | 208 | 209 | Basic Demo 210 | 253 | 254 | 255 | 263 |

My first styled page

264 |

Welcome to my styled page!

265 | 266 | 267 | 268 | " 269 | `; 270 | -------------------------------------------------------------------------------- /src/dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import parse5 from 'parse5'; 18 | import nwmatcher from 'nwmatcher'; 19 | 20 | // htmlparser2 has a relatively DOM-like tree format, which we'll massage into a DOM elsewhere 21 | const treeAdapter = parse5.treeAdapters.htmlparser2; 22 | 23 | const PARSE5_OPTS = { 24 | treeAdapter 25 | }; 26 | 27 | /** 28 | * Parse HTML into a mutable, serializable DOM Document. 29 | * The DOM implementation is an htmlparser2 DOM enhanced with basic DOM mutation methods. 30 | * @param {String} html HTML to parse into a Document instance 31 | */ 32 | export function createDocument (html) { 33 | const document = parse5.parse(html, PARSE5_OPTS); 34 | 35 | defineProperties(document, DocumentExtensions); 36 | // Find the first element within the document 37 | 38 | // Extend Element.prototype with DOM manipulation methods. 39 | // Note: document.$$scratchElement is also used by createTextNode() 40 | const scratch = document.$$scratchElement = document.createElement('div'); 41 | const elementProto = Object.getPrototypeOf(scratch); 42 | defineProperties(elementProto, ElementExtensions); 43 | elementProto.ownerDocument = document; 44 | 45 | // nwmatcher is a selector engine that happens to work with Parse5's htmlparser2 DOM (they form the base of jsdom). 46 | // It is exposed to the document so that it can be used within Element.prototype methods. 47 | document.$match = nwmatcher({ document }); 48 | document.$match.configure({ 49 | CACHING: false, 50 | USE_QSAPI: false, 51 | USE_HTML5: false 52 | }); 53 | 54 | return document; 55 | } 56 | 57 | /** 58 | * Serialize a Document to an HTML String 59 | * @param {Document} document A Document, such as one created via `createDocument()` 60 | */ 61 | export function serializeDocument (document) { 62 | return parse5.serialize(document, PARSE5_OPTS); 63 | } 64 | 65 | /** 66 | * Methods and descriptors to mix into Element.prototype 67 | */ 68 | const ElementExtensions = { 69 | /** @extends htmlparser2.Element.prototype */ 70 | 71 | nodeName: { 72 | get () { 73 | return this.tagName.toUpperCase(); 74 | } 75 | }, 76 | 77 | id: reflectedProperty('id'), 78 | 79 | className: reflectedProperty('class'), 80 | 81 | insertBefore (child, referenceNode) { 82 | if (!referenceNode) return this.appendChild(child); 83 | treeAdapter.insertBefore(this, child, referenceNode); 84 | return child; 85 | }, 86 | 87 | appendChild (child) { 88 | treeAdapter.appendChild(this, child); 89 | return child; 90 | }, 91 | 92 | removeChild (child) { 93 | treeAdapter.detachNode(child); 94 | }, 95 | 96 | setAttribute (name, value) { 97 | if (this.attribs == null) this.attribs = {}; 98 | if (value == null) value = ''; 99 | this.attribs[name] = value; 100 | }, 101 | 102 | removeAttribute (name) { 103 | if (this.attribs != null) { 104 | delete this.attribs[name]; 105 | } 106 | }, 107 | 108 | getAttribute (name) { 109 | return this.attribs != null && this.attribs[name]; 110 | }, 111 | 112 | hasAttribute (name) { 113 | return this.attribs != null && this.attribs[name] != null; 114 | }, 115 | 116 | getAttributeNode (name) { 117 | const value = this.getAttribute(name); 118 | if (value != null) return { specified: true, value }; 119 | }, 120 | 121 | getElementsByTagName 122 | }; 123 | 124 | /** 125 | * Methods and descriptors to mix into the global document instance 126 | * @private 127 | */ 128 | const DocumentExtensions = { 129 | /** @extends htmlparser2.Document.prototype */ 130 | 131 | // document is just an Element in htmlparser2, giving it a nodeType of ELEMENT_NODE. 132 | // nwmatcher requires that it at least report a correct nodeType of DOCUMENT_NODE. 133 | nodeType: { 134 | get () { 135 | return 9; 136 | } 137 | }, 138 | 139 | nodeName: { 140 | get () { 141 | return '#document'; 142 | } 143 | }, 144 | 145 | documentElement: { 146 | get () { 147 | // Find the first element within the document 148 | return this.childNodes.filter(child => String(child.tagName).toLowerCase() === 'html')[0]; 149 | } 150 | }, 151 | 152 | body: { 153 | get () { 154 | return this.querySelector('body'); 155 | } 156 | }, 157 | 158 | createElement (name) { 159 | return treeAdapter.createElement(name, null, []); 160 | }, 161 | 162 | createTextNode (text) { 163 | // there is no dedicated createTextNode equivalent in htmlparser2's DOM, so 164 | // we have to insert Text and then remove and return the resulting Text node. 165 | const scratch = this.$$scratchElement; 166 | treeAdapter.insertText(scratch, text); 167 | const node = scratch.lastChild; 168 | treeAdapter.detachNode(node); 169 | return node; 170 | }, 171 | 172 | querySelector (sel) { 173 | return this.$match.first(sel, this.documentElement); 174 | }, 175 | 176 | querySelectorAll (sel) { 177 | return this.$match.select(sel, this.documentElement); 178 | }, 179 | 180 | getElementsByTagName, 181 | 182 | // Bugfix: nwmatcher uses inexistence of `document.addEventListener` to detect IE: 183 | // @see https://github.com/dperini/nwmatcher/blob/3edb471e12ce7f7d46dc1606c7f659ff45675a29/src/nwmatcher.js#L353 184 | addEventListener: Object 185 | }; 186 | 187 | /** 188 | * Essentially `Object.defineProperties()`, except function values are assigned as value descriptors for convenience. 189 | * @private 190 | */ 191 | function defineProperties (obj, properties) { 192 | for (const i in properties) { 193 | const value = properties[i]; 194 | Object.defineProperty(obj, i, typeof value === 'function' ? { value } : value); 195 | } 196 | } 197 | 198 | /** 199 | * A simple implementation of Element.prototype.getElementsByTagName(). 200 | * This is the only tree traversal method nwmatcher uses to implement its selector engine. 201 | * @private 202 | * @note 203 | * If perf issues arise, 2 faster but more verbose implementations are benchmarked here: 204 | * https://esbench.com/bench/5ac3b647f2949800a0f619e1 205 | */ 206 | function getElementsByTagName (tagName) { 207 | // Only return Element/Document nodes 208 | if ((this.nodeType !== 1 && this.nodeType !== 9) || this.type === 'directive') return []; 209 | return Array.prototype.concat.apply( 210 | // Add current element if it matches tag 211 | (tagName === '*' || (this.tagName && (this.tagName === tagName || this.nodeName === tagName.toUpperCase()))) ? [this] : [], 212 | // Check children recursively 213 | this.children.map(child => getElementsByTagName.call(child, tagName)) 214 | ); 215 | } 216 | 217 | /** 218 | * Create a property descriptor defining a getter/setter pair alias for a named attribute. 219 | * @private 220 | */ 221 | function reflectedProperty (attributeName) { 222 | return { 223 | get () { 224 | return this.getAttribute(attributeName); 225 | }, 226 | set (value) { 227 | this.setAttribute(attributeName, value); 228 | } 229 | }; 230 | } 231 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2018 Google Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import path from 'path'; 18 | import prettyBytes from 'pretty-bytes'; 19 | import sources from 'webpack-sources'; 20 | import { createDocument, serializeDocument } from './dom'; 21 | import { parseStylesheet, serializeStylesheet, walkStyleRules } from './css'; 22 | 23 | // Used to annotate this plugin's hooks in Tappable invocations 24 | const PLUGIN_NAME = 'critters-webpack-plugin'; 25 | 26 | /** 27 | * The mechanism to use for lazy-loading stylesheets. 28 | * _[JS]_ indicates that a strategy requires JavaScript (falls back to `