├── .eslintignore ├── .prettierignore ├── index.js ├── test ├── fixtures-no-preserve │ ├── atRule.non-critical.expected.css │ ├── media.non-critical.expected.css │ ├── scope.non-critical.expected.css │ ├── this.non-critical.expected.css │ ├── atRule.non-critical.actual.css │ ├── default.non-critical.actual.css │ ├── default.non-critical.expected.css │ ├── media-scope.non-critical.expected.css │ ├── media.non-critical.actual.css │ ├── scope.non-critical.actual.css │ ├── this.non-critical.actual.css │ ├── media-scope.non-critical.actual.css │ ├── this.critical.actual.css │ ├── this.critical.expected.css │ ├── atRule-wrapping.critical.actual.css │ ├── scope.critical.actual.css │ ├── scope.critical.expected.css │ ├── atRule-wrapping.critical.expected.css │ ├── critical.css │ ├── media.critical.actual.css │ ├── media.critical.expected.css │ ├── default.critical.expected.css │ ├── atRule-wrapping.non-critical.actual.css │ ├── atRule-wrapping.non-critical.expected.css │ ├── atRule.critical.actual.css │ ├── media-this.critical.actual.css │ ├── atRule.critical.expected.css │ ├── media-scope.critical.actual.css │ ├── media-this.critical.expected.css │ ├── media-scope.critical.expected.css │ ├── default.css │ ├── this.css │ ├── scope.css │ ├── media.css │ ├── atRule.css │ ├── atRule-wrapping.css │ ├── media-this.non-critical.actual.css │ ├── media-this.non-critical.expected.css │ ├── media-scope.css │ └── media-this.css ├── fixtures │ ├── this.critical.actual.css │ ├── this.critical.expected.css │ ├── atRule-wrapping.critical.actual.css │ ├── scope.critical.actual.css │ ├── atRule-wrapping.critical.expected.css │ ├── critical.css │ ├── scope.critical.expected.css │ ├── media.critical.actual.css │ ├── media.critical.expected.css │ ├── this.non-critical.actual.css │ ├── this.non-critical.expected.css │ ├── default.critical.expected.css │ ├── atRule.critical.actual.css │ ├── atRule.expected.css │ ├── media-this.critical.actual.css │ ├── scope.non-critical.actual.css │ ├── atRule.critical.expected.css │ ├── media-scope.critical.actual.css │ ├── media-this.critical.expected.css │ ├── scope.non-critical.expected.css │ ├── media-scope.critical.expected.css │ ├── media.non-critical.actual.css │ ├── media.non-critical.expected.css │ ├── default.css │ ├── default.non-critical.actual.css │ ├── default.non-critical.expected.css │ ├── this.css │ ├── atRule-wrapping.non-critical.actual.css │ ├── atRule-wrapping.non-critical.expected.css │ ├── media-scope.non-critical.actual.css │ ├── atRule.non-critical.actual.css │ ├── media-scope.non-critical.expected.css │ ├── atRule.non-critical.expected.css │ ├── scope.css │ ├── media.css │ ├── atRule.css │ ├── atRule-wrapping.css │ ├── media-scope.css │ ├── media-this.non-critical.actual.css │ ├── media-this.non-critical.expected.css │ └── media-this.css ├── fixtures-output-dest │ ├── custom.css │ ├── custom.non-critical.actual.css │ ├── default.critical.expected.css │ ├── default.css │ ├── default.non-critical.actual.css │ └── default.non-critical.expected.css ├── fixtures-output-path │ ├── critical.css │ ├── default.critical.expected.css │ ├── default.css │ ├── default.non-critical.actual.css │ └── default.non-critical.expected.css ├── fixtures-no-minify │ ├── critical.css │ ├── this.critical.actual.css │ ├── default.css │ ├── default.non-critical.actual.css │ ├── default.non-critical.expected.css │ ├── this.critical.expected.css │ ├── this.non-critical.actual.css │ ├── default.critical.expected.css │ ├── this.non-critical.expected.css │ └── this.css ├── preTest.js └── index.js ├── critical.css ├── .flowconfig ├── .gitignore ├── commitlint.config.js ├── default.critical.expected.css ├── .babelrc ├── default.non-critical.actual.css ├── .npmignore ├── default.non-critical.expected.css ├── default.css ├── example ├── example.css └── example.js ├── src ├── getCriticalDestination.js ├── matchChild.js ├── atRule.js ├── getChildRules.js ├── getCriticalRules.js └── index.js ├── .github └── workflows │ └── nodejs.yml ├── CHANGELOG.md ├── .eslintrc ├── LICENSE ├── CONTRIBUTING.md ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./dist"); 2 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/atRule.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/scope.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/this.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/atRule.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/default.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/default.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media-scope.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/scope.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/this.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /critical.css: -------------------------------------------------------------------------------- 1 | .foo{color:#32cd32}.foo:before{content:"Hello, World."} -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media-scope.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/this.critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo{color:red;width:100%} -------------------------------------------------------------------------------- /test/fixtures/this.critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo{color:red;width:100%} 2 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [untyped] 2 | .*/node_modules/postcss 3 | .*/node_modules/chalk -------------------------------------------------------------------------------- /test/fixtures-no-preserve/this.critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo{color:red;width:100%} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | dist/ 4 | node_modules/ 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/this.critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo{color:red;width:100%} 2 | -------------------------------------------------------------------------------- /test/fixtures/atRule-wrapping.critical.actual.css: -------------------------------------------------------------------------------- 1 | .bar{color:tomato;height:200px} -------------------------------------------------------------------------------- /test/fixtures/scope.critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo .bar{color:green}.foo{color:orange} -------------------------------------------------------------------------------- /test/fixtures/atRule-wrapping.critical.expected.css: -------------------------------------------------------------------------------- 1 | .bar{color:tomato;height:200px} 2 | -------------------------------------------------------------------------------- /test/fixtures/critical.css: -------------------------------------------------------------------------------- 1 | .default{color:orange;float:left;margin-right:1em;width:100%} -------------------------------------------------------------------------------- /test/fixtures/scope.critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo .bar{color:green}.foo{color:orange} 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /default.critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo{color:#32cd32}.foo:before{content:"Hello, World."} 2 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/atRule-wrapping.critical.actual.css: -------------------------------------------------------------------------------- 1 | .bar{color:tomato;height:200px} -------------------------------------------------------------------------------- /test/fixtures-no-preserve/scope.critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo .bar{color:green}.foo{color:orange} -------------------------------------------------------------------------------- /test/fixtures/media.critical.actual.css: -------------------------------------------------------------------------------- 1 | @media (min-width:700px){.foo{color:red;width:100%}} -------------------------------------------------------------------------------- /test/fixtures-no-preserve/scope.critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo .bar{color:green}.foo{color:orange} 2 | -------------------------------------------------------------------------------- /test/fixtures-output-dest/custom.css: -------------------------------------------------------------------------------- 1 | .default{color:orange;float:left;margin-right:1em;width:100%} -------------------------------------------------------------------------------- /test/fixtures/media.critical.expected.css: -------------------------------------------------------------------------------- 1 | @media (min-width:700px){.foo{color:red;width:100%}} 2 | -------------------------------------------------------------------------------- /test/fixtures/this.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/this.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/atRule-wrapping.critical.expected.css: -------------------------------------------------------------------------------- 1 | .bar{color:tomato;height:200px} 2 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/critical.css: -------------------------------------------------------------------------------- 1 | .default{color:orange;float:left;margin-right:1em;width:100%} -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media.critical.actual.css: -------------------------------------------------------------------------------- 1 | @media (min-width:700px){.foo{color:red;width:100%}} -------------------------------------------------------------------------------- /test/fixtures-output-path/critical.css: -------------------------------------------------------------------------------- 1 | .default{color:orange;float:left;margin-right:1em;width:100%} -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media.critical.expected.css: -------------------------------------------------------------------------------- 1 | @media (min-width:700px){.foo{color:red;width:100%}} 2 | -------------------------------------------------------------------------------- /test/fixtures/default.critical.expected.css: -------------------------------------------------------------------------------- 1 | .default{color:orange;float:left;margin-right:1em;width:100%} 2 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/default.critical.expected.css: -------------------------------------------------------------------------------- 1 | .default{color:orange;float:left;margin-right:1em;width:100%} -------------------------------------------------------------------------------- /test/fixtures-output-dest/custom.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .default{color:orange;float:left;margin-right:1em;width:100%} -------------------------------------------------------------------------------- /test/fixtures-no-preserve/atRule-wrapping.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: gold; 3 | height: 100px; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures-output-dest/default.critical.expected.css: -------------------------------------------------------------------------------- 1 | .default{color:orange;float:left;margin-right:1em;width:100%} 2 | -------------------------------------------------------------------------------- /test/fixtures-output-path/default.critical.expected.css: -------------------------------------------------------------------------------- 1 | .default{color:orange;float:left;margin-right:1em;width:100%} 2 | -------------------------------------------------------------------------------- /test/fixtures/atRule.critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo{display:flex;width:calc(100% - 200px)}.bar{border:10px solid gold;height:100%} -------------------------------------------------------------------------------- /test/fixtures/atRule.expected.css: -------------------------------------------------------------------------------- 1 | .foo{display:flex;width:calc(100% - 200px)}.bar{border:10px solid gold;height:100%} 2 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/atRule-wrapping.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: gold; 3 | height: 100px; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/media-this.critical.actual.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width:1024px){.foo{display:flex;width:calc(100% - 200px)}} -------------------------------------------------------------------------------- /test/fixtures/scope.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo .bar { 2 | color: green; 3 | } 4 | 5 | .foo { 6 | color: orange; 7 | } 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-flow-strip-types", "transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /default.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: limegreen; 3 | } 4 | 5 | .foo::before { 6 | content: 'Hello, World.' 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/atRule.critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo{display:flex;width:calc(100% - 200px)}.bar{border:10px solid gold;height:100%} 2 | -------------------------------------------------------------------------------- /test/fixtures/media-scope.critical.actual.css: -------------------------------------------------------------------------------- 1 | header.top{background:red}@media screen and (max-width:400px){header.top{width:100%}} -------------------------------------------------------------------------------- /test/fixtures/media-this.critical.expected.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width:1024px){.foo{display:flex;width:calc(100% - 200px)}} 2 | -------------------------------------------------------------------------------- /test/fixtures/scope.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo .bar { 2 | color: green; 3 | } 4 | 5 | .foo { 6 | color: orange; 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist/**/* 3 | !CHANGELOG.md 4 | !CONTRIBUTING.md 5 | !index.js 6 | !package.json 7 | !package-lock.json 8 | !README.md -------------------------------------------------------------------------------- /default.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: limegreen; 3 | } 4 | 5 | .foo::before { 6 | content: 'Hello, World.' 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/atRule.critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo{display:flex;width:calc(100% - 200px)}.bar{border:10px solid gold;height:100%} -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media-this.critical.actual.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width:1024px){.foo{display:flex;width:calc(100% - 200px)}} -------------------------------------------------------------------------------- /test/fixtures/media-scope.critical.expected.css: -------------------------------------------------------------------------------- 1 | header.top{background:red}@media screen and (max-width:400px){header.top{width:100%}} 2 | -------------------------------------------------------------------------------- /default.css: -------------------------------------------------------------------------------- 1 | @critical; 2 | 3 | .foo { 4 | color: limegreen; 5 | } 6 | 7 | .foo::before { 8 | content: 'Hello, World.' 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/atRule.critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo{display:flex;width:calc(100% - 200px)}.bar{border:10px solid gold;height:100%} 2 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media-scope.critical.actual.css: -------------------------------------------------------------------------------- 1 | header.top{background:red}@media screen and (max-width:400px){header.top{width:100%}} -------------------------------------------------------------------------------- /test/fixtures/media.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 700px) { 2 | .foo { 3 | color: red; 4 | width: 100%; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-no-minify/critical.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .default { 4 | color: orange; 5 | float: left; 6 | margin-right: 1em; 7 | width: 100%; 8 | } -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media-this.critical.expected.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width:1024px){.foo{display:flex;width:calc(100% - 200px)}} 2 | -------------------------------------------------------------------------------- /test/fixtures/media.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 700px) { 2 | .foo { 3 | color: red; 4 | width: 100%; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-no-minify/this.critical.actual.css: -------------------------------------------------------------------------------- 1 | .testing-this { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media-scope.critical.expected.css: -------------------------------------------------------------------------------- 1 | header.top{background:red}@media screen and (max-width:400px){header.top{width:100%}} 2 | -------------------------------------------------------------------------------- /test/fixtures/default.css: -------------------------------------------------------------------------------- 1 | @critical; 2 | 3 | .default { 4 | color: orange; 5 | float: left; 6 | margin-right: 1em; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/default.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .default { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/default.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .default { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-no-minify/default.css: -------------------------------------------------------------------------------- 1 | @critical; 2 | 3 | .default { 4 | color: orange; 5 | float: left; 6 | margin-right: 1em; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures-no-minify/default.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .default { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-no-minify/default.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .default { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-no-minify/this.critical.expected.css: -------------------------------------------------------------------------------- 1 | .testing-this { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-no-minify/this.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .testing-this { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/default.css: -------------------------------------------------------------------------------- 1 | @critical; 2 | 3 | .default { 4 | color: orange; 5 | float: left; 6 | margin-right: 1em; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures-output-dest/default.css: -------------------------------------------------------------------------------- 1 | @critical; 2 | 3 | .default { 4 | color: orange; 5 | float: left; 6 | margin-right: 1em; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures-output-dest/default.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .default { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-output-path/default.css: -------------------------------------------------------------------------------- 1 | @critical; 2 | 3 | .default { 4 | color: orange; 5 | float: left; 6 | margin-right: 1em; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures-output-path/default.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .default { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/this.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | critical-selector: this; 4 | width: 100%; 5 | critical-filename: 'this.critical.actual.css'; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-no-minify/default.critical.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .default { 4 | color: orange; 5 | float: left; 6 | margin-right: 1em; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures-no-minify/this.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .testing-this { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-output-dest/default.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .default { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-output-path/default.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .default { 2 | color: orange; 3 | float: left; 4 | margin-right: 1em; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/this.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | critical-selector: this; 4 | width: 100%; 5 | critical-filename: 'this.critical.actual.css'; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/atRule-wrapping.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: gold; 3 | height: 100px; 4 | } 5 | 6 | .bar { 7 | color: tomato; 8 | height: 200px; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/atRule-wrapping.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: gold; 3 | height: 100px; 4 | } 5 | 6 | .bar { 7 | color: tomato; 8 | height: 200px; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/media-scope.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | header.top { 2 | background: red; 3 | } 4 | @media screen and (max-width: 400px) { 5 | header.top { 6 | width: 100%; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/atRule.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | display: flex; 3 | width: calc(100% - 200px); 4 | } 5 | 6 | .bar { 7 | border: 10px solid gold; 8 | height: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/media-scope.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | header.top { 2 | background: red; 3 | } 4 | @media screen and (max-width: 400px) { 5 | header.top { 6 | width: 100%; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/atRule.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | display: flex; 3 | width: calc(100% - 200px); 4 | } 5 | 6 | .bar { 7 | border: 10px solid gold; 8 | height: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/scope.css: -------------------------------------------------------------------------------- 1 | .foo .bar { 2 | color: green; 3 | } 4 | 5 | .foo { 6 | color: orange; 7 | critical-selector: scope; 8 | critical-filename: scope.critical.actual.css; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/media.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 700px) { 2 | .foo { 3 | color: red; 4 | critical-selector: this; 5 | width: 100%; 6 | critical-filename: 'media.critical.actual.css'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/scope.css: -------------------------------------------------------------------------------- 1 | .foo .bar { 2 | color: green; 3 | } 4 | 5 | .foo { 6 | color: orange; 7 | critical-selector: scope; 8 | critical-filename: scope.critical.actual.css; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures-no-minify/this.css: -------------------------------------------------------------------------------- 1 | .testing-this { 2 | critical-filename: this.critical.actual.css; 3 | critical-selector: this; 4 | color: orange; 5 | float: left; 6 | margin-right: 1em; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 700px) { 2 | .foo { 3 | color: red; 4 | critical-selector: this; 5 | width: 100%; 6 | critical-filename: 'media.critical.actual.css'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/atRule.css: -------------------------------------------------------------------------------- 1 | @critical atRule.critical.actual.css; 2 | 3 | .foo { 4 | display: flex; 5 | width: calc(100% - 200px); 6 | } 7 | 8 | .bar { 9 | border: 10px solid gold; 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/atRule-wrapping.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: gold; 3 | height: 100px; 4 | } 5 | 6 | @critical atRule-wrapping.critical.actual.css { 7 | .bar { 8 | color: tomato; 9 | height: 200px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/atRule.css: -------------------------------------------------------------------------------- 1 | @critical atRule.critical.actual.css; 2 | 3 | .foo { 4 | display: flex; 5 | width: calc(100% - 200px); 6 | } 7 | 8 | .bar { 9 | border: 10px solid gold; 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/atRule-wrapping.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: gold; 3 | height: 100px; 4 | } 5 | 6 | @critical atRule-wrapping.critical.actual.css { 7 | .bar { 8 | color: tomato; 9 | height: 200px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/media-scope.css: -------------------------------------------------------------------------------- 1 | header.top { 2 | critical-filename: media-scope.critical.actual.css; 3 | critical-selector: scope; 4 | background: red; 5 | } 6 | @media screen and (max-width: 400px) { 7 | header.top { 8 | width: 100%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/example.css: -------------------------------------------------------------------------------- 1 | @critical; 2 | 3 | .foo { 4 | display: flex; 5 | width: calc(100% - 200px); 6 | } 7 | 8 | .bar { 9 | border: 10px solid gold; 10 | height: 100%; 11 | } 12 | 13 | .baz::before { 14 | content: 'test'; 15 | position: fixed; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media-this.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 1024px) { 2 | 3 | .bar { 4 | border: 10px solid gold; 5 | height: 100%; 6 | } 7 | 8 | .baz::before { 9 | content: 'test'; 10 | position: fixed; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media-this.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 1024px) { 2 | 3 | .bar { 4 | border: 10px solid gold; 5 | height: 100%; 6 | } 7 | 8 | .baz::before { 9 | content: 'test'; 10 | position: fixed; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media-scope.css: -------------------------------------------------------------------------------- 1 | header.top { 2 | critical-filename: media-scope.critical.actual.css; 3 | critical-selector: scope; 4 | background: red; 5 | } 6 | @media screen and (max-width: 400px) { 7 | header.top { 8 | width: 100%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/media-this.non-critical.actual.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 1024px) { 2 | .foo { 3 | display: flex; 4 | width: calc(100% - 200px); 5 | } 6 | 7 | .bar { 8 | border: 10px solid gold; 9 | height: 100%; 10 | } 11 | 12 | .baz::before { 13 | content: 'test'; 14 | position: fixed; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/media-this.non-critical.expected.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 1024px) { 2 | .foo { 3 | display: flex; 4 | width: calc(100% - 200px); 5 | } 6 | 7 | .bar { 8 | border: 10px solid gold; 9 | height: 100%; 10 | } 11 | 12 | .baz::before { 13 | content: 'test'; 14 | position: fixed; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/media-this.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 1024px) { 2 | .foo { 3 | critical-selector: this; 4 | critical-filename: media-this.critical.actual.css; 5 | display: flex; 6 | width: calc(100% - 200px); 7 | } 8 | 9 | .bar { 10 | border: 10px solid gold; 11 | height: 100%; 12 | } 13 | 14 | .baz::before { 15 | content: 'test'; 16 | position: fixed; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures-no-preserve/media-this.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 1024px) { 2 | .foo { 3 | critical-selector: this; 4 | critical-filename: media-this.critical.actual.css; 5 | display: flex; 6 | width: calc(100% - 200px); 7 | } 8 | 9 | .bar { 10 | border: 10px solid gold; 11 | height: 100%; 12 | } 13 | 14 | .baz::before { 15 | content: 'test'; 16 | position: fixed; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/getCriticalDestination.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Identify critical CSS destinations. 5 | * 6 | * @param {object} rule PostCSS rule. 7 | * @param {string} Default output CSS file name. 8 | * @return {string} String corresponding to output destination. 9 | */ 10 | export function getCriticalDestination(rule: Object, dest: string): string { 11 | rule.walkDecls("critical-filename", (decl: Object) => { 12 | dest = decl.value.replace(/['"]*/g, ""); 13 | decl.remove(); 14 | }); 15 | return dest; 16 | } 17 | -------------------------------------------------------------------------------- /src/matchChild.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Get rules for selectors nested within parent node 5 | * 6 | * @param {obj} PostCSS CSS object 7 | * @return {object} Parent rule for which children should be included 8 | */ 9 | export function matchChild(parent: Object, rule: Object): boolean { 10 | const childRegExp = new RegExp(`(, )?(${parent.selector} [^,\s]*),?.*`); // eslint-disable-line no-useless-escape 11 | return ( 12 | rule.selector !== parent.selector && 13 | rule.selector.match(childRegExp) !== null 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [8.x, 10.x, 12.x, 14.x, 15.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install, build, and test 20 | run: | 21 | npm ci 22 | npm run build --if-present 23 | npm test 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [3.0.7](https://github.com/zgreen/postcss-critical-css/compare/v3.0.6...v3.0.7) (2020-12-29) 6 | 7 | ### [3.0.6](https://github.com/zgreen/postcss-critical-css/compare/v3.0.6-0...v3.0.6) (2020-01-02) 8 | 9 | ### [3.0.6-0](https://github.com/zgreen/postcss-critical-css/compare/v3.0.3...v3.0.6-0) (2019-12-30) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * require dist ([877bc95](https://github.com/zgreen/postcss-critical-css/commit/877bc9559ec088c27f139612930cc4477aa4cd89)) 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard", "plugin:prettier/recommended"], 3 | "plugins": ["flowtype"], 4 | "parser": "babel-eslint", 5 | "rules": { 6 | "flowtype/define-flow-type": 1, 7 | "flowtype/require-parameter-type": 1, 8 | "flowtype/require-return-type": [ 9 | 1, 10 | "always", 11 | { 12 | "annotateUndefined": "never" 13 | } 14 | ], 15 | "flowtype/space-after-type-colon": [1, "always"], 16 | "flowtype/space-before-type-colon": [1, "never"], 17 | "flowtype/type-id-match": [1, "^([A-Z][a-z0-9]+)+Type$"], 18 | "flowtype/use-flow-type": 1, 19 | "flowtype/valid-syntax": 1, 20 | "import/prefer-default-export": 0 21 | }, 22 | "settings": { 23 | "flowtype": { 24 | "onlyFilesWithFlowAnnotation": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/atRule.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import postcss from "postcss"; 3 | /** 4 | * Get critical CSS from an at-rule. 5 | * 6 | * @param {Object} args Function args. See flow type alias. 7 | */ 8 | export function getCriticalFromAtRule(args: Object): Object { 9 | const result: Object = {}; 10 | const options = { 11 | defaultDest: "critical.css", 12 | css: postcss.root(), 13 | ...args 14 | }; 15 | 16 | options.css.walkAtRules("critical", (atRule: Object) => { 17 | const name = atRule.params ? atRule.params : options.defaultDest; 18 | // If rule has no nodes, all the nodes of the parent will be critical. 19 | let rule = atRule; 20 | if (!atRule.nodes) { 21 | rule = atRule.root(); 22 | } 23 | rule.clone().each((node: Object) => { 24 | if (node.name !== "critical") { 25 | result[name] = result[name] 26 | ? result[name].append(node) 27 | : postcss.root().append(node); 28 | } 29 | }); 30 | }); 31 | return result; 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016 Zachary Green 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require("fs"); 3 | var postcss = require("postcss"); 4 | var postcssCriticalCSS = require(".."); 5 | 6 | const basePath = `${process.cwd()}/example`; 7 | function cb(files) { 8 | function useFileData(data, file) { 9 | postcss([postcssCriticalCSS({ outputPath: basePath })]) 10 | .process(data, { from: undefined }) 11 | .then(result => { 12 | fs.writeFile( 13 | `${basePath}/${file.split(".")[0]}.non-critical.css`, 14 | result.css, 15 | err => { 16 | if (err) { 17 | console.error(`ERROR: `, err); 18 | process.exit(1); 19 | } 20 | } 21 | ); 22 | }); 23 | } 24 | files.forEach(function(file) { 25 | if (file === "example.css") { 26 | fs.readFile(`${basePath}/${file}`, "utf8", (err, data) => { 27 | if (err) { 28 | throw new Error(err); 29 | } 30 | useFileData(data, file); 31 | }); 32 | } 33 | }); 34 | } 35 | 36 | fs.readdir(basePath, "utf8", (err, files) => { 37 | if (err) { 38 | throw new Error(err); 39 | } 40 | cb(files); 41 | }); 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to postcss-critical-css 2 | 3 | Thank you for your contribution! 4 | 5 | - Issues are welcome. 6 | - PRs are welcome. 7 | 8 | If you'd like to propose a major change, please open an issue first so that the change can be discussed. 9 | 10 | ## Linting, type checking, and testing 11 | 12 | Code is linted using [eslint](https://eslint.org/), type-checked using [flow](https://flow.org/), and tested using [tape](https://github.com/substack/tape). 13 | 14 | You can lint your code locally using the following command: 15 | 16 | ```sh 17 | npm run eslint 18 | ``` 19 | 20 | You can type check your code locally using the following command: 21 | 22 | ```sh 23 | npm run flow 24 | ``` 25 | 26 | You can test your code locally using the following command: 27 | 28 | ```sh 29 | npm run test 30 | ``` 31 | 32 | ## Commits 33 | 34 | To ensure proper versioning, commits to are linted using [commitlint](https://commitlint.js.org). 35 | 36 | [prettier](https://prettier.io/) is run via a hook on every commit. 37 | 38 | ## Building 39 | 40 | Source code is compliled using [babel](https://babeljs.io/). You can run the build locally using the following command: 41 | 42 | ```sh 43 | npm run build 44 | ``` 45 | 46 | Note that code that fails either the linting and/or type-checking step will fail to build successfully. 47 | 48 | ## CI 49 | 50 | All commits pushed to the remote repository are tested using [Github actions](https://github.com/features/actions). 51 | -------------------------------------------------------------------------------- /src/getChildRules.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import postcss from "postcss"; 4 | import { matchChild } from "./matchChild"; 5 | 6 | /** 7 | * Get rules for selectors nested within parent node 8 | * 9 | * @param {Object} PostCSS CSS object 10 | * @param {Object} Parent rule for which children should be included 11 | * @return {array} Array of child rules. 12 | */ 13 | export function getChildRules(css: Object, parent: Object): Array { 14 | const result = []; 15 | const selectorRegExp: Object = new RegExp(parent.selector); 16 | 17 | // Walk all rules to mach child selectors 18 | css.walkRules(selectorRegExp, (rule: Object) => { 19 | const childRule = matchChild(parent, rule); 20 | if (childRule) { 21 | result.push(rule); 22 | } 23 | }); 24 | 25 | // Walk all at-rules to match nested child selectors 26 | css.walkAtRules((atRule: Object) => { 27 | atRule.walkRules(selectorRegExp, (rule: Object) => { 28 | const childRule = matchChild(parent, rule); 29 | // Create new at-rule to append only necessary selector to critical 30 | const criticalAtRule = postcss.atRule({ 31 | name: atRule.name, 32 | params: atRule.params 33 | }); 34 | /** 35 | * Should append even if parent selector, but make sure the two rules 36 | * aren't identical. 37 | */ 38 | if ( 39 | (rule.selector === parent.selector || childRule) && 40 | postcss.parse(rule).toString() !== postcss.parse(parent).toString() 41 | ) { 42 | const clone = rule.clone(); 43 | criticalAtRule.append(clone); 44 | result.push(criticalAtRule); 45 | } 46 | }); 47 | }); 48 | 49 | return result; 50 | } 51 | -------------------------------------------------------------------------------- /test/preTest.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require("fs"); 3 | const { bold } = require("chalk"); 4 | const postcssCriticalCSS = require(".."); 5 | const cliArgs = require("minimist")(process.argv.slice(2), { 6 | boolean: ["minify", "preserve"], 7 | default: { minify: true, preserve: true } 8 | }); 9 | const fixturesDir = cliArgs["fixtures-dir"] || "fixtures"; 10 | let basePath = cliArgs.outputPath || `${process.cwd()}/test/${fixturesDir}`; 11 | let pluginOpts = Object.assign( 12 | {}, 13 | { 14 | minify: cliArgs.minify, 15 | outputDest: cliArgs.outputDest, 16 | outputPath: basePath, 17 | preserve: typeof cliArgs.preserve !== "undefined" ? cliArgs.preserve : true 18 | } 19 | ); 20 | if (cliArgs.noArgs) { 21 | basePath = process.cwd(); 22 | pluginOpts = {}; 23 | } 24 | 25 | function useFileData(data, file) { 26 | postcssCriticalCSS 27 | .process(data, {}, pluginOpts) 28 | .catch(err => { 29 | console.error(bold.red("Error: "), err); 30 | process.exit(1); 31 | }) 32 | .then(result => { 33 | fs.writeFile( 34 | `${basePath}/${file.split(".")[0]}.non-critical.actual.css`, 35 | result.css, 36 | "utf8", 37 | err => { 38 | if (err) { 39 | throw new Error(err); 40 | } 41 | } 42 | ); 43 | }); 44 | } 45 | 46 | function deleteOldFixtures(files) { 47 | let totalProcessed = 0; 48 | files.forEach(file => { 49 | if (file.indexOf(".actual") !== -1 || file === "critical.css") { 50 | fs.unlink(`${basePath}/${file}`, err => { 51 | if (err) { 52 | throw new Error(err); 53 | } 54 | totalProcessed++; 55 | writeNewFixtures(totalProcessed, files); 56 | }); 57 | } else { 58 | totalProcessed++; 59 | writeNewFixtures(totalProcessed, files); 60 | } 61 | }); 62 | } 63 | 64 | function writeNewFixtures(totalProcessed, files) { 65 | if (totalProcessed !== files.length) { 66 | return; 67 | } 68 | files.forEach(file => { 69 | if ( 70 | file.indexOf(".css") !== -1 && 71 | file.indexOf(".expected") === -1 && 72 | file.indexOf(".actual") === -1 && 73 | file !== "critical.css" 74 | ) { 75 | fs.readFile(`${basePath}/${file}`, "utf8", (err, data) => { 76 | if (err) { 77 | throw new Error(err); 78 | } 79 | useFileData(data, file); 80 | }); 81 | } 82 | }); 83 | } 84 | 85 | fs.readdir(basePath, "utf8", (err, files) => { 86 | if (err) { 87 | throw new Error(err); 88 | } 89 | deleteOldFixtures(files); 90 | }); 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-critical-css", 3 | "version": "3.0.7", 4 | "description": "Generate critical CSS using PostCSS", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/zgreen/postcss-critical-css" 9 | }, 10 | "keywords": [ 11 | "postcss-plugin", 12 | "postcss plugin", 13 | "postcss", 14 | "critical-css", 15 | "critical", 16 | "css", 17 | "critical css" 18 | ], 19 | "author": "Zach Green", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@commitlint/cli": "^8.3.5", 23 | "@commitlint/config-conventional": "^8.3.4", 24 | "babel-cli": "^6.26.0", 25 | "babel-eslint": "^7.2.3", 26 | "babel-plugin-transform-flow-strip-types": "^6.8.0", 27 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 28 | "babel-preset-es2015": "^6.13.2", 29 | "eslint": "^3.3.1", 30 | "eslint-config-prettier": "^6.15.0", 31 | "eslint-config-standard": "^10.2.1", 32 | "eslint-plugin-flowtype": "^2.50.3", 33 | "eslint-plugin-import": "^2.22.1", 34 | "eslint-plugin-node": "^4.2.3", 35 | "eslint-plugin-prettier": "^3.3.0", 36 | "eslint-plugin-promise": "^3.8.0", 37 | "eslint-plugin-standard": "^3.1.0", 38 | "flow-bin": "^0.100.0", 39 | "husky": "^3.1.0", 40 | "lint-staged": "^9.5.0", 41 | "minimist": "^1.2.5", 42 | "prettier": "1.19.1", 43 | "standard-version": "^7.1.0", 44 | "tape": "^4.13.3" 45 | }, 46 | "scripts": { 47 | "build": "eslint src/** && npm run flow && babel src --out-dir dist", 48 | "example": "./node_modules/.bin/babel-node example/example.js", 49 | "flow": "flow; test $? -eq 0 -o $? -eq 2", 50 | "eslint": "eslint test/**/*.js && eslint src/**", 51 | "format": "prettier --write \"**/*.{js,md,yml,.babelrc,.eslintrc}\"", 52 | "prerelease": "npm run build && npm test", 53 | "pretest": "./node_modules/.bin/babel-node test/preTest.js", 54 | "release": "standard-version", 55 | "test": "npm run test-default && npm run test-no-preserve && npm run test-output-path && npm run test-output-dest && npm run test-no-args && npm run test-no-minify", 56 | "test-no-args": "npm run pretest -- --noArgs && tape test --noArgs --test=default", 57 | "test-no-minify": "npm run pretest -- --minify=false --fixtures-dir=fixtures-no-minify && tape test --fixtures-dir=fixtures-no-minify --test=default,this", 58 | "test-output-dest": "npm run pretest -- --outputDest='custom.css' --fixtures-dir=fixtures-output-dest && tape test --outputDest='custom.css' --fixtures-dir=fixtures-output-dest --test=default", 59 | "test-default": "npm run pretest && tape test", 60 | "test-no-preserve": "npm run pretest -- --fixtures-dir=fixtures-no-preserve --preserve=false && tape test --fixtures-dir=fixtures-no-preserve --preserve=false", 61 | "test-output-path": "npm run pretest -- --outputPath='test/fixtures-output-path' && tape test --outputPath='test/fixtures-output-path' --test=default" 62 | }, 63 | "dependencies": { 64 | "chalk": "^1.1.3", 65 | "cssnano": "^4.1.10", 66 | "fs-extra": "^8.1.0", 67 | "postcss": "^7.0.35" 68 | }, 69 | "husky": { 70 | "hooks": { 71 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 72 | "pre-commit": "lint-staged" 73 | } 74 | }, 75 | "lint-staged": { 76 | "*.js": [ 77 | "eslint -- --fix", 78 | "git add" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/getCriticalRules.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import postcss from "postcss"; 4 | import { getChildRules } from "./getChildRules"; 5 | import { getCriticalFromAtRule } from "./atRule"; 6 | import { getCriticalDestination } from "./getCriticalDestination"; 7 | 8 | /** 9 | * Clean a root node of a declaration. 10 | * 11 | * @param {Object} root PostCSS root node. 12 | * @param {string} test Declaration string. Default `critical-selector` 13 | * @return {Object} clone Cloned, cleaned root node. 14 | */ 15 | function clean(root: Object, test: string = "critical-selector"): Object { 16 | const clone = root.clone(); 17 | if (clone.type === "decl") { 18 | clone.remove(); 19 | } else { 20 | clone.walkDecls(test, (decl: Object) => { 21 | decl.remove(); 22 | }); 23 | } 24 | return clone; 25 | } 26 | 27 | /** 28 | * Correct the source order of nodes in a root. 29 | * 30 | * @param {Object} root PostCSS root node. 31 | * @return {Object} sortedRoot Root with nodes sorted by source order. 32 | */ 33 | function correctSourceOrder(root: Object): Object { 34 | const sortedRoot = postcss.root(); 35 | const clone = root.clone(); 36 | clone.walkRules((rule: Object) => { 37 | let start = rule.source.start.line; 38 | if (rule.parent.type === "atrule") { 39 | const child = rule; 40 | rule = postcss 41 | .atRule({ 42 | name: rule.parent.name, 43 | params: rule.parent.params 44 | }) 45 | .append(rule.clone()); 46 | rule.source = child.source; 47 | start = child.source.start.line; 48 | } 49 | if ( 50 | sortedRoot.nodes.length === 0 || 51 | (sortedRoot.last && sortedRoot.last.source.start.line > start) 52 | ) { 53 | sortedRoot.prepend(rule); 54 | } else { 55 | sortedRoot.append(rule); 56 | } 57 | }); 58 | return sortedRoot; 59 | } 60 | 61 | /** 62 | * Establish the container of a given node. Useful when preserving media queries 63 | * or other atrules. 64 | * 65 | * @param {Object} node PostCSS node. 66 | * @return {Object} A new root node with an atrule at its base. 67 | */ 68 | function establishContainer(node: Object): Object { 69 | return node.parent.type === "atrule" && node.parent.name !== "critical" 70 | ? postcss.atRule({ 71 | name: node.parent.name, 72 | type: node.parent.type, 73 | params: node.parent.params, 74 | nodes: [node] 75 | }) 76 | : node.clone(); 77 | } 78 | 79 | /** 80 | * Update a critical root. 81 | * 82 | * @param {Object} root Root object to update. 83 | * @param {Object} update Update object. 84 | * @return {Object} clonedRoot Root object. 85 | */ 86 | function updateCritical(root: Object, update: Object): Object { 87 | const clonedRoot = root.clone(); 88 | if (update.type === "rule") { 89 | clonedRoot.append(clean(update.clone())); 90 | } else { 91 | update.clone().each((rule: Object) => { 92 | clonedRoot.append(clean(rule.root())); 93 | }); 94 | } 95 | return clonedRoot; 96 | } 97 | 98 | /** 99 | * Identify critical CSS selectors 100 | * 101 | * @param {object} PostCSS CSS object. 102 | * @param {boolean} Whether or not to remove selectors from primary CSS document. 103 | * @param {string} Default output CSS file name. 104 | * @return {object} Object containing critical rules, organized by output destination 105 | */ 106 | export function getCriticalRules(css: Object, defaultDest: string): Object { 107 | const critical: Object = getCriticalFromAtRule({ css, defaultDest }); 108 | css.walkDecls("critical-selector", (decl: Object) => { 109 | const { parent, value } = decl; 110 | const dest = getCriticalDestination(parent, defaultDest); 111 | const container = establishContainer(parent); 112 | const childRules = value === "scope" ? getChildRules(css, parent) : []; 113 | // Sanity check, make sure we've got a root node 114 | critical[dest] = critical[dest] || postcss.root(); 115 | 116 | switch (value) { 117 | case "scope": 118 | // Add all child rules 119 | const criticalRoot = childRules.reduce( 120 | (acc: Object, rule: Object): Object => { 121 | return acc.append(rule.clone()); 122 | }, 123 | critical[dest].append(container) 124 | ); 125 | 126 | critical[dest] = clean(correctSourceOrder(criticalRoot)); 127 | break; 128 | 129 | default: 130 | critical[dest] = updateCritical(critical[dest], container); 131 | break; 132 | } 133 | }); 134 | return critical; 135 | } 136 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | const test = require("tape"); 5 | const chalk = require("chalk"); 6 | const cliArgs = require("minimist")(process.argv.slice(2), { 7 | boolean: ["preserve"], 8 | default: { preserve: true } 9 | }); 10 | const fixturesDir = cliArgs["fixtures-dir"] || "fixtures"; 11 | let basePath = cliArgs.outputPath || `${process.cwd()}/test/${fixturesDir}`; 12 | if (cliArgs.noArgs) { 13 | basePath = process.cwd(); 14 | } 15 | 16 | function compareCritical(t, name, testNonCritical) { 17 | let actual = cliArgs.outputDest || "critical.css"; 18 | const expected = testNonCritical 19 | ? `${name}.non-critical.expected.css` 20 | : `${name}.critical.expected.css`; 21 | if (name !== "default" || testNonCritical) { 22 | actual = testNonCritical 23 | ? `${name}.non-critical.actual.css` 24 | : `${name}.critical.actual.css`; 25 | } 26 | console.log(`Comparing: ${expected} and ${actual}`); 27 | t.equal( 28 | fs.readFileSync(`${basePath}/${actual}`, "utf8").trim(), 29 | fs.readFileSync(`${basePath}/${expected}`, "utf8").trim(), 30 | `Expect ${chalk.bold(name)} should be equal to actual output` 31 | ); 32 | t.end(); 33 | } 34 | 35 | function initTests(key) { 36 | const tests = { 37 | default: () => { 38 | test("Testing default critical result", t => { 39 | compareCritical(t, "default"); 40 | }); 41 | 42 | test("Testing default non-critical result", t => { 43 | compareCritical(t, "default", true); 44 | }); 45 | }, 46 | 47 | this: () => { 48 | test('Testing "this" critical result', t => { 49 | compareCritical(t, "this"); 50 | }); 51 | 52 | test('Testing "this" non-critical result', t => { 53 | compareCritical(t, "this", true); 54 | }); 55 | }, 56 | 57 | atRule: () => { 58 | test('Testing "atRule" critical result', t => { 59 | compareCritical(t, "atRule"); 60 | }); 61 | 62 | test('Testing "atRule" non-critical result', t => { 63 | compareCritical(t, "atRule", true); 64 | }); 65 | }, 66 | 67 | atRuleWrapping: () => { 68 | test( 69 | chalk.yellow( 70 | `Testing ${chalk.bold("atRule.wrapping")} critical result` 71 | ), 72 | t => { 73 | compareCritical(t, "atRule-wrapping"); 74 | } 75 | ); 76 | 77 | test( 78 | chalk.yellow( 79 | `Testing ${chalk.bold("atRule.wrapping")} non-critical result` 80 | ), 81 | t => { 82 | compareCritical(t, "atRule-wrapping", true); 83 | } 84 | ); 85 | }, 86 | 87 | media: () => { 88 | test('Testing "media" critical result', t => { 89 | compareCritical(t, "media"); 90 | }); 91 | 92 | test('Testing "media" non-critical result', t => { 93 | compareCritical(t, "media", true); 94 | }); 95 | }, 96 | 97 | scope: () => { 98 | test( 99 | chalk.yellow(`Testing ${chalk.bold("scope")} critical result`), 100 | t => { 101 | compareCritical(t, "scope"); 102 | } 103 | ); 104 | 105 | test( 106 | chalk.yellow(`Testing ${chalk.bold("scope")} non-critical result`), 107 | t => { 108 | compareCritical(t, "scope", true); 109 | } 110 | ); 111 | }, 112 | 113 | mediaScope: () => { 114 | test( 115 | chalk.yellow(`Testing ${chalk.bold("media-scope")} critical result`), 116 | t => { 117 | compareCritical(t, "media-scope"); 118 | } 119 | ); 120 | 121 | test( 122 | chalk.yellow( 123 | `Testing ${chalk.bold("media-scope")} non-critical result` 124 | ), 125 | t => { 126 | compareCritical(t, "media-scope", true); 127 | } 128 | ); 129 | }, 130 | 131 | mediaThis: () => { 132 | test( 133 | chalk.yellow(`Testing ${chalk.bold("media-this")} critical result`), 134 | t => { 135 | compareCritical(t, "media-this"); 136 | } 137 | ); 138 | 139 | test( 140 | chalk.yellow(`Testing ${chalk.bold("media-this")} non-critical result`), 141 | t => { 142 | compareCritical(t, "media-this", true); 143 | } 144 | ); 145 | } 146 | }; 147 | 148 | if (key) { 149 | const keys = key.split(","); 150 | keys.forEach(k => tests[k]()); 151 | } else { 152 | Object.keys(tests).forEach(key => tests[key]()); 153 | } 154 | } 155 | 156 | initTests(cliArgs.test); 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Critical CSS 2 | 3 | This plugin allows the user to define and output critical CSS using custom atRules, and/or custom CSS properties. Critical CSS may be output to one or more files, as defined within the [plugin options](#plugin-options) and/or within the CSS. Depending on the plugin options used, processed CSS may be left unchanged, or critical CSS may be removed from it. 4 | 5 | ## Install 6 | 7 | `npm install postcss-critical-css --save` 8 | 9 | ## Examples 10 | 11 | An example is available in this repo. See the `/example` directory, and use the command `npm run example` to test it out. 12 | 13 | ## Usage examples 14 | 15 | All examples given below show the input CSS and the critical CSS that is output from it. Note that the input CSS will remain unchanged, unless `preserve` is set to `false` in the [plugin options](#plugin-options). Use `npm run example` to see how this works. 16 | 17 | ### Using the `@critical` atRule 18 | 19 | ```css 20 | /* In foo.css */ 21 | @critical; 22 | 23 | .foo { 24 | border: 3px solid gray; 25 | display: flex; 26 | padding: 1em; 27 | } 28 | ``` 29 | 30 | Will output: 31 | 32 | ```css 33 | /* In critical.css */ 34 | .foo { 35 | border: 3px solid gray; 36 | display: flex; 37 | padding: 1em; 38 | } 39 | ``` 40 | 41 | ### Using the `@critical` atRule with a custom file path 42 | 43 | ```css 44 | /* In foo.css */ 45 | @critical bar.css; 46 | 47 | .foo { 48 | border: 3px solid gray; 49 | display: flex; 50 | padding: 1em; 51 | } 52 | ``` 53 | 54 | Will output: 55 | 56 | ```css 57 | /* In bar.css */ 58 | .foo { 59 | border: 3px solid gray; 60 | display: flex; 61 | padding: 1em; 62 | } 63 | ``` 64 | 65 | ### Using the `@critical` atRule with a subset of styles 66 | 67 | ```css 68 | /* In foo.css */ 69 | .foo { 70 | border: 3px solid gray; 71 | display: flex; 72 | padding: 1em; 73 | } 74 | 75 | @critical { 76 | .bar { 77 | border: 10px solid gold; 78 | color: gold; 79 | } 80 | } 81 | ``` 82 | 83 | Will output: 84 | 85 | ```css 86 | /* In critical.css */ 87 | .bar { 88 | border: 10px solid gold; 89 | color: gold; 90 | } 91 | ``` 92 | 93 | ### Using the custom property, `critical-selector` 94 | 95 | ```css 96 | /* In foo.css */ 97 | .foo { 98 | critical-selector: this; 99 | border: 3px solid gray; 100 | display: flex; 101 | padding: 1em; 102 | } 103 | ``` 104 | 105 | Will output: 106 | 107 | ```css 108 | /* In critical.css */ 109 | .foo { 110 | border: 3px solid gray; 111 | display: flex; 112 | padding: 1em; 113 | } 114 | ``` 115 | 116 | ### Using the custom property, `critical-selector`, with a custom selector. 117 | 118 | ```css 119 | /* In foo.css */ 120 | .foo { 121 | critical-selector: .bar; 122 | border: 3px solid gray; 123 | display: flex; 124 | padding: 1em; 125 | } 126 | ``` 127 | 128 | Will output: 129 | 130 | ```css 131 | /* In critical.css */ 132 | .bar { 133 | border: 3px solid gray; 134 | display: flex; 135 | padding: 1em; 136 | } 137 | ``` 138 | 139 | ### Using the custom property, `critical-filename` 140 | 141 | ```css 142 | /* in foo.css */ 143 | .foo { 144 | critical-selector: this; 145 | critical-filename: secondary-critical.css; 146 | border: 3px solid gray; 147 | display: flex; 148 | padding: 1em; 149 | } 150 | ``` 151 | 152 | Will output: 153 | 154 | ```css 155 | /* In secondary-critical.css */ 156 | .foo { 157 | border: 3px solid gray; 158 | display: flex; 159 | padding: 1em; 160 | } 161 | ``` 162 | 163 | ### Using the custom property, `critical-selector`, with value `scope` 164 | 165 | This allows the user to output the entire scope of a module, including children. 166 | 167 | ```css 168 | /* in foo.css */ 169 | .foo { 170 | critical-selector: scope; 171 | border: 3px solid gray; 172 | display: flex; 173 | padding: 1em; 174 | } 175 | 176 | .foo a { 177 | color: blue; 178 | text-decoration: none; 179 | } 180 | ``` 181 | 182 | Will output: 183 | 184 | ```css 185 | /* In critical.css */ 186 | .foo { 187 | border: 3px solid gray; 188 | display: flex; 189 | padding: 1em; 190 | } 191 | 192 | .foo a { 193 | color: blue; 194 | text-decoration: none; 195 | } 196 | ``` 197 | 198 | ## Plugin options 199 | 200 | The plugin takes a single object as its only parameter. The following properties are valid: 201 | 202 | | Arg | Type | Description | Default | 203 | | ------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | 204 | | `outputPath` | `string` | Path to which critical CSS should be output | Current working directory | 205 | | `outputDest` | `string` | Default critical CSS file name | `"critical.css"` | 206 | | `preserve` | `boolean` | Whether or not to remove selectors from primary CSS document once they've been marked as critical. This should prevent duplication of selectors across critical and non-critical CSS. | `true` | 207 | | `minify` | `boolean` | Minify output CSS? | `true` | 208 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { green, yellow } from "chalk"; 4 | import postcss from "postcss"; 5 | import cssnano from "cssnano"; 6 | import fs from "fs-extra"; 7 | import path from "path"; 8 | import { getCriticalRules } from "./getCriticalRules"; 9 | 10 | /** 11 | * Append to an existing critical CSS file? 12 | */ 13 | let append = false; 14 | 15 | /** 16 | * Clean the original root node passed to the plugin, removing custom atrules, 17 | * properties. Will additionally delete nodes as appropriate if 18 | * `preserve === false`. 19 | * 20 | * @param {Object} root The root PostCSS object. 21 | * @param {boolean} preserve Preserve identified critical CSS in the root? 22 | */ 23 | function clean(root: Object, preserve: boolean) { 24 | root.walkAtRules("critical", (atRule: Object) => { 25 | if (preserve === false) { 26 | if (atRule.nodes && atRule.nodes.length) { 27 | atRule.remove(); 28 | } else { 29 | root.removeAll(); 30 | } 31 | } else { 32 | if (atRule.nodes && atRule.nodes.length) { 33 | atRule.replaceWith(atRule.nodes); 34 | } else { 35 | atRule.remove(); 36 | } 37 | } 38 | }); 39 | // @TODO `scope` Makes this kind of gnarly. This could be cleaned up a bit. 40 | root.walkDecls(/critical-(selector|filename)/, (decl: Object) => { 41 | if (preserve === false) { 42 | if (decl.value === "scope") { 43 | root.walk((node: Object) => { 44 | if ( 45 | node.selector && 46 | node.selector.indexOf(decl.parent.selector) === 0 47 | ) { 48 | if (node.parent && hasNoOtherChildNodes(node.parent.nodes, node)) { 49 | node.parent.remove(); 50 | } else { 51 | node.remove(); 52 | } 53 | } 54 | }); 55 | } 56 | let wrapper = {}; 57 | if (decl && decl.parent) { 58 | wrapper = decl.parent.parent; 59 | decl.parent.remove(); 60 | } 61 | // If the wrapper has no valid child nodes, remove it entirely. 62 | if (wrapper && hasNoOtherChildNodes(wrapper.nodes, decl)) { 63 | wrapper.remove(); 64 | } 65 | } else { 66 | decl.remove(); 67 | } 68 | }); 69 | } 70 | 71 | /** 72 | * Do a dry run, console.log the output. 73 | * 74 | * @param {string} css CSS to output. 75 | */ 76 | function doDryRun(css: string) { 77 | console.log( 78 | // eslint-disable-line no-console 79 | green(`Critical CSS result is: ${yellow(css)}`) 80 | ); 81 | } 82 | 83 | /** 84 | * Do a dry run, or write a file. 85 | * 86 | * @param {bool} dryRun Do a dry run? 87 | * @param {string} filePath Path to write file to. 88 | * @param {Object} result PostCSS root object. 89 | * @return {Promise} Resolves with writeCriticalFile or doDryRun function call. 90 | */ 91 | function dryRunOrWriteFile( 92 | dryRun: boolean, 93 | filePath: string, 94 | result: Object 95 | ): Promise { 96 | const { css } = result; 97 | return new Promise((resolve: Function): void => 98 | resolve(dryRun ? doDryRun(css) : writeCriticalFile(filePath, css)) 99 | ); 100 | } 101 | 102 | /** 103 | * Confirm a node has no child nodes other than a specific node. 104 | * 105 | * @param {array} nodes Nodes array to check. 106 | * @param {Object} node Node to check. 107 | * @return {boolean} Whether or not the node has no other children. 108 | */ 109 | function hasNoOtherChildNodes( 110 | nodes: Array = [], 111 | node: Object = postcss.root() 112 | ): boolean { 113 | return nodes.filter((child: Object): boolean => child !== node).length === 0; 114 | } 115 | 116 | /** 117 | * Write a file containing critical CSS. 118 | * 119 | * @param {string} filePath Path to write file to. 120 | * @param {string} css CSS to write to file. 121 | */ 122 | function writeCriticalFile(filePath: string, css: string) { 123 | fs.outputFile( 124 | filePath, 125 | css, 126 | { flag: append ? "a" : "w" }, 127 | (err: ?ErrnoError) => { 128 | append = true; 129 | if (err) { 130 | console.error(err); 131 | process.exit(1); 132 | } 133 | } 134 | ); 135 | } 136 | 137 | /** 138 | * Primary plugin function. 139 | * 140 | * @param {object} options Object of function args. 141 | * @return {function} function for PostCSS plugin. 142 | */ 143 | function buildCritical(options: Object = {}): Function { 144 | const filteredOptions = Object.keys(options).reduce( 145 | (acc: Object, key: string): Object => 146 | typeof options[key] !== "undefined" 147 | ? { ...acc, [key]: options[key] } 148 | : acc, 149 | {} 150 | ); 151 | const args = { 152 | outputPath: process.cwd(), 153 | outputDest: "critical.css", 154 | preserve: true, 155 | minify: true, 156 | dryRun: false, 157 | ...filteredOptions 158 | }; 159 | append = false; 160 | return (css: Object): Object => { 161 | const { dryRun, preserve, minify, outputPath, outputDest } = args; 162 | const criticalOutput = getCriticalRules(css, outputDest); 163 | return Object.keys(criticalOutput).reduce( 164 | (init: Object, cur: string): Function => { 165 | const criticalCSS = postcss.root(); 166 | const filePath = path.join(outputPath, cur); 167 | criticalOutput[cur].each((rule: Object): Function => 168 | criticalCSS.append(rule.clone()) 169 | ); 170 | return ( 171 | postcss(minify ? [cssnano] : []) 172 | // @TODO Use from/to correctly. 173 | .process(criticalCSS, { 174 | from: undefined 175 | }) 176 | .then(dryRunOrWriteFile.bind(null, dryRun, filePath)) 177 | .then(clean.bind(null, css, preserve)) 178 | ); 179 | }, 180 | {} 181 | ); 182 | }; 183 | } 184 | 185 | module.exports = postcss.plugin("postcss-critical", buildCritical); 186 | --------------------------------------------------------------------------------