├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── critical-split.gif ├── index.js ├── package-lock.json ├── package.json ├── test.js └── testsuite ├── deprecation-warn-save-feature ├── input.css ├── output.css ├── process.js ├── setup.json └── split-settings.json ├── dev-debug-mode ├── input.css ├── output.css ├── process.js ├── setup.json └── split-settings.json ├── errors-error-same-start-end-tag ├── input.css ├── output.css ├── process.js ├── setup.json └── split-settings.json ├── errors-prevent-crash-without-split-options ├── input.css ├── output.css └── setup.json ├── markers-block-tags ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── markers-custom-block-tag ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── markers-custom-start-end-tags ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── markers-start-end-tags ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── media-queries-advanced-start-end ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── media-queries-append-001 ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── media-queries-append-002 ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── media-queries-basic-start-end ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── media-queries-basic ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── media-queries-split ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── modules-basic ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── modules-media-queries ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── modules-no-array ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── modules-separator ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── multiple-font-faces ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── ouput-mode-critical ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── ouput-mode-default ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── ouput-mode-input ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── ouput-mode-rest ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── save-feature ├── input.css ├── output.css ├── process.js ├── setup.json └── split-settings.json ├── special-rules-keyframes-blocktag ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── special-rules-keyframes-start-end-tag ├── input.css ├── output.css ├── setup.json └── split-settings.json ├── support-important-comments ├── input.css ├── output.css ├── setup.json └── split-settings.json └── weird-first-line-bug ├── input.css ├── output.css ├── setup.json └── split-settings.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | paths-ignore: 11 | - '**.md' 12 | 13 | jobs: 14 | test: 15 | name: Node.js ${{ matrix.node-version }} 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | node-version: 20 | - 14 21 | - 12 22 | - 10 23 | 24 | steps: 25 | - name: Checkout the repository 26 | uses: actions/checkout@v2 27 | - name: Install Node.js 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | - name: Install dependencies 32 | uses: bahmutov/npm-install@v1 33 | - name: Run tests 34 | run: npx jest 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | 36 | # webstorm / interlij 37 | .idea 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Ronny Welter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-critical-split 2 | A PostCSS plugin that takes existing CSS files and splits out the annotated critical styles into a separate file, inspired by https://github.com/wladston/postcss-split 3 | 4 | ![A PostCSS plugin to split your Critical CSS from the rest](https://raw.githubusercontent.com/mrnocreativity/postcss-critical-split/master/critical-split.gif) 5 | 6 | ## What exactly does this plugin do? 7 | * It supports 3 output modes: `critical`, `rest` and `input`. This can be used to choose which type of output you'd like to generate. 8 | * In critical-output-mode it goes through the given CSS files and finds all rules that have a CSS comment in them that indicates they are critical. These rules are isolated and returned to PostCSS (this is called the critical CSS). 9 | * In rest-output-mode it goes through the given CSS files and finds all rules that have a CSS comment in them that indicates they are critical. These rules ignored; the rest is returned to PostCSS (this is called the rest CSS). 10 | * It can select/ignore critical CSS rules between start- & end-comment-tags or select/ignore rules based on a single line in a CSS block. 11 | * It keeps track of media-queries: If a tagged rule is inside a media query, the media query will be copied to the critical CSS as well. 12 | * It also works inside @font-face declarations: you can tag @font-face declarations which will be added to the critical CSS as well. 13 | * It also supports @keyframes: if a keyframes animation is found in the critical CSS, it gets added to the critical CSS at once. 14 | * You can label your critical CSS with so-called module names. This allows for selecting which pieces of critical CSS you actually want to extract (and keep things as lean as possible). This comes in handy to select only what is absolutely necessary for a specific page template in your website. 15 | 16 | ## What does it NOT do? 17 | * It does not automatically detect which rules should be considered critical. You have to tag them yourself. 18 | * It does not change any paths in the critical ruleset. If you want are to include the critical-CSS file in the head of your site, you WILL need to run another PostCSS to adapt the paths. Check out this example for more info: https://github.com/mrnocreativity/critical-css-example 19 | * Remove the tag-comment you added yourself. This can be removed in a later stage with other capable and proven PostCSS plugins 20 | 21 | ## Why should I tag my CSS rules myself? 22 | For larger scale projects, automating critical-CSS detection is complicated, unprecise or damn-nearly impossible. Annotating your CSS with a simple comment gives your perfect control over which CSS rules are to be considered critical and which ones are not. 23 | 24 | If you later decide to no longer support this workflow or switch to a different one (with different tools), the critical-comments are standard CSS and will not break your project. 25 | 26 | ## Why split the files into 2 (or more) separate files? Why not immediately move it into HTML? 27 | The idea here is that we want to generate our entire CSS file first and then split out what is considered 'critical'. 28 | Injecting it into an HTML file right away would be fairly dictative of your workflow. This allows for more flexible setups. 29 | 30 | For example: during development you could `` the critical-CSS file, while rendering it out into the HTML templates once you get ready for production (remember to adjust the URL's in the CSS file for the changed context of the CSS execution). 31 | 32 | ## Install 33 | 34 | ```bash 35 | npm install --save-dev postcss postcss-critical-split 36 | ``` 37 | 38 | ## Test 39 | To run tests: 40 | ```bash 41 | npm test 42 | ``` 43 | 44 | If you want to contribute to the project and write additional tests, look into the testsuite folder. You'll find a folder per test with input/output results, splitSettings and optional process tasks where you can write a custom test scenario. 45 | 46 | ## Usage 47 | 48 | ```javascript 49 | gulp.src(['**/*.css','!**/*-critical.css']) 50 | .pipe(postcss(require('postcss-critical-split'))); 51 | ``` 52 | ```css 53 | /* before: main.css */ 54 | 55 | /* critical:start */ 56 | header{ 57 | background-color: #1d1d1d; 58 | font-size: 2em; 59 | } 60 | 61 | .aside { 62 | text-decoration: underline; 63 | } 64 | 65 | /* critical:end */ 66 | 67 | p { 68 | font-size: 14px; 69 | } 70 | 71 | li { 72 | float: left; 73 | /* critical:start */ 74 | color: red; 75 | /* critical:end */ 76 | } 77 | 78 | footer{ 79 | background-color: #1d1d1d; 80 | font-size: 1.1em; 81 | } 82 | 83 | a[rel=external]:before{ 84 | content: '[!]'; 85 | /* critical */ 86 | } 87 | 88 | /* critical:start */ 89 | @media screen and (min-width: 400px) { 90 | footer { 91 | padding: 50px; 92 | } 93 | } 94 | /* critical:end */ 95 | 96 | @media screen and (min-width: 1400px) { 97 | header { 98 | height: 300px; 99 | /* critical:start */ 100 | background-color: #FF0066; 101 | font-size: 3em; 102 | /* critical:end */ 103 | } 104 | } 105 | ``` 106 | 107 | ```css 108 | /* after: main.css */ 109 | 110 | p { 111 | font-size: 14px; 112 | } 113 | 114 | li { 115 | float: left; 116 | } 117 | 118 | footer{ 119 | background-color: #1d1d1d; 120 | font-size: 1.1em; 121 | } 122 | 123 | @media screen and (min-width: 1400px) { 124 | header { 125 | background-color: #FF0066; 126 | font-size: 3em; 127 | } 128 | } 129 | ``` 130 | 131 | ```css 132 | /* after main-critical.css */ 133 | 134 | header{ 135 | background-color: #1d1d1d; 136 | font-size: 2em; 137 | } 138 | 139 | .aside { 140 | text-decoration: underline; 141 | } 142 | 143 | li { 144 | color: red; 145 | } 146 | 147 | a[rel=external]:before{ 148 | content: '[!]'; 149 | } 150 | 151 | @media screen and (min-width: 400px) { 152 | footer { 153 | padding: 50px; 154 | } 155 | } 156 | 157 | @media screen and (min-width: 1400px) { 158 | header { 159 | background-color: #FF0066; 160 | font-size: 3em; 161 | } 162 | } 163 | 164 | ``` 165 | 166 | ### Important comments 167 | This plugin supports important comments (e.g. `/*! critical */`). Any of the 168 | examples above should work using this comment style. 169 | 170 | ## Options 171 | The plugin accepts an object with additional options. 172 | 173 | ### options.output 174 | *defaults to `critical`* 175 | Allowed values: `critical`, `rest` or `input` to return either the critical-css, the rest-css or just the original input-css. 176 | 177 | ### options.blockTag 178 | *defaults to `critical`* 179 | 180 | This is the comment text that is matched in every rule/atRule in the original CSS file. If the blockTag is encountered in a rule, the rule is appended to the critical-CSS file and removed in the original file. All declarations in the rule will be carried over. 181 | ```javascript 182 | /* gulpfile */ 183 | gulp.src(['**/*.css','!**/*-critical.css']) 184 | .pipe(postcss(require('postcss-critical-split')({ 185 | 'blockTag':'criticalCss' 186 | })); 187 | ``` 188 | ```css 189 | /* before: main.css */ 190 | header{ 191 | /* criticalCss */ 192 | background-color: #1d1d1d; 193 | font-size: 2em; 194 | } 195 | 196 | footer{ 197 | background-color: #1d1d1d; 198 | font-size: 1.1em; 199 | } 200 | ``` 201 | ```css 202 | /* after: main.css */ 203 | footer{ 204 | background-color: #1d1d1d; 205 | font-size: 1.1em; 206 | } 207 | ``` 208 | ```css 209 | /* after: main-critical.css */ 210 | header{ 211 | background-color: #1d1d1d; 212 | font-size: 2em; 213 | } 214 | ``` 215 | 216 | ### options.startTag & options.endTag 217 | *startTag defaults to `critical:start`* 218 | *endTag defaults to `critical:end`* 219 | 220 | These are the comment texts that are matched throughout the original CSS file. If the startTag is encountered, every rule, declaration, atRule is carried into the critical-CSS until the endTag is encountered. All the rules that appy will be removed from the original CSS. 221 | 222 | ```javascript 223 | /* gulpfile */ 224 | gulp.src(['**/*.css','!**/*-critical.css']) 225 | .pipe(postcss(require('postcss-critical-split')({ 226 | 'startTag':'grab:open', 227 | 'endTag':'grab:close', 228 | })); 229 | ``` 230 | ```css 231 | /* before: main.css */ 232 | 233 | /* grab:open */ 234 | header{ 235 | background-color: #1d1d1d; 236 | font-size: 2em; 237 | } 238 | 239 | .aside { 240 | text-decoration: underline; 241 | } 242 | 243 | /* grab:close */ 244 | 245 | footer{ 246 | background-color: #1d1d1d; 247 | font-size: 1.1em; 248 | } 249 | ``` 250 | ```css 251 | /* after: main.css */ 252 | footer{ 253 | background-color: #1d1d1d; 254 | font-size: 1.1em; 255 | } 256 | ``` 257 | 258 | ```css 259 | /* after: main-critical.css */ 260 | header{ 261 | background-color: #1d1d1d; 262 | font-size: 2em; 263 | } 264 | .aside { 265 | text-decoration: underline; 266 | } 267 | ``` 268 | 269 | ### options.modules 270 | * defaults to `null`, should be an array of strings* 271 | 272 | These are the modules you want to select from your css file into the critical file. This allows for targetting which parts of the CSS to include in the critical and which ones not. 273 | CSS rules that are not labeled by a module will be considered 'common' and thus will be added to the critical CSS at all times. 274 | 275 | ```javascript 276 | /* gulpfile */ 277 | gulp.src(['**/*.css','!**/*-critical.css']) 278 | .pipe(postcss(require('postcss-critical-split')({ 279 | 'modules': ['header', 'top-photo'] 280 | })); 281 | ``` 282 | ```css 283 | /* before: main.css */ 284 | 285 | /* critical:start:header */ 286 | header{ 287 | background-color: #1d1d1d; 288 | font-size: 2em; 289 | } 290 | /* critical:end */ 291 | 292 | .login-button { 293 | display: block; 294 | border: red thin solid; 295 | } 296 | 297 | /* critical:start:top-photo */ 298 | .top-photo{ 299 | background-color: #1d1d1d; 300 | font-size: 2em; 301 | } 302 | /* critical:end */ 303 | 304 | /* critical:start:preview-article */ 305 | .preview-article{ 306 | color: #CCC; 307 | } 308 | /* critical:end */ 309 | 310 | /* critical:start */ 311 | .profile-picture{ 312 | float: right; 313 | width: 20%; 314 | height: auto; 315 | } 316 | /* critical:end */ 317 | 318 | .aside { 319 | text-decoration: underline; 320 | } 321 | 322 | footer{ 323 | background-color: #1d1d1d; 324 | font-size: 1.1em; 325 | } 326 | ``` 327 | ```css 328 | /* after: main.css */ 329 | .login-button { 330 | display: block; 331 | border: red thin solid; 332 | } 333 | 334 | .preview-article{ 335 | color: #CCC; 336 | } 337 | 338 | .aside { 339 | text-decoration: underline; 340 | } 341 | 342 | footer{ 343 | background-color: #1d1d1d; 344 | font-size: 1.1em; 345 | } 346 | ``` 347 | 348 | ```css 349 | /* after: main-critical.css */ 350 | header{ 351 | background-color: #1d1d1d; 352 | font-size: 2em; 353 | } 354 | 355 | .top-photo{ 356 | background-color: #1d1d1d; 357 | font-size: 2em; 358 | } 359 | 360 | .aside { 361 | text-decoration: underline; 362 | } 363 | 364 | footer{ 365 | background-color: #1d1d1d; 366 | font-size: 1.1em; 367 | } 368 | ``` 369 | 370 | ### options.separator 371 | * defaults to `:`* 372 | 373 | This is the separator used in your critical start-tag to tag a module. 374 | 375 | ```javascript 376 | /* gulpfile */ 377 | gulp.src(['**/*.css','!**/*-critical.css']) 378 | .pipe(postcss(require('postcss-critical-split')({ 379 | 'modules': ['header', 'top-photo'], 380 | 'separator': '--' 381 | })); 382 | ``` 383 | 384 | ```css 385 | /* critical:start--header */ 386 | 387 | ``` 388 | -------------------------------------------------------------------------------- /critical-split.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrnocreativity/postcss-critical-split/51bb6d62bc75d635e32dcce90f30ba3ef6299081/critical-split.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var output_types = { 4 | 'INPUT_CSS': 'input', 5 | 'CRITICAL_CSS': 'critical', 6 | 'REST_CSS': 'rest' 7 | }, 8 | path = require('path'), 9 | fs = require('fs'), 10 | userOptions = null, 11 | criticalActive = false, 12 | defaults = { 13 | 'startTag': 'critical:start', 14 | 'endTag': 'critical:end', 15 | 'blockTag': 'critical', 16 | 'suffix': '-critical', 17 | 'output': output_types.CRITICAL_CSS, 18 | 'save': false, 19 | 'modules': null, 20 | 'separator': ':', 21 | 'debug': false 22 | }, 23 | stats = null; 24 | 25 | function CriticalSplit(newOptions = {}) { 26 | return function(originalCss, { result, postcss }) { 27 | if (applyUserOptions(newOptions)) { 28 | setupStats(); 29 | performTransform(originalCss, result, postcss); 30 | 31 | if (userOptions.debug === true) { 32 | processStats(); 33 | } 34 | } 35 | 36 | return result; 37 | }; 38 | } 39 | 40 | function timestamp() { 41 | return new Date().getTime(); 42 | } 43 | 44 | function setupStats() { 45 | stats = {}; 46 | stats.startTime = 0; 47 | stats.endTime = 0; 48 | stats.processTime = 0; 49 | 50 | stats.rules = 0; 51 | stats.criticals = 0; 52 | stats.criticalPercentage = 0; 53 | 54 | stats.loops = 0; 55 | 56 | stats.clones = 0; 57 | stats.emtpyClones = 0; 58 | 59 | stats.compares = 0; 60 | stats.appends = 0; 61 | stats.getParents = 0; 62 | stats.parentRequest = 0; 63 | } 64 | 65 | function processStats() { 66 | var key = null, 67 | value = null, 68 | message = ''; 69 | 70 | stats.processTime = stats.endTime - stats.startTime; 71 | message += '----- postcss-critical-split debug info ---------\n'; 72 | 73 | for (key in stats) { 74 | /* istanbul ignore else */ 75 | if (stats.hasOwnProperty(key)) { 76 | value = stats[key]; 77 | 78 | switch(key) { 79 | case 'processTime': 80 | value = value + 'ms'; 81 | break; 82 | case 'startTime': 83 | case 'endTime': 84 | value = null; 85 | break; 86 | case 'criticalPercentage': 87 | value = (stats.criticals / stats.rules * 100).toFixed(2) + '%'; 88 | break; 89 | } 90 | 91 | if (value !== null) { 92 | message += key + ': ' + value + '\n'; 93 | } 94 | } 95 | } 96 | 97 | message += '-------------------------------------------------\n'; 98 | console.log(message); 99 | } 100 | 101 | function performTransform(inputCss, result, postcss) { 102 | var originalCss = clone(inputCss), 103 | criticalCss = postcss.root(), 104 | absolutePath = null, 105 | directoryPath = null, 106 | nonCriticalFilename = null, 107 | criticalFilename = null; 108 | 109 | getAllCriticals(originalCss, criticalCss); 110 | 111 | cleanUp(originalCss); 112 | cleanUp(criticalCss); 113 | 114 | if (userOptions.save === true) { 115 | //console.log('warning'); 116 | console.warn('postcss-critical-split: The save feature has been deprecated and should be avoided. This feature will be removed in v3.0.0. Read more about it here: https://github.com/mrnocreativity/postcss-critical-split/issues/3'); 117 | 118 | absolutePath = originalCss.source.input.file, 119 | directoryPath = path.dirname(absolutePath), 120 | nonCriticalFilename = path.basename(absolutePath), 121 | criticalFilename = createCriticalFilename(nonCriticalFilename); 122 | 123 | saveCssFile(path.join(directoryPath, nonCriticalFilename), originalCss); 124 | saveCssFile(path.join(directoryPath, criticalFilename), criticalCss); 125 | } 126 | 127 | switch(userOptions.output) { 128 | case output_types.INPUT_CSS: 129 | result.root = inputCss; 130 | break; 131 | case output_types.CRITICAL_CSS: 132 | result.root = criticalCss; 133 | break; 134 | case output_types.REST_CSS: 135 | result.root = originalCss; 136 | break; 137 | } 138 | } 139 | 140 | function saveCssFile(filepath, cssRoot) { 141 | if (cssRoot.nodes.length > 0) { 142 | fs.writeFileSync(filepath, cssRoot.toResult()); 143 | } 144 | } 145 | 146 | function cleanUp(cssRoot) { 147 | 148 | var handleBlock = function(block) { 149 | if (block.nodes && block.nodes.length === 0) { 150 | block.remove(); 151 | } 152 | }; 153 | 154 | cssRoot.walkRules(handleBlock); 155 | cssRoot.walkAtRules(handleBlock); 156 | 157 | cssRoot.raws.semicolon = true; 158 | } 159 | 160 | function hasEndMarker(block) { 161 | var result = false; 162 | 163 | block.walkComments(function(line) { 164 | if (line.text === userOptions.endTag) { 165 | result = true; 166 | } 167 | }); 168 | 169 | return result; 170 | } 171 | 172 | function applyUserOptions(newOptions) { 173 | var errorMessage ='', 174 | result = true; 175 | 176 | userOptions = { ...defaults, ...newOptions }; 177 | 178 | if (userOptions.startTag === userOptions.endTag) { 179 | errorMessage += '\n\n'; 180 | errorMessage += 'ERROR :: PostCSS Plugin: Critical Split\n'; 181 | errorMessage += '.Critical CSS start and end tag must not be the same. \n'; 182 | errorMessage += 'Please adapt your options. \n\n'; 183 | errorMessage += '------ Current Options ----- \n\n'; 184 | errorMessage += JSON.stringify(userOptions, null, 2) + '\n\n'; 185 | errorMessage += '---------- End -------------\n'; 186 | 187 | console.error(errorMessage); 188 | result = false; 189 | } 190 | 191 | if (typeof userOptions.modules === 'string'){ 192 | userOptions.modules = [userOptions.modules]; 193 | } else if (userOptions.modules instanceof Array === false) { 194 | userOptions.modules = defaults.modules; 195 | } 196 | 197 | return result; 198 | } 199 | 200 | function createCriticalFilename(filename) { 201 | var position = filename.lastIndexOf('.css'), 202 | result = ''; 203 | 204 | result = filename.substring(0, position); 205 | result += userOptions.suffix; 206 | result += '.css'; 207 | 208 | return result; 209 | } 210 | 211 | function getAllCriticals(originalCss, criticalCss) { 212 | var currentLevel = null, 213 | blockMarkers = getModuleMarkers(userOptions.blockTag), 214 | moduleMarkers = getModuleMarkers(userOptions.startTag); 215 | 216 | stats.startTime = timestamp(); 217 | originalCss.walk(function(line) { 218 | var temp = null; 219 | 220 | stats.rules++; 221 | line.parent.raws.semicolon = true; 222 | stats.parentRequest++; 223 | 224 | if (line.type === 'comment' && (line.text === userOptions.endTag || line.text === '! ' + userOptions.endTag)) { 225 | criticalActive = false 226 | currentLevel = null; 227 | line.remove(); // remove tagging comment 228 | } else if (line.type === 'comment' && isBlockTag(line.text, blockMarkers)) { 229 | if (hasParentAtRule(line, 'keyframes')) { 230 | stats.criticals++; 231 | temp = appendFullBlock(criticalCss, line, 'keyframes'); 232 | removeMarkersInBlock(temp, blockMarkers, moduleMarkers); 233 | } else { 234 | appendFullBlock(criticalCss, line); 235 | line.remove(); // remove tagging comment 236 | } 237 | } else if (line.type === 'comment' && isStartTag(line.text, moduleMarkers)) { 238 | criticalActive = true; 239 | line.remove(); // remove tagging comment 240 | } else if (criticalActive === true && (line.type === 'atrule' && line.name === 'keyframes')) { //keyframes shouldn't be split 241 | stats.criticals++; 242 | temp = appendDeclaration(criticalCss, line); 243 | removeMarkersInBlock(temp, blockMarkers, moduleMarkers); 244 | 245 | if (hasEndMarker(line) === true) { 246 | criticalActive = false; 247 | } 248 | } else if (criticalActive === true && (line.type === 'decl' && hasParentAtRule(line, 'keyframes') === true)) { 249 | // ignore this rule... 250 | } else if (criticalActive === true && (line.type === 'atrule' && line.name === 'font-face')){ 251 | // this is a rather difficult one 252 | // @font-face is a 'naked atrule': it has no params at all 253 | // it is defined once every time you want to add a font 254 | // so we can't rely on 'searching for existing parent atrules' for new declarations as it might cross the context 255 | // so we manually add the atrule ourselves whenever we come across once 256 | stats.criticals++; 257 | appendEmptyRule(criticalCss, line); 258 | } else if (criticalActive === true && (line.type === 'decl' || line.type === 'comment')) { 259 | stats.criticals++; 260 | appendDeclaration(criticalCss, line); 261 | line.remove(); // remove line from originalCss as it is now alive in criticalCss 262 | } 263 | }); 264 | stats.endTime = timestamp(); 265 | 266 | originalCss.raws.semicolon = true; 267 | } 268 | 269 | function getModuleMarkers(startTag) { 270 | var modules = userOptions.modules, 271 | markers = null; 272 | 273 | if (userOptions.modules !== null) { 274 | markers = []; 275 | 276 | modules.forEach(function(currentModule){ 277 | stats.loops++; 278 | markers.push(startTag + userOptions.separator + currentModule); 279 | }); 280 | } 281 | 282 | return markers; 283 | } 284 | 285 | function isMarkedTag(currentText, marker, markers) { 286 | var result = false, 287 | typedMarker = marker, 288 | typedMarkers = markers; 289 | 290 | if (currentText.indexOf('! ') === 0) { 291 | typedMarker = '! ' + marker; 292 | 293 | if (typedMarkers !== null) { 294 | typedMarkers = markers.map(function(thisMarker) { 295 | return '! ' + thisMarker; 296 | }); 297 | } 298 | } 299 | 300 | if (currentText === typedMarker) { 301 | result = true; 302 | } else if (typedMarkers !== null && typedMarkers.indexOf(currentText) != -1) { 303 | result = true; 304 | } 305 | 306 | return result; 307 | } 308 | 309 | function isBlockTag(currentText, moduleMarkers) { 310 | return isMarkedTag(currentText, userOptions.blockTag, moduleMarkers); 311 | } 312 | 313 | function isStartTag(currentText, moduleMarkers) { 314 | return isMarkedTag(currentText, userOptions.startTag, moduleMarkers); 315 | } 316 | 317 | function getBlockFromTriggerTag(line, parentAtRule) { 318 | var result = null; 319 | 320 | if (typeof parentAtRule === 'undefined') { 321 | stats.parentRequest++; 322 | 323 | if (line.parent.type !== 'root') { 324 | result = line.parent; 325 | stats.parentRequest++; 326 | } 327 | } else { 328 | result = getParentAtRule(line, parentAtRule, true); 329 | } 330 | 331 | return result; 332 | } 333 | 334 | function appendFullBlock(criticalCss, line, parentAtRule) { 335 | var currentLevel = null, 336 | parents = null, 337 | block = getBlockFromTriggerTag(line, parentAtRule), 338 | temp = null; 339 | 340 | if (block !== null) { 341 | parents = getParents(line); 342 | 343 | if (block.type === 'atrule' && block.name === 'font-face') { 344 | appendEmptyRule(criticalCss, block); 345 | } 346 | 347 | currentLevel = prepareSelectors(criticalCss, parents); 348 | 349 | if (currentLevel.type === 'rule' || currentLevel.type === 'atrule') { 350 | if (block.name === 'keyframes') { 351 | if (areTheSame(block, currentLevel)) { 352 | temp = currentLevel; 353 | currentLevel = currentLevel.parent; 354 | temp.remove(); 355 | } 356 | currentLevel.append(clone(block)); 357 | } else { 358 | block.walk(function(currentLine) { 359 | var level = null; 360 | 361 | // we don't want to add the blockTag comment back; skip that 362 | // we don't want to loop over content that is inside a keyframes rule, it has been added already 363 | 364 | if (!(currentLine.type === 'comment' && line.text === currentLine.text)){ 365 | currentLevel.append(currentLine.clone()); 366 | stats.appends++; 367 | currentLine.remove(); 368 | currentLevel.raws.semicolon = true; 369 | } 370 | }); 371 | } 372 | } 373 | } 374 | 375 | return currentLevel; 376 | } 377 | 378 | function appendDeclaration(criticalCss, line) { 379 | var parents = getParents(line), 380 | currentLevel = prepareSelectors(criticalCss, parents), 381 | rule = clone(line); 382 | 383 | currentLevel.append(rule); 384 | stats.appends++; 385 | currentLevel.raws.semicolon = true; 386 | 387 | return rule; 388 | } 389 | 390 | function removeCommentIfMarker(blockMarkers, moduleMarkers, line) { 391 | if (line !== null) { 392 | if(line.type === 'comment' && (line.text === userOptions.endTag || isBlockTag(line.text, blockMarkers)) || isStartTag(line.text, moduleMarkers)) { 393 | line.remove(); 394 | } 395 | } 396 | } 397 | 398 | function removeMarkersInBlock(line, blockMarkers, moduleMarkers) { 399 | if (line !== null && typeof line.walkComments === 'function') { 400 | line.walkComments(removeCommentIfMarker.bind(null, blockMarkers, moduleMarkers)); 401 | } else { 402 | removeCommentIfMarker(blockMarkers, moduleMarkers, line); 403 | } 404 | } 405 | 406 | function appendEmptyRule(criticalCss, line) { 407 | var rule = clone(line, true); 408 | 409 | appendDeclaration(criticalCss, rule); 410 | 411 | return rule; 412 | } 413 | 414 | function prepareSelectors(criticalCss, selectorLevels) { 415 | var currentLevel = null; 416 | 417 | currentLevel = findSelector(criticalCss, selectorLevels); 418 | 419 | if (currentLevel === null) { 420 | currentLevel = createSelectorLevels(criticalCss, selectorLevels); 421 | currentLevel.raws.semicolon = true; 422 | } 423 | 424 | return currentLevel; 425 | } 426 | 427 | function createSelectorLevels(criticalCss, selectorLevels) { 428 | var i = null, 429 | currentLevel = null, 430 | temp = null; 431 | 432 | currentLevel = criticalCss; 433 | 434 | for (i = 0; i < selectorLevels.length; i++) { 435 | stats.loops++; 436 | temp = selectorLevels[i]; 437 | 438 | if (typeof currentLevel.last !== 'undefined' && areTheSame(temp, currentLevel.last)) { 439 | currentLevel = currentLevel.last; 440 | } else { 441 | if (currentLevel.name !== 'keyframes') { 442 | currentLevel.append(temp); 443 | currentLevel = temp; 444 | currentLevel.raws.semicolon = true; 445 | } 446 | 447 | temp = null; 448 | } 449 | } 450 | 451 | return currentLevel; 452 | } 453 | 454 | function findSelector(criticalCss, selectorLevels) { 455 | var result = null, 456 | currentLevel = null, 457 | temp = null, 458 | i = null; 459 | 460 | currentLevel = criticalCss; 461 | 462 | for (i = 0; i < selectorLevels.length; i++) { 463 | stats.loops++; 464 | temp = selectorLevels[i]; 465 | 466 | currentLevel = currentLevel.last; 467 | 468 | if (typeof currentLevel === 'undefined' || areTheSame(temp, currentLevel) === false) { 469 | currentLevel = null; 470 | break; 471 | } 472 | } 473 | 474 | result = currentLevel; 475 | 476 | return result; 477 | } 478 | 479 | function areTheSame(a, b) { 480 | var tempA = null, 481 | tempB = null, 482 | result = false; 483 | 484 | if (a.type === b.type) { 485 | stats.compares++; 486 | tempA = clone(a, true); 487 | tempB = clone(b, true); 488 | 489 | if (tempA.toString() === tempB.toString()) { 490 | result = true; 491 | } 492 | } 493 | 494 | return result; 495 | } 496 | 497 | function getParentAtRule(line, name) { 498 | var result = null, 499 | currentParent = line.parent; 500 | 501 | while (result === null && typeof currentParent !== 'undefined' && currentParent !== null) { 502 | if (currentParent.type === 'atrule' && currentParent.name === name) { 503 | result = currentParent; 504 | } else { 505 | stats.parentRequest++; 506 | currentParent = currentParent.parent; 507 | } 508 | } 509 | 510 | return result; 511 | } 512 | 513 | function hasParentAtRule(line, name) { 514 | var result = true, 515 | parent = getParentAtRule(line, name); 516 | 517 | if (parent === null) { 518 | result = false; 519 | } 520 | 521 | return result; 522 | } 523 | 524 | function getParents(line) { 525 | var parents = [], 526 | currentParent = null, 527 | temp = null; 528 | 529 | stats.getParents++; 530 | currentParent = line.parent; 531 | stats.parentRequest++; 532 | 533 | while (typeof currentParent !== 'undefined' && currentParent.type !== 'root') { 534 | temp = clone(currentParent, true); 535 | parents.push(temp); 536 | temp = null; 537 | currentParent = currentParent.parent; 538 | stats.parentRequest++; 539 | } 540 | 541 | parents = parents.reverse(); 542 | 543 | return parents; 544 | } 545 | 546 | function clone(originalRule, makeEmpty) { 547 | var newRule = null, 548 | temp = null; 549 | 550 | if (makeEmpty === true) { 551 | temp = originalRule.nodes; 552 | originalRule.nodes = []; 553 | newRule = originalRule.clone(); 554 | originalRule.nodes = temp; 555 | stats.emtpyClones++; 556 | } else { 557 | newRule = originalRule.clone(); 558 | stats.clones++; 559 | } 560 | 561 | return newRule; 562 | } 563 | 564 | module.exports = (opts = {}) => { 565 | return { 566 | postcssPlugin: 'postcss-critical-split', 567 | OnceExit: CriticalSplit( opts ), 568 | } 569 | }; 570 | module.exports.postcss = true; 571 | module.exports.output_types = output_types; 572 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-critical-split", 3 | "version": "2.5.3", 4 | "repository": "https://github.com/menocreativity/postcss-critical-split", 5 | "description": "A PostCSS plugin that takes existing CSS files and splits out the annotated critical styles into a seperate file.", 6 | "scripts": { 7 | "test": "clear && jest test.js" 8 | }, 9 | "keywords": [ 10 | "postcss", 11 | "critical css", 12 | "split", 13 | "css", 14 | "plugin" 15 | ], 16 | "author": "Ronny Welter", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "clear": "0.1.0", 20 | "jest": "^26.6.3", 21 | "postcss": "^8.3.0" 22 | }, 23 | "engines": { 24 | "node": ">=10.0.0" 25 | }, 26 | "peerDependencies": { 27 | "postcss": "^8.3.0" 28 | }, 29 | "jest": { 30 | "testEnvironment": "node", 31 | "bail": false, 32 | "collectCoverage": true, 33 | "collectCoverageFrom": [ 34 | "index.js" 35 | ] 36 | }, 37 | "files": [ 38 | "index.js" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var postcss = require('postcss'), 4 | criticalSplit = require('./index'), 5 | tests = null, 6 | directories = null, 7 | fs = require('fs'), 8 | path = require('path'), 9 | tests = null, 10 | singleRequest = process.argv[process.argv.length - 1]; 11 | 12 | if (singleRequest === 'test.js') singleRequest = null; 13 | 14 | function getDirectories(srcPath) { 15 | return fs.readdirSync(srcPath).filter(function(file) { 16 | return fs.statSync(path.join(srcPath, file)).isDirectory(); 17 | }); 18 | } 19 | 20 | function loadAllTests() { 21 | var testDirectory = './testsuite', 22 | directories = getDirectories(testDirectory), 23 | i = 0, 24 | currentPath = '', 25 | currentTest = null; 26 | 27 | tests = []; 28 | 29 | for (i; i < directories.length; i++) { 30 | currentPath = path.join(testDirectory, directories[i]); 31 | 32 | currentTest = {}; 33 | currentTest.setup = require('./' + path.join(currentPath, 'setup')); 34 | 35 | try { 36 | currentTest.split = require('./' + path.join(currentPath, 'split-settings')); 37 | } catch(ex) { 38 | //do nothing 39 | } 40 | currentTest.directory = currentPath; 41 | 42 | tests.push(currentTest); 43 | } 44 | } 45 | 46 | function getSingleTest(testName) { 47 | if (!testName) return null; 48 | 49 | var testDirectory = './testsuite', 50 | currentPath = path.join(testDirectory, testName), 51 | testIsValid = true, 52 | theTest = { 53 | 'directory': currentPath 54 | }; 55 | 56 | try { 57 | theTest.setup = require('./' + path.join(currentPath, 'setup')); 58 | } catch(e) { 59 | //this file is required 60 | testIsValid = false; 61 | } 62 | 63 | try { 64 | theTest.split = require('./' + path.join(currentPath, 'split-settings')); 65 | } catch(ex) { 66 | // this file is optional 67 | } 68 | 69 | if (!testIsValid) return null; 70 | 71 | return theTest; 72 | } 73 | 74 | function runTests() { 75 | var i = 0, 76 | currentTest = 0; 77 | 78 | for (i; i < tests.length; i++) { 79 | currentTest = tests[i]; 80 | createScenario(currentTest); 81 | } 82 | } 83 | 84 | function run(input, output, opts) { 85 | return postcss([criticalSplit(opts)]).process(input, { 86 | 'from': 'input.css', 87 | 'to': 'output.css' 88 | }) 89 | .then(function(result) { 90 | var resultCss = clean(result.root), 91 | outputCss = clean(output.root); 92 | 93 | expect(resultCss).toEqual(outputCss); 94 | expect(result.warnings().length).toBe(0); 95 | }); 96 | } 97 | 98 | function createRaws(type) { 99 | var result = null; 100 | 101 | if (type === 'decl') { 102 | result = { 103 | 'before': '\n ', 104 | 'after': ' ', 105 | 'between': ': ', 106 | 'semicolon': true, 107 | 'afterName': ' ', 108 | 'left': ' ', 109 | 'right': ' ', 110 | 'important': '!important' 111 | }; 112 | } else { 113 | result = { 114 | 'before': '\n ', 115 | 'after': '\n ', 116 | 'between': ' ', 117 | 'semicolon': true, 118 | 'afterName': ' ', 119 | 'left': ' ', 120 | 'right': ' ', 121 | 'important': '!important' 122 | }; 123 | } 124 | 125 | return result; 126 | } 127 | 128 | function clean(root) { 129 | root.raws = createRaws(); 130 | 131 | root.walk(function(line) { 132 | line.raws = createRaws(line.type); 133 | }); 134 | 135 | return root.toString(); 136 | } 137 | 138 | function createScenario(test) { 139 | var inputBytes = null, 140 | input = null, 141 | inputFile = './' + path.join(test.directory, test.setup.input), 142 | outputBytes = null, 143 | output = null, 144 | outputFile = './' + path.join(test.directory, test.setup.output), 145 | testResult = null, 146 | customProcess = null; 147 | 148 | inputBytes = fs.readFileSync(inputFile), 149 | input = postcss.parse(inputBytes, {'from': inputFile}).toResult(), 150 | outputBytes = fs.readFileSync(outputFile), 151 | output = postcss.parse(outputBytes, {'from': outputFile}).toResult(); 152 | 153 | if (typeof test.setup.process === 'string') { 154 | customProcess = require('./' + path.join(test.directory, test.setup.process)); 155 | 156 | it (test.setup.name, customProcess.bind(null, criticalSplit, input, output, test.split)); 157 | } else { 158 | it (test.setup.name, function(){ 159 | return run(input, output, test.split); 160 | }); 161 | } 162 | } 163 | 164 | var singleTest = getSingleTest(singleRequest); 165 | 166 | if (singleTest){ 167 | tests = []; 168 | tests.push(singleTest); 169 | } else { 170 | loadAllTests(); 171 | } 172 | 173 | runTests(); 174 | -------------------------------------------------------------------------------- /testsuite/deprecation-warn-save-feature/input.css: -------------------------------------------------------------------------------- 1 | strong { 2 | font-weight: bold; 3 | } 4 | 5 | p{ 6 | color: #333; 7 | } 8 | 9 | @media screen and (min-width: 300px) { 10 | strong { 11 | color: #f00; 12 | /* critical */ 13 | } 14 | } 15 | 16 | 17 | @media screen and (min-width: 500px) { 18 | p { 19 | float: left; 20 | text-decoration: underline; 21 | } 22 | } 23 | 24 | @media screen and (min-width: 300px) { 25 | li { 26 | display: block; 27 | float: left; 28 | text-align: center; 29 | padding: 0; 30 | /* critical */ 31 | margin: 0; 32 | position: relative; 33 | border: 1px solid red; 34 | } 35 | } 36 | 37 | table { 38 | background-color: green; 39 | /* critical */ 40 | border-top: 1px solid blue; 41 | } 42 | 43 | h1 { 44 | text-decoration: underline; 45 | } 46 | -------------------------------------------------------------------------------- /testsuite/deprecation-warn-save-feature/output.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 300px) { 2 | strong { 3 | color: #f00; 4 | } 5 | 6 | li { 7 | display: block; 8 | float: left; 9 | text-align: center; 10 | padding: 0; 11 | margin: 0; 12 | position: relative; 13 | border: 1px solid red; 14 | } 15 | } 16 | 17 | table { 18 | background-color: green; 19 | border-top: 1px solid blue; 20 | } 21 | -------------------------------------------------------------------------------- /testsuite/deprecation-warn-save-feature/process.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'), 2 | fs = require('fs'); 3 | 4 | module.exports = function(criticalSplit, input, output, opts) { 5 | spyOn(console, 'warn').and.callFake(function(message){ 6 | // do nothing 7 | }); 8 | 9 | spyOn(fs, 'writeFileSync').and.callFake(function(path, data) { 10 | return true; 11 | }); 12 | 13 | expect(console.warn).not.toHaveBeenCalled(); 14 | 15 | return postcss([criticalSplit(opts)]).process(input) 16 | .then(function(result) { 17 | expect(console.warn).toHaveBeenCalled(); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /testsuite/deprecation-warn-save-feature/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should log a deprecation warning for the save feature", 3 | "input": "input.css", 4 | "output": "output.css", 5 | "process": "process.js" 6 | } 7 | -------------------------------------------------------------------------------- /testsuite/deprecation-warn-save-feature/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "save": true 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/dev-debug-mode/input.css: -------------------------------------------------------------------------------- 1 | strong { 2 | font-weight: bold; 3 | } 4 | 5 | p{ 6 | color: #333; 7 | } 8 | 9 | @media screen and (min-width: 300px) { 10 | strong { 11 | color: #f00; 12 | /* critical */ 13 | } 14 | } 15 | 16 | 17 | @media screen and (min-width: 500px) { 18 | p { 19 | float: left; 20 | text-decoration: underline; 21 | } 22 | } 23 | 24 | @media screen and (min-width: 300px) { 25 | li { 26 | display: block; 27 | float: left; 28 | text-align: center; 29 | padding: 0; 30 | /* critical */ 31 | margin: 0; 32 | position: relative; 33 | border: 1px solid red; 34 | } 35 | } 36 | 37 | table { 38 | background-color: green; 39 | /* critical */ 40 | border-top: 1px solid blue; 41 | } 42 | 43 | h1 { 44 | text-decoration: underline; 45 | } 46 | -------------------------------------------------------------------------------- /testsuite/dev-debug-mode/output.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 300px) { 2 | strong { 3 | color: #f00; 4 | } 5 | 6 | li { 7 | display: block; 8 | float: left; 9 | text-align: center; 10 | padding: 0; 11 | margin: 0; 12 | position: relative; 13 | border: 1px solid red; 14 | } 15 | } 16 | 17 | table { 18 | background-color: green; 19 | border-top: 1px solid blue; 20 | } 21 | -------------------------------------------------------------------------------- /testsuite/dev-debug-mode/process.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | 3 | 4 | module.exports = function(criticalSplit, input, output, opts) { 5 | spyOn(console, 'log').and.callFake(function(message){ 6 | // do nothing 7 | }); 8 | 9 | return postcss([criticalSplit(opts)]).process(input) 10 | .then( function(result) { 11 | expect(console.log).toHaveBeenCalledTimes(1); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /testsuite/dev-debug-mode/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should log statistics when debug mode is enabled", 3 | "input": "input.css", 4 | "output": "output.css", 5 | "process": "process.js" 6 | } 7 | -------------------------------------------------------------------------------- /testsuite/dev-debug-mode/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug": true 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/errors-error-same-start-end-tag/input.css: -------------------------------------------------------------------------------- 1 | strong { 2 | font-weight: bold; 3 | } 4 | 5 | p{ 6 | color: #333; 7 | } 8 | 9 | @media screen and (min-width: 300px) { 10 | strong { 11 | color: #f00; 12 | /* critical */ 13 | } 14 | } 15 | 16 | 17 | @media screen and (min-width: 500px) { 18 | p { 19 | float: left; 20 | text-decoration: underline; 21 | } 22 | } 23 | 24 | @media screen and (min-width: 300px) { 25 | li { 26 | display: block; 27 | float: left; 28 | text-align: center; 29 | padding: 0; 30 | /* critical */ 31 | margin: 0; 32 | position: relative; 33 | border: 1px solid red; 34 | } 35 | } 36 | 37 | table { 38 | background-color: green; 39 | /* critical */ 40 | border-top: 1px solid blue; 41 | } 42 | 43 | h1 { 44 | text-decoration: underline; 45 | } 46 | -------------------------------------------------------------------------------- /testsuite/errors-error-same-start-end-tag/output.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 300px) { 2 | strong { 3 | color: #f00; 4 | } 5 | 6 | li { 7 | display: block; 8 | float: left; 9 | text-align: center; 10 | padding: 0; 11 | margin: 0; 12 | position: relative; 13 | border: 1px solid red; 14 | } 15 | } 16 | 17 | table { 18 | background-color: green; 19 | border-top: 1px solid blue; 20 | } 21 | -------------------------------------------------------------------------------- /testsuite/errors-error-same-start-end-tag/process.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | 3 | 4 | module.exports = function(criticalSplit, input, output, opts) { 5 | spyOn(console, 'error').and.callFake(function(message){ 6 | // do nothing 7 | }); 8 | 9 | return postcss([criticalSplit(opts)]).process(input) 10 | .then( function(result) { 11 | expect(console.error).toHaveBeenCalledTimes(1); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /testsuite/errors-error-same-start-end-tag/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should not throw an error when starttag and endtag are the same", 3 | "input": "input.css", 4 | "output": "output.css", 5 | "process": "process.js" 6 | } 7 | -------------------------------------------------------------------------------- /testsuite/errors-error-same-start-end-tag/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "startTag": "hallo-tag", 3 | "endTag": "hallo-tag" 4 | } 5 | -------------------------------------------------------------------------------- /testsuite/errors-prevent-crash-without-split-options/input.css: -------------------------------------------------------------------------------- 1 | strong { 2 | font-weight: bold; 3 | } 4 | 5 | p{ 6 | color: #333; 7 | } 8 | 9 | @media screen and (min-width: 300px) { 10 | strong { 11 | color: #f00; 12 | /* critical */ 13 | } 14 | } 15 | 16 | 17 | @media screen and (min-width: 500px) { 18 | p { 19 | float: left; 20 | text-decoration: underline; 21 | } 22 | } 23 | 24 | @media screen and (min-width: 300px) { 25 | li { 26 | display: block; 27 | float: left; 28 | text-align: center; 29 | padding: 0; 30 | /* critical */ 31 | margin: 0; 32 | position: relative; 33 | border: 1px solid red; 34 | } 35 | } 36 | 37 | table { 38 | background-color: green; 39 | /* critical */ 40 | border-top: 1px solid blue; 41 | } 42 | 43 | h1 { 44 | text-decoration: underline; 45 | } 46 | -------------------------------------------------------------------------------- /testsuite/errors-prevent-crash-without-split-options/output.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 300px) { 2 | strong { 3 | color: #f00; 4 | } 5 | 6 | li { 7 | display: block; 8 | float: left; 9 | text-align: center; 10 | padding: 0; 11 | margin: 0; 12 | position: relative; 13 | border: 1px solid red; 14 | } 15 | } 16 | 17 | table { 18 | background-color: green; 19 | border-top: 1px solid blue; 20 | } 21 | -------------------------------------------------------------------------------- /testsuite/errors-prevent-crash-without-split-options/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should not crash when no split options are passed to the plugin", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/markers-block-tags/input.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | 9 | a { 10 | /* critical */ 11 | text-decoration: none; 12 | } 13 | 14 | ul { 15 | padding: 0; 16 | } 17 | -------------------------------------------------------------------------------- /testsuite/markers-block-tags/output.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/markers-block-tags/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should split rules using blocktags", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/markers-block-tags/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/markers-custom-block-tag/input.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | 9 | a { 10 | /* this-is-critical */ 11 | text-decoration: none; 12 | } 13 | 14 | ul { 15 | padding: 0; 16 | } 17 | -------------------------------------------------------------------------------- /testsuite/markers-custom-block-tag/output.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/markers-custom-block-tag/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should split using custom block tags", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/markers-custom-block-tag/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockTag": "this-is-critical" 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/markers-custom-start-end-tags/input.css: -------------------------------------------------------------------------------- 1 | /* grab-here */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | /* stop-here */ 10 | 11 | a { 12 | text-decoration: none; 13 | } 14 | -------------------------------------------------------------------------------- /testsuite/markers-custom-start-end-tags/output.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | -------------------------------------------------------------------------------- /testsuite/markers-custom-start-end-tags/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should split using custom start/end-tags", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/markers-custom-start-end-tags/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "startTag": "grab-here", 3 | "endTag": "stop-here" 4 | } 5 | -------------------------------------------------------------------------------- /testsuite/markers-start-end-tags/input.css: -------------------------------------------------------------------------------- 1 | /* critical:start */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | /* critical:end */ 10 | 11 | a { 12 | text-decoration: none; 13 | } 14 | -------------------------------------------------------------------------------- /testsuite/markers-start-end-tags/output.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | -------------------------------------------------------------------------------- /testsuite/markers-start-end-tags/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should split rules using start-end tags", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/markers-start-end-tags/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/media-queries-advanced-start-end/input.css: -------------------------------------------------------------------------------- 1 | strong { 2 | font-weight: bold; 3 | } 4 | 5 | /* critical:start */ 6 | p{ 7 | color: #333; 8 | } 9 | 10 | @media screen and (min-width: 300px) { 11 | strong { 12 | color: #f00; 13 | } 14 | 15 | p { 16 | float: left; 17 | /* critical:end */ 18 | text-decoration: underline; 19 | } 20 | 21 | li { 22 | display: block; 23 | 24 | float: left; 25 | text-align: center; 26 | padding: 0; 27 | /* critical:start */ 28 | margin: 0; 29 | position: relative; 30 | border: 1px solid red; 31 | } 32 | } 33 | 34 | table { 35 | background-color: green; 36 | /* critical:end */ 37 | border-top: 1px solid blue; 38 | } 39 | 40 | h1 { 41 | text-decoration: underline; 42 | } 43 | -------------------------------------------------------------------------------- /testsuite/media-queries-advanced-start-end/output.css: -------------------------------------------------------------------------------- 1 | p{ 2 | color: #333; 3 | } 4 | 5 | @media screen and (min-width: 300px) { 6 | strong { 7 | color: #f00; 8 | } 9 | 10 | p { 11 | float: left; 12 | } 13 | 14 | li { 15 | margin: 0; 16 | position: relative; 17 | border: 1px solid red; 18 | } 19 | } 20 | 21 | table { 22 | background-color: green; 23 | } 24 | -------------------------------------------------------------------------------- /testsuite/media-queries-advanced-start-end/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should keep the media-query nesting intact with unmatched start and end tag nesting levels", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/media-queries-advanced-start-end/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/media-queries-append-001/input.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | 9 | a { 10 | text-decoration: none; 11 | } 12 | 13 | @media screen and (min-width: 400px) { 14 | a { 15 | color: #ff0; 16 | border-bottom: 1px solid #ff0; 17 | /* critical */ 18 | } 19 | } 20 | 21 | section { 22 | position: absolute; 23 | } 24 | 25 | @media screen and (min-width: 400px) { 26 | ul div { 27 | float: left; 28 | /* critical */ 29 | } 30 | 31 | section:before { 32 | content: ' '; 33 | /* critical */ 34 | clear: both; 35 | display: block; 36 | } 37 | } 38 | 39 | h1 { 40 | font-size: 3em; 41 | font-weight: bold; 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /testsuite/media-queries-append-001/output.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 400px) { 2 | a { 3 | color: #ff0; 4 | border-bottom: 1px solid #ff0; 5 | } 6 | 7 | ul div { 8 | float: left; 9 | } 10 | 11 | section:before { 12 | content: ' '; 13 | clear: both; 14 | display: block; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testsuite/media-queries-append-001/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should append new critical rules to existing media-query selectors (mq - normal- mq)", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/media-queries-append-001/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/media-queries-append-002/input.css: -------------------------------------------------------------------------------- 1 | strong { 2 | font-weight: bold; 3 | } 4 | 5 | p{ 6 | color: #333; 7 | } 8 | 9 | @media screen and (min-width: 300px) { 10 | strong { 11 | color: #f00; 12 | /* critical */ 13 | } 14 | } 15 | 16 | 17 | @media screen and (min-width: 500px) { 18 | p { 19 | float: left; 20 | text-decoration: underline; 21 | } 22 | } 23 | 24 | @media screen and (min-width: 300px) { 25 | li { 26 | display: block; 27 | float: left; 28 | text-align: center; 29 | padding: 0; 30 | /* critical */ 31 | margin: 0; 32 | position: relative; 33 | border: 1px solid red; 34 | } 35 | } 36 | 37 | table { 38 | background-color: green; 39 | /* critical */ 40 | border-top: 1px solid blue; 41 | } 42 | 43 | h1 { 44 | text-decoration: underline; 45 | } 46 | -------------------------------------------------------------------------------- /testsuite/media-queries-append-002/output.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 300px) { 2 | strong { 3 | color: #f00; 4 | } 5 | 6 | li { 7 | display: block; 8 | float: left; 9 | text-align: center; 10 | padding: 0; 11 | margin: 0; 12 | position: relative; 13 | border: 1px solid red; 14 | } 15 | } 16 | 17 | table { 18 | background-color: green; 19 | border-top: 1px solid blue; 20 | } 21 | -------------------------------------------------------------------------------- /testsuite/media-queries-append-002/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should append new critical rules to existing media-query selectors (mq - mq- mq)", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/media-queries-append-002/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/media-queries-basic-start-end/input.css: -------------------------------------------------------------------------------- 1 | strong { 2 | font-weight: bold; 3 | } 4 | 5 | p{ 6 | color: #333; 7 | /* critical */ 8 | } 9 | 10 | @media screen and (min-width: 300px) { 11 | strong { 12 | color: #f00; 13 | } 14 | 15 | p { 16 | /* critical */ 17 | float: left; 18 | text-decoration: underline; 19 | } 20 | 21 | li { 22 | display: block; 23 | /* critical:start */ 24 | float: left; 25 | text-align: center; 26 | padding: 0; 27 | /* critical:end */ 28 | margin: 0; 29 | position: relative; 30 | border: 1px solid red; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /testsuite/media-queries-basic-start-end/output.css: -------------------------------------------------------------------------------- 1 | p{ 2 | color: #333; 3 | } 4 | 5 | @media screen and (min-width: 300px) { 6 | p { 7 | float: left; 8 | text-decoration: underline; 9 | } 10 | 11 | li { 12 | float: left; 13 | text-align: center; 14 | padding: 0; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testsuite/media-queries-basic-start-end/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should keep the media-query nesting intact while only grabbing a few rules", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/media-queries-basic-start-end/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/media-queries-basic/input.css: -------------------------------------------------------------------------------- 1 | strong { 2 | font-weight: bold; 3 | } 4 | 5 | @media screen and (min-width: 300px) { 6 | strong { 7 | color: #f00; 8 | } 9 | 10 | p { 11 | /* critical */ 12 | float: left; 13 | text-decoration: underline; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /testsuite/media-queries-basic/output.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 300px) { 2 | p { 3 | float: left; 4 | text-decoration: underline; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /testsuite/media-queries-basic/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should keep the media-query nesting intact", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/media-queries-basic/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/media-queries-split/input.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | 9 | a { 10 | text-decoration: none; 11 | } 12 | 13 | @media screen and (min-width: 400px) { 14 | a { 15 | color: #ff0; 16 | border-bottom: 1px solid #ff0; 17 | /* critical */ 18 | } 19 | } 20 | 21 | @media screen and (min-width: 450px) { 22 | strong { 23 | transform: scale(200%); 24 | } 25 | } 26 | 27 | h1 { 28 | font-size: 3em; 29 | font-weight: bold; 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /testsuite/media-queries-split/output.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 400px) { 2 | a { 3 | color: #ff0; 4 | border-bottom: 1px solid #ff0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /testsuite/media-queries-split/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should split nested media queries correctly based on blocktags", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/media-queries-split/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/modules-basic/input.css: -------------------------------------------------------------------------------- 1 | /* critical:start:header */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | /* critical:end */ 10 | 11 | a { 12 | text-decoration: none; 13 | } 14 | 15 | table { 16 | width: 100%; 17 | height: 200%; 18 | } 19 | 20 | * { 21 | font-family: Arial, "Arial Black", sans-serif; 22 | } 23 | 24 | span { 25 | /* critical:nav */ 26 | display: inline-block; 27 | color: #333; 28 | } 29 | -------------------------------------------------------------------------------- /testsuite/modules-basic/output.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | 9 | span { 10 | display: inline-block; 11 | color: #333; 12 | } 13 | -------------------------------------------------------------------------------- /testsuite/modules-basic/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should grab the criticals listed in the modules option", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/modules-basic/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": ["header", "logo", "nav"] 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/modules-media-queries/input.css: -------------------------------------------------------------------------------- 1 | /* critical:start:header */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | 10 | @media screen and (min-width: 400px) { 11 | p { 12 | display: inline-block; 13 | border: red thin solid; 14 | } 15 | 16 | div { 17 | float: left; 18 | } 19 | 20 | div::before { 21 | display: table; 22 | } 23 | } 24 | /* critical:end */ 25 | 26 | a { 27 | text-decoration: none; 28 | } 29 | 30 | table { 31 | width: 100%; 32 | height: 200%; 33 | } 34 | 35 | * { 36 | font-family: Arial, "Arial Black", sans-serif; 37 | } 38 | 39 | @media screen and (min-width: 500px) { 40 | p { 41 | display: inline-block; 42 | border: red thin solid; 43 | } 44 | 45 | div { 46 | /* critical:nav */ 47 | float: left; 48 | } 49 | 50 | div::before { 51 | display: table; 52 | } 53 | } 54 | 55 | span { 56 | /* critical:header */ 57 | display: inline-block; 58 | color: #333; 59 | } 60 | -------------------------------------------------------------------------------- /testsuite/modules-media-queries/output.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | 9 | @media screen and (min-width: 400px) { 10 | p { 11 | display: inline-block; 12 | border: red thin solid; 13 | } 14 | 15 | div { 16 | float: left; 17 | } 18 | 19 | div::before { 20 | display: table; 21 | } 22 | } 23 | 24 | @media screen and (min-width: 500px) { 25 | div { 26 | float: left; 27 | } 28 | } 29 | 30 | span { 31 | display: inline-block; 32 | color: #333; 33 | } 34 | -------------------------------------------------------------------------------- /testsuite/modules-media-queries/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should grab modules and keep custom media queries in place", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/modules-media-queries/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": ["header", "logo", "nav"] 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/modules-no-array/input.css: -------------------------------------------------------------------------------- 1 | /* critical:start:header */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | /* critical:end */ 10 | 11 | a { 12 | text-decoration: none; 13 | } 14 | 15 | table { 16 | width: 100%; 17 | height: 200%; 18 | } 19 | 20 | * { 21 | font-family: Arial, "Arial Black", sans-serif; 22 | } 23 | 24 | span { 25 | /* critical:nav */ 26 | display: inline-block; 27 | color: #333; 28 | } 29 | -------------------------------------------------------------------------------- /testsuite/modules-no-array/output.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | -------------------------------------------------------------------------------- /testsuite/modules-no-array/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should support single modules instead of arrays of modules listed in the modules option", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/modules-no-array/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": "header" 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/modules-separator/input.css: -------------------------------------------------------------------------------- 1 | /* critical:start#header */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | /* critical:end */ 10 | 11 | a { 12 | text-decoration: none; 13 | } 14 | 15 | table { 16 | width: 100%; 17 | height: 200%; 18 | } 19 | 20 | * { 21 | font-family: Arial, "Arial Black", sans-serif; 22 | } 23 | 24 | span { 25 | /* critical#header */ 26 | display: inline-block; 27 | color: #333; 28 | } 29 | -------------------------------------------------------------------------------- /testsuite/modules-separator/output.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | 9 | span { 10 | display: inline-block; 11 | color: #333; 12 | } 13 | -------------------------------------------------------------------------------- /testsuite/modules-separator/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should allow for custom separators in modules", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/modules-separator/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": ["header", "logo", "nav"], 3 | "separator": "#" 4 | } 5 | -------------------------------------------------------------------------------- /testsuite/multiple-font-faces/input.css: -------------------------------------------------------------------------------- 1 | /* critical:start */ 2 | @font-face { 3 | font-family: "myFancyFont"; 4 | font-weight: normal; 5 | font-style: normal; 6 | src: url('myfile.woff') format('woff'); 7 | } 8 | /* critical:end */ 9 | 10 | a { 11 | text-decoration: none; 12 | } 13 | 14 | table { 15 | width: 100%; 16 | height: 200%; 17 | } 18 | 19 | @font-face { 20 | font-family: "myFancyFont2"; 21 | font-weight: normal; 22 | font-style: normal; 23 | src: url('myfile2.woff') format('woff2'); 24 | /* critical */ 25 | } 26 | 27 | * { 28 | font-family: Arial, "Arial Black", sans-serif; 29 | } 30 | 31 | span { 32 | /* critical */ 33 | display: inline-block; 34 | color: #333; 35 | } 36 | -------------------------------------------------------------------------------- /testsuite/multiple-font-faces/output.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "myFancyFont"; 3 | font-weight: normal; 4 | font-style: normal; 5 | src: url('myfile.woff') format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: "myFancyFont2"; 10 | font-weight: normal; 11 | font-style: normal; 12 | src: url('myfile2.woff') format('woff2'); 13 | } 14 | 15 | span { 16 | display: inline-block; 17 | color: #333; 18 | } 19 | -------------------------------------------------------------------------------- /testsuite/multiple-font-faces/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should always split up font-face rules and not merge them", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/multiple-font-faces/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-critical/input.css: -------------------------------------------------------------------------------- 1 | /* critical:start */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | /* critical:end */ 10 | 11 | a { 12 | text-decoration: none; 13 | } 14 | 15 | table { 16 | width: 100%; 17 | height: 200%; 18 | } 19 | 20 | * { 21 | font-family: Arial, "Arial Black", sans-serif; 22 | } 23 | 24 | span { 25 | /* critical */ 26 | display: inline-block; 27 | color: #333; 28 | } 29 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-critical/output.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | 9 | span { 10 | display: inline-block; 11 | color: #333; 12 | } 13 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-critical/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should return critical-css when in critical-output-mode", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-critical/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "output": "critical" 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-default/input.css: -------------------------------------------------------------------------------- 1 | /* critical:start */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | /* critical:end */ 10 | 11 | a { 12 | text-decoration: none; 13 | } 14 | 15 | table { 16 | width: 100%; 17 | height: 200%; 18 | } 19 | 20 | * { 21 | font-family: Arial, "Arial Black", sans-serif; 22 | } 23 | 24 | span { 25 | /* critical */ 26 | display: inline-block; 27 | color: #333; 28 | } 29 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-default/output.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | } 8 | 9 | span { 10 | display: inline-block; 11 | color: #333; 12 | } 13 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-default/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should use critical as default output mode", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-default/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-input/input.css: -------------------------------------------------------------------------------- 1 | /* critical:start */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | /* critical:end */ 10 | 11 | a { 12 | text-decoration: none; 13 | } 14 | 15 | table { 16 | width: 100%; 17 | height: 200%; 18 | } 19 | 20 | * { 21 | font-family: Arial, "Arial Black", sans-serif; 22 | } 23 | 24 | span { 25 | /* critical */ 26 | display: inline-block; 27 | color: #333; 28 | } 29 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-input/output.css: -------------------------------------------------------------------------------- 1 | /* critical:start */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | /* critical:end */ 10 | 11 | a { 12 | text-decoration: none; 13 | } 14 | 15 | table { 16 | width: 100%; 17 | height: 200%; 18 | } 19 | 20 | * { 21 | font-family: Arial, "Arial Black", sans-serif; 22 | } 23 | 24 | span { 25 | /* critical */ 26 | display: inline-block; 27 | color: #333; 28 | } 29 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-input/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should return original input when in input-output-mode", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-input/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "output": "input" 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-rest/input.css: -------------------------------------------------------------------------------- 1 | /* critical:start */ 2 | body { 3 | background-color: black; 4 | } 5 | 6 | p { 7 | color: red; 8 | } 9 | /* critical:end */ 10 | 11 | a { 12 | text-decoration: none; 13 | } 14 | 15 | table { 16 | width: 100%; 17 | height: 200%; 18 | } 19 | 20 | * { 21 | font-family: Arial, "Arial Black", sans-serif; 22 | } 23 | 24 | span { 25 | /* critical */ 26 | display: inline-block; 27 | color: #333; 28 | } 29 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-rest/output.css: -------------------------------------------------------------------------------- 1 | 2 | a { 3 | text-decoration: none; 4 | } 5 | 6 | table { 7 | width: 100%; 8 | height: 200%; 9 | } 10 | 11 | * { 12 | font-family: Arial, "Arial Black", sans-serif; 13 | } 14 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-rest/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should return rest-css when in rest-output-mode", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/ouput-mode-rest/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "output": "rest" 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/save-feature/input.css: -------------------------------------------------------------------------------- 1 | strong { 2 | font-weight: bold; 3 | } 4 | 5 | p{ 6 | color: #333; 7 | } 8 | 9 | @media screen and (min-width: 300px) { 10 | strong { 11 | color: #f00; 12 | /* critical */ 13 | } 14 | } 15 | 16 | 17 | @media screen and (min-width: 500px) { 18 | p { 19 | float: left; 20 | text-decoration: underline; 21 | } 22 | } 23 | 24 | @media screen and (min-width: 300px) { 25 | li { 26 | display: block; 27 | float: left; 28 | text-align: center; 29 | padding: 0; 30 | /* critical */ 31 | margin: 0; 32 | position: relative; 33 | border: 1px solid red; 34 | } 35 | } 36 | 37 | table { 38 | background-color: green; 39 | /* critical */ 40 | border-top: 1px solid blue; 41 | } 42 | 43 | h1 { 44 | text-decoration: underline; 45 | } 46 | -------------------------------------------------------------------------------- /testsuite/save-feature/output.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 300px) { 2 | strong { 3 | color: #f00; 4 | } 5 | 6 | li { 7 | display: block; 8 | float: left; 9 | text-align: center; 10 | padding: 0; 11 | margin: 0; 12 | position: relative; 13 | border: 1px solid red; 14 | } 15 | } 16 | 17 | table { 18 | background-color: green; 19 | border-top: 1px solid blue; 20 | } 21 | -------------------------------------------------------------------------------- /testsuite/save-feature/process.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'), 2 | fs = require('fs'); 3 | 4 | 5 | module.exports = function(criticalSplit, input, output, opts) { 6 | spyOn(console, 'warn').and.callFake(function(message){ 7 | // do nothing 8 | }); 9 | 10 | spyOn(fs, 'writeFileSync').and.callFake(function(path, data) { 11 | return true; 12 | }); 13 | 14 | return postcss([criticalSplit(opts)]).process(input) 15 | .then( function(result) { 16 | expect(fs.writeFileSync).toHaveBeenCalledTimes(2); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /testsuite/save-feature/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should save files to disk when the save option is enabled", 3 | "input": "input.css", 4 | "output": "output.css", 5 | "process": "process.js" 6 | } 7 | -------------------------------------------------------------------------------- /testsuite/save-feature/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "save": true 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/special-rules-keyframes-blocktag/input.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | animation: do-something-cool 1s infinite; 8 | } 9 | 10 | a { 11 | /* critical */ 12 | text-decoration: none; 13 | } 14 | 15 | ul { 16 | padding: 0; 17 | } 18 | 19 | @keyframes do-something-amazing { 20 | 0% { 21 | padding: 0px; 22 | opacity: 1; 23 | } 24 | 25 | 50% { 26 | padding: 20px; 27 | opacity: 0; 28 | /* critical */ 29 | } 30 | 31 | 100% { 32 | padding: 5px; 33 | opacity: .8; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /testsuite/special-rules-keyframes-blocktag/output.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | } 4 | 5 | @keyframes do-something-amazing { 6 | 0% { 7 | padding: 0px; 8 | opacity: 1; 9 | } 10 | 11 | 50% { 12 | padding: 20px; 13 | opacity: 0; 14 | } 15 | 16 | 100% { 17 | padding: 5px; 18 | opacity: .8; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testsuite/special-rules-keyframes-blocktag/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should split keyframes without breaking them when tagging anything inside with a blocktag", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/special-rules-keyframes-blocktag/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/special-rules-keyframes-start-end-tag/input.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | p { 6 | color: red; 7 | animation: do-something-cool 1s infinite; 8 | } 9 | 10 | a { 11 | /* critical */ 12 | text-decoration: none; 13 | } 14 | 15 | ul { 16 | padding: 0; 17 | } 18 | 19 | /* critical:start */ 20 | @keyframes do-something-cool { 21 | 0% { 22 | padding: 0px; 23 | opacity: 1; 24 | } 25 | 26 | 50% { 27 | padding: 20px; 28 | opacity: 0; 29 | /* critical:end */ 30 | } 31 | 32 | 100% { 33 | padding: 0px; 34 | opacity: 1; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /testsuite/special-rules-keyframes-start-end-tag/output.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | } 4 | 5 | @keyframes do-something-cool { 6 | 0% { 7 | padding: 0px; 8 | opacity: 1; 9 | } 10 | 11 | 50% { 12 | padding: 20px; 13 | opacity: 0; 14 | } 15 | 16 | 100% { 17 | padding: 0px; 18 | opacity: 1; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testsuite/special-rules-keyframes-start-end-tag/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should split keyframes without breaking them when tagging anything inside with a start/end tag", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/special-rules-keyframes-start-end-tag/split-settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testsuite/support-important-comments/input.css: -------------------------------------------------------------------------------- 1 | /*! critical:start */ 2 | strong { 3 | font-weight: bold; 4 | } 5 | 6 | p{ 7 | color: #333; 8 | } 9 | /*! critical:end */ 10 | 11 | @media screen and (min-width: 300px) { 12 | strong { 13 | color: #f00; 14 | /*! critical */ 15 | } 16 | } 17 | 18 | 19 | @media screen and (min-width: 500px) { 20 | p { 21 | /*! critical:start:header */ 22 | float: left; 23 | /* critical:end */ 24 | text-decoration: underline; 25 | } 26 | } 27 | 28 | @media screen and (min-width: 300px) { 29 | li { 30 | display: block; 31 | float: left; 32 | text-align: center; 33 | padding: 0; 34 | /*! critical */ 35 | margin: 0; 36 | position: relative; 37 | border: 1px solid red; 38 | } 39 | } 40 | 41 | table { 42 | background-color: green; 43 | /*! critical */ 44 | border-top: 1px solid blue; 45 | } 46 | 47 | h1 { 48 | text-decoration: underline; 49 | } 50 | -------------------------------------------------------------------------------- /testsuite/support-important-comments/output.css: -------------------------------------------------------------------------------- 1 | strong { 2 | font-weight: bold; 3 | } 4 | 5 | p{ 6 | color: #333; 7 | } 8 | 9 | @media screen and (min-width: 300px) { 10 | strong { 11 | color: #f00; 12 | } 13 | } 14 | 15 | @media screen and (min-width: 500px) { 16 | p { 17 | float: left; 18 | } 19 | } 20 | 21 | @media screen and (min-width: 300px) { 22 | li { 23 | display: block; 24 | float: left; 25 | text-align: center; 26 | padding: 0; 27 | margin: 0; 28 | position: relative; 29 | border: 1px solid red; 30 | } 31 | } 32 | 33 | table { 34 | background-color: green; 35 | border-top: 1px solid blue; 36 | } 37 | -------------------------------------------------------------------------------- /testsuite/support-important-comments/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should also support the use of important comments", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/support-important-comments/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": ["header"] 3 | } 4 | -------------------------------------------------------------------------------- /testsuite/weird-first-line-bug/input.css: -------------------------------------------------------------------------------- 1 | table { 2 | display: block; 3 | width: 100%; 4 | } 5 | 6 | /* critical:start */ 7 | .logobar { 8 | width: 80px; 9 | height: 100%; 10 | z-index: 999; 11 | position: fixed; } 12 | /* critical:end */ 13 | 14 | .strong { 15 | font-weight: bold; 16 | } 17 | -------------------------------------------------------------------------------- /testsuite/weird-first-line-bug/output.css: -------------------------------------------------------------------------------- 1 | table { 2 | display: block; 3 | width: 100%; 4 | } 5 | 6 | .strong { 7 | font-weight: bold; 8 | } 9 | -------------------------------------------------------------------------------- /testsuite/weird-first-line-bug/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should not accitentally remove the first line", 3 | "input": "input.css", 4 | "output": "output.css" 5 | } 6 | -------------------------------------------------------------------------------- /testsuite/weird-first-line-bug/split-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "output": "rest" 3 | } 4 | --------------------------------------------------------------------------------