├── .editorconfig
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── babel.config.js
├── package.json
├── packages
├── critters-webpack-plugin
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── index.js
│ │ └── util.js
│ └── test
│ │ ├── __snapshots__
│ │ ├── index.test.js.snap
│ │ └── standalone.test.js.snap
│ │ ├── _helpers.js
│ │ ├── fixtures
│ │ ├── additionalStylesheets
│ │ │ ├── additional.css
│ │ │ ├── chunk.js
│ │ │ ├── index.html
│ │ │ ├── index.js
│ │ │ └── style.css
│ │ ├── basic
│ │ │ ├── index.html
│ │ │ └── index.js
│ │ ├── external
│ │ │ ├── index.html
│ │ │ ├── index.js
│ │ │ └── style.css
│ │ ├── inlineThreshold
│ │ │ ├── index.html
│ │ │ ├── index.js
│ │ │ └── style.css
│ │ ├── keyframes
│ │ │ ├── index.html
│ │ │ ├── index.js
│ │ │ └── style.css
│ │ ├── raw
│ │ │ ├── index.html
│ │ │ └── index.js
│ │ └── unused
│ │ │ ├── index.html
│ │ │ └── index.js
│ │ ├── index.test.js
│ │ └── standalone.test.js
└── critters
│ ├── README.md
│ ├── package.json
│ ├── src
│ ├── css.js
│ ├── dom.js
│ ├── index.d.ts
│ ├── index.js
│ ├── text.css
│ └── util.js
│ └── test
│ ├── __snapshots__
│ └── critters.test.js.snap
│ ├── critters.test.js
│ ├── security.test.js
│ └── src
│ ├── index.html
│ ├── media-validation.html
│ ├── styles.css
│ ├── styles2.css
│ └── subpath-validation.html
└── yarn.lock
/.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 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: actions/setup-node@v1
18 | with:
19 | node-version: 18
20 | - name: install, build, test
21 | run: |
22 | yarn --frozen-lockfile
23 | yarn build
24 | yarn test
25 | env:
26 | CI: true
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .DS_store
4 | next_sites
5 | coverage
6 | .vscode
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [v0.0.17](https://www.npmjs.com/package/critters/v/0.0.17)
2 |
3 | - Updated the HTML parser from parse5 to htmlparser2
4 | - Implemented caching to improve speed of matching class and id based selectors
5 | - Added option `includeSelectors`
6 | - Added option `reduceInlineStyles`
7 |
8 | Feature
9 | Include/Exclude CSS rules via comments in CSS
10 |
11 | Feature
12 | Adding Critters containers. Wrap the HTML contents in a `data-critters-container` container to indicate above the fold HTML.
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | packages/critters/README.md
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | targets: {
7 | node: 'current'
8 | }
9 | }
10 | ]
11 | ]
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "critters-root",
3 | "private": true,
4 | "description": "Inline critical CSS and lazy-load the rest.",
5 | "license": "Apache-2.0",
6 | "author": "The Chromium Authors",
7 | "contributors": [
8 | {
9 | "name": "Jason Miller",
10 | "email": "developit@google.com"
11 | },
12 | {
13 | "name": "Janicklas Ralph",
14 | "email": "janicklas@google.com"
15 | }
16 | ],
17 | "workspaces": {
18 | "packages": [
19 | "./packages/*"
20 | ],
21 | "nohoist": [
22 | "**/css-loader",
23 | "**/file-loader",
24 | "**/mini-css-extract-plugin",
25 | "**/webpack"
26 | ]
27 | },
28 | "scripts": {
29 | "build": "yarn workspaces run build",
30 | "build:main": "yarn workspace critters run build",
31 | "build:webpack": "yarn workspace critters-webpack-plugin run build",
32 | "docs": "yarn workspaces run docs",
33 | "release": "npm run build -s && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish",
34 | "test": "jest --coverage"
35 | },
36 | "engines": {
37 | "node": ">=18.13.0",
38 | "yarn": ">=1.21.1 <2",
39 | "npm": "Please use yarn instead of NPM to install dependencies"
40 | },
41 | "jest": {
42 | "testEnvironment": "node",
43 | "coverageReporters": [
44 | "text"
45 | ],
46 | "moduleNameMapper": {
47 | "^critters$": "/packages/critters/src/index.js",
48 | "#(.*)": "/node_modules/$1"
49 | },
50 | "collectCoverageFrom": [
51 | "packages/*/src/**/*.js"
52 | ],
53 | "modulePaths": [
54 | "/packages/critters-webpack-plugin/node_modules",
55 | "/packages/critters/node_modules"
56 | ],
57 | "watchPathIgnorePatterns": [
58 | "node_modules",
59 | "dist"
60 | ],
61 | "transformIgnorePatterns": []
62 | },
63 | "prettier": {
64 | "singleQuote": true,
65 | "trailingComma": "none"
66 | },
67 | "eslintConfig": {
68 | "extends": [
69 | "standard",
70 | "plugin:jest/recommended",
71 | "prettier"
72 | ],
73 | "globals": {
74 | "document": "readonly",
75 | "DOMParser": "readonly"
76 | },
77 | "parserOptions": {
78 | "ecmaFeatures": {
79 | "jsx": true,
80 | "modules": true
81 | }
82 | }
83 | },
84 | "devDependencies": {
85 | "@babel/preset-env": "^7.11.0",
86 | "@types/jest": "^26.0.23",
87 | "babel-core": "^6.26.0",
88 | "babel-jest": "^26.3.0",
89 | "cheerio": "^1.0.0-rc.12",
90 | "eslint": "^7.6.0",
91 | "eslint-config-prettier": "^6.11.0",
92 | "eslint-config-standard": "^14.1.1",
93 | "eslint-plugin-import": "^2.11.0",
94 | "eslint-plugin-jest": "^23.20.0",
95 | "eslint-plugin-node": "^11.1.0",
96 | "eslint-plugin-promise": "^4.2.1",
97 | "eslint-plugin-standard": "^4.0.1",
98 | "jest": "^26.3.0",
99 | "jsdom": "^16.6.0",
100 | "microbundle": "^0.12.3",
101 | "prettier": "^2.3.0"
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/packages/critters-webpack-plugin/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Critters Webpack plugin
4 |
5 |
6 | > critters-webpack-plugin inlines your app's [critical CSS] and lazy-loads the rest.
7 |
8 | ## critters-webpack-plugin [](https://www.npmjs.org/package/critters-webpack-plugin)
9 |
10 | It's a little different from [other options](#similar-libraries), because it **doesn't use a headless browser** to render content. This tradeoff allows Critters to be very **fast and lightweight**. It also means Critters inlines all CSS rules used by your document, rather than only those needed for above-the-fold content. For alternatives, see [Similar Libraries](#similar-libraries).
11 |
12 | Critters' design makes it a good fit when inlining critical CSS for prerendered/SSR'd Single Page Applications. It was developed to be an excellent compliment to [prerender-loader](https://github.com/GoogleChromeLabs/prerender-loader), combining to dramatically improve first paint time for most Single Page Applications.
13 |
14 | ## Features
15 |
16 | * Fast - no browser, few dependencies
17 | * Integrates with [html-webpack-plugin]
18 | * Works with `webpack-dev-server` / `webpack serve`
19 | * Supports preloading and/or inlining critical fonts
20 | * Prunes unused CSS keyframes and media queries
21 | * Removes inlined CSS rules from lazy-loaded stylesheets
22 |
23 | ## Installation
24 |
25 | First, install Critters as a development dependency:
26 |
27 | ```sh
28 | npm i -D critters-webpack-plugin
29 | ```
30 |
31 | Then, import Critters into your Webpack configuration and add it to your list of plugins:
32 |
33 | ```diff
34 | // webpack.config.js
35 | +const Critters = require('critters-webpack-plugin');
36 |
37 | module.exports = {
38 | plugins: [
39 | + new Critters({
40 | + // optional configuration (see below)
41 | + })
42 | ]
43 | }
44 | ```
45 |
46 | That's it! Now when you run Webpack, the CSS used by your HTML will be inlined and the imports for your full CSS will be converted to load asynchronously.
47 |
48 | ## Usage
49 |
50 |
51 |
52 | ### CrittersWebpackPlugin
53 |
54 | **Extends Critters**
55 |
56 | Create a Critters plugin instance with the given options.
57 |
58 | #### Parameters
59 |
60 | * `options` **Options** Options to control how Critters inlines CSS. See
61 |
62 | #### Examples
63 |
64 | ```javascript
65 | // webpack.config.js
66 | module.exports = {
67 | plugins: [
68 | new Critters({
69 | // Outputs:
70 | preload: 'swap',
71 |
72 | // Don't inline critical font-face rules, but preload the font URLs:
73 | preloadFonts: true
74 | })
75 | ]
76 | }
77 | ```
78 |
79 | ## Similar Libraries
80 |
81 | There are a number of other libraries that can inline Critical CSS, each with a slightly different approach. Here are a few great options:
82 |
83 | * [Critical](https://github.com/addyosmani/critical)
84 | * [Penthouse](https://github.com/pocketjoso/penthouse)
85 | * [webpack-critical](https://github.com/lukeed/webpack-critical)
86 | * [webpack-plugin-critical](https://github.com/nrwl/webpack-plugin-critical)
87 | * [html-critical-webpack-plugin](https://github.com/anthonygore/html-critical-webpack-plugin)
88 | * [react-snap](https://github.com/stereobooster/react-snap)
89 |
90 | ## License
91 |
92 | [Apache 2.0](LICENSE)
93 |
94 | This is not an official Google product.
95 |
96 | [critical css]: https://www.smashingmagazine.com/2015/08/understanding-critical-css/
97 |
98 | [html-webpack-plugin]: https://github.com/jantimon/html-webpack-plugin
99 |
--------------------------------------------------------------------------------
/packages/critters-webpack-plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "critters-webpack-plugin",
3 | "version": "3.0.2",
4 | "description": "Webpack plugin to inline critical CSS and lazy-load the rest.",
5 | "main": "dist/critters-webpack-plugin.js",
6 | "module": "dist/critters-webpack-plugin.mjs",
7 | "source": "src/index.js",
8 | "exports": {
9 | "import": "./dist/critters-webpack-plugin.mjs",
10 | "require": "./dist/critters-webpack-plugin.js",
11 | "default": "./dist/critters-webpack-plugin.mjs"
12 | },
13 | "files": [
14 | "src",
15 | "dist"
16 | ],
17 | "license": "Apache-2.0",
18 | "author": "The Chromium Authors",
19 | "contributors": [
20 | {
21 | "name": "Jason Miller",
22 | "email": "developit@google.com"
23 | },
24 | {
25 | "name": "Janicklas Ralph",
26 | "email": "janicklas@google.com"
27 | }
28 | ],
29 | "keywords": [
30 | "critical css",
31 | "inline css",
32 | "critical",
33 | "critters",
34 | "webpack plugin",
35 | "performance"
36 | ],
37 | "repository": {
38 | "type": "git",
39 | "url": "https://github.com/GoogleChromeLabs/critters",
40 | "directory": "packages/critters-webpack-plugin"
41 | },
42 | "scripts": {
43 | "build": "microbundle --target node --no-sourcemap -f cjs,esm",
44 | "docs": "documentation readme -q --no-markdown-toc -a public -s Usage --sort-order alpha src",
45 | "prepare": "npm run -s build"
46 | },
47 | "devDependencies": {
48 | "css-loader": "^4.2.1",
49 | "documentation": "^13.0.2",
50 | "file-loader": "^6.0.0",
51 | "html-webpack-plugin": "^4.5.2",
52 | "microbundle": "^0.12.3",
53 | "mini-css-extract-plugin": "^0.10.0",
54 | "webpack": "^4.46.0"
55 | },
56 | "dependencies": {
57 | "critters": "^0.0.16",
58 | "minimatch": "^3.0.4",
59 | "webpack-log": "^3.0.1",
60 | "webpack-sources": "^1.3.0"
61 | },
62 | "peerDependencies": {
63 | "html-webpack-plugin": "^4.5.2"
64 | },
65 | "peerDependenciesMeta": {
66 | "html-webpack-plugin": {
67 | "optional": true
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/packages/critters-webpack-plugin/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 { createRequire } from 'module';
19 | import minimatch from 'minimatch';
20 | import sources from 'webpack-sources';
21 | import log from 'webpack-log';
22 | import Critters from 'critters';
23 | import { tap } from './util';
24 |
25 | const $require =
26 | typeof require !== 'undefined'
27 | ? require
28 | : createRequire(eval('import.meta.url'));
29 |
30 | // Used to annotate this plugin's hooks in Tappable invocations
31 | const PLUGIN_NAME = 'critters-webpack-plugin';
32 |
33 | /** @typedef {import('critters').Options} Options */
34 |
35 | /**
36 | * Create a Critters plugin instance with the given options.
37 | * @public
38 | * @param {Options} options Options to control how Critters inlines CSS. See https://github.com/GoogleChromeLabs/critters#usage
39 | * @example
40 | * // webpack.config.js
41 | * module.exports = {
42 | * plugins: [
43 | * new Critters({
44 | * // Outputs:
45 | * preload: 'swap',
46 | *
47 | * // Don't inline critical font-face rules, but preload the font URLs:
48 | * preloadFonts: true
49 | * })
50 | * ]
51 | * }
52 | */
53 | export default class CrittersWebpackPlugin extends Critters {
54 | constructor(options) {
55 | super(options);
56 |
57 | // TODO: Remove webpack-log
58 | this.logger = log({
59 | name: 'Critters',
60 | unique: true,
61 | level: this.options.logLevel
62 | });
63 | }
64 |
65 | /**
66 | * Invoked by Webpack during plugin initialization
67 | */
68 | apply(compiler) {
69 | // hook into the compiler to get a Compilation instance...
70 | tap(compiler, 'compilation', PLUGIN_NAME, false, (compilation) => {
71 | this.options.path = compiler.options.output.path;
72 | this.options.publicPath = compiler.options.output.publicPath;
73 |
74 | const hasHtmlPlugin = compilation.options.plugins.find(
75 | (p) => p.constructor && p.constructor.name === 'HtmlWebpackPlugin'
76 | );
77 | try {
78 | var htmlPluginHooks = $require('html-webpack-plugin').getHooks(
79 | compilation
80 | );
81 | } catch (err) {}
82 |
83 | const handleHtmlPluginData = (htmlPluginData, callback) => {
84 | this.fs = compilation.outputFileSystem;
85 | this.compilation = compilation;
86 | this.process(htmlPluginData.html)
87 | .then((html) => {
88 | callback(null, { html });
89 | })
90 | .catch(callback);
91 | };
92 |
93 | // get an "after" hook into html-webpack-plugin's HTML generation.
94 | if (
95 | compilation.hooks &&
96 | compilation.hooks.htmlWebpackPluginAfterHtmlProcessing
97 | ) {
98 | tap(
99 | compilation,
100 | 'html-webpack-plugin-after-html-processing',
101 | PLUGIN_NAME,
102 | true,
103 | handleHtmlPluginData
104 | );
105 | } else if (hasHtmlPlugin && htmlPluginHooks) {
106 | htmlPluginHooks.beforeEmit.tapAsync(PLUGIN_NAME, handleHtmlPluginData);
107 | } else {
108 | // If html-webpack-plugin isn't used, process the first HTML asset as an optimize step
109 | tap(
110 | compilation,
111 | 'optimize-assets',
112 | PLUGIN_NAME,
113 | true,
114 | (assets, callback) => {
115 | this.fs = compilation.outputFileSystem;
116 | this.compilation = compilation;
117 |
118 | let htmlAssetName;
119 | for (const name in assets) {
120 | if (name.match(/\.html$/)) {
121 | htmlAssetName = name;
122 | break;
123 | }
124 | }
125 | if (!htmlAssetName) {
126 | return callback(Error('Could not find HTML asset.'));
127 | }
128 | const html = assets[htmlAssetName].source();
129 | if (!html) return callback(Error('Empty HTML asset.'));
130 |
131 | this.process(String(html))
132 | .then((html) => {
133 | assets[htmlAssetName] = new sources.RawSource(html);
134 | callback();
135 | })
136 | .catch(callback);
137 | }
138 | );
139 | }
140 | });
141 | }
142 |
143 | /**
144 | * Given href, find the corresponding CSS asset
145 | */
146 | async getCssAsset(href, style) {
147 | const outputPath = this.options.path;
148 | const publicPath = this.options.publicPath;
149 |
150 | // CHECK - the output path
151 | // path on disk (with output.publicPath removed)
152 | let normalizedPath = href.replace(/^\//, '');
153 | const pathPrefix = (publicPath || '').replace(/(^\/|\/$)/g, '') + '/';
154 | if (normalizedPath.indexOf(pathPrefix) === 0) {
155 | normalizedPath = normalizedPath
156 | .substring(pathPrefix.length)
157 | .replace(/^\//, '');
158 | }
159 | const filename = path.resolve(outputPath, normalizedPath);
160 |
161 | // try to find a matching asset by filename in webpack's output (not yet written to disk)
162 | const relativePath = path
163 | .relative(outputPath, filename)
164 | .replace(/^\.\//, '');
165 | const asset = this.compilation.assets[relativePath]; // compilation.assets[relativePath];
166 |
167 | // Attempt to read from assets, falling back to a disk read
168 | let sheet = asset && asset.source();
169 |
170 | if (!sheet) {
171 | try {
172 | sheet = await this.readFile(this.compilation, filename);
173 | this.logger.warn(
174 | `Stylesheet "${relativePath}" not found in assets, but a file was located on disk.${
175 | this.options.pruneSource
176 | ? ' This means pruneSource will not be applied.'
177 | : ''
178 | }`
179 | );
180 | } catch (e) {
181 | this.logger.warn(`Unable to locate stylesheet: ${relativePath}`);
182 | return;
183 | }
184 | }
185 |
186 | style.$$asset = asset;
187 | style.$$assetName = relativePath;
188 | // style.$$assets = this.compilation.assets;
189 |
190 | return sheet;
191 | }
192 |
193 | checkInlineThreshold(link, style, sheet) {
194 | const inlined = super.checkInlineThreshold(link, style, sheet);
195 |
196 | if (inlined) {
197 | const asset = style.$$asset;
198 | if (asset) {
199 | delete this.compilation.assets[style.$$assetName];
200 | } else {
201 | this.logger.warn(
202 | ` > ${style.$$name} was not found in assets. the resource may still be emitted but will be unreferenced.`
203 | );
204 | }
205 | }
206 |
207 | return inlined;
208 | }
209 |
210 | /**
211 | * Inline the stylesheets from options.additionalStylesheets (assuming it passes `options.filter`)
212 | */
213 | async embedAdditionalStylesheet(document) {
214 | const styleSheetsIncluded = [];
215 | (this.options.additionalStylesheets || []).forEach((cssFile) => {
216 | if (styleSheetsIncluded.includes(cssFile)) {
217 | return;
218 | }
219 | styleSheetsIncluded.push(cssFile);
220 | const webpackCssAssets = Object.keys(this.compilation.assets).filter(
221 | (file) => minimatch(file, cssFile)
222 | );
223 | webpackCssAssets.map((asset) => {
224 | const style = document.createElement('style');
225 | style.$$external = true;
226 | style.textContent = this.compilation.assets[asset].source();
227 | document.head.appendChild(style);
228 | });
229 | });
230 | }
231 |
232 | /**
233 | * Prune the source CSS files
234 | */
235 | pruneSource(style, before, sheetInverse) {
236 | const isStyleInlined = super.pruneSource(style, before, sheetInverse);
237 | const asset = style.$$asset;
238 | const name = style.$$name;
239 |
240 | if (asset) {
241 | // if external stylesheet would be below minimum size, just inline everything
242 | const minSize = this.options.minimumExternalSize;
243 | if (minSize && sheetInverse.length < minSize) {
244 | // delete the webpack asset:
245 | delete this.compilation.assets[style.$$assetName];
246 | return true;
247 | }
248 | this.compilation.assets[style.$$assetName] =
249 | new sources.LineToLineMappedSource(
250 | sheetInverse,
251 | style.$$assetName,
252 | before
253 | );
254 | } else {
255 | this.logger.warn(
256 | 'pruneSource is enabled, but a style (' +
257 | name +
258 | ') has no corresponding Webpack asset.'
259 | );
260 | }
261 |
262 | return isStyleInlined;
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/packages/critters-webpack-plugin/src/util.js:
--------------------------------------------------------------------------------
1 | export function tap(inst, hook, pluginName, async, callback) {
2 | if (inst.hooks) {
3 | const camel = hook.replace(/-([a-z])/g, (s, i) => i.toUpperCase());
4 | inst.hooks[camel][async ? 'tapAsync' : 'tap'](pluginName, callback);
5 | } else {
6 | inst.plugin(hook, callback);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/critters-webpack-plugin/test/__snapshots__/index.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`External CSS should match snapshot 1`] = `
4 | "
5 |
6 |
7 | External CSS Demo
8 |
57 |
58 |