Socket
Socket
Sign inDemoInstall

postcss-critical-css

Package Overview
Dependencies
93
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.0.3 to 2.0.0

.eslintrc

212

index.js

@@ -1,211 +0,1 @@

'use strict';
var chalk = require('chalk');
var postcss = require('postcss');
var cssnano = require('cssnano');
var fs = require('fs');
var path = require('path');
/**
* Throw a warning if a critical selector is used more than once.
*
* @param {array} Array of critical CSS rules.
* @param {selector} Selector to check for.
* @return {string} Console warning.
*/
function selectorReuseWarning(rules, selector) {
return rules.reduce(function (init, rule) {
if (rule.selector === selector) {
console.warn('Warning: Selector ' + selector + ' is used more than once.');
}
return rules;
}, []);
}
/**
* Identify critical CSS selectors
*
* @param {obj} PostCSS CSS object.
* @return {object} Object containing critical rules, organized by output destination
*/
function getCriticalRules(css, preserve) {
var critical = {};
css.walkDecls('critical-selector', function (decl) {
var dest = getDest(decl.parent);
if ('undefined' === typeof critical[dest]) {
critical[dest] = [];
}
switch (decl.value) {
case 'scope':
var childRules = getChildRules(css, decl.parent, preserve);
selectorReuseWarning(critical[dest], decl.parent.selector);
// Make sure the parent selector contains declarations
if (decl.parent.nodes.length > 1) {
critical[dest].push(decl.parent);
}
// Push all child rules
if (childRules !== null && childRules.length) {
childRules.forEach(function (rule) {
critical[dest].push(rule);
});
}
break;
case 'this':
selectorReuseWarning(critical[dest], decl.parent.selector);
critical[dest].push(decl.parent);
break;
default:
var container = decl.parent;
container.selector = decl.value.replace(/['"]*/g, '');
selectorReuseWarning(critical[dest], container.selector);
critical[dest].push(container);
break;
}
decl.remove();
});
return critical;
}
/**
* Get rules for selectors nested within parent node
*
* @param {obj} PostCSS CSS object
* @param {object} Parent rule for which children should be included
* @param {bool} Whether or not to keep the critical rule in the stylesheet
*/
function getChildRules(css, parent, preserve) {
var ruleList = [];
var selectorRegExp = new RegExp(parent.selector);
// Walk all rules to mach child selectors
css.walkRules(selectorRegExp, function (rule) {
var childRule = matchChild(parent, rule);
if (childRule) {
ruleList.push(rule);
}
});
// Walk all at-rules to match nested child selectors
css.walkAtRules(function (atRule) {
atRule.walkRules(selectorRegExp, function (rule) {
var childRule = matchChild(parent, rule);
// Create new at-rule to append only necessary selector to critical
var criticalAtRule = postcss.atRule({
name: atRule.name,
params: atRule.params
});
// Should append even if parent selector
if (rule.selector === parent.selector || childRule) {
criticalAtRule.append(rule);
ruleList.push(criticalAtRule);
if (!preserve) {
rule.remove();
}
}
});
});
return ruleList;
}
/**
* Get rules for selectors nested within parent node
*
* @param {obj} PostCSS CSS object
* @return {object} Parent rule for which children should be included
*/
function matchChild(parent, rule) {
var childRegExp = new RegExp('(, )?(' + parent.selector + ' [^,\s]*),?.*');
var childMatch = rule.selector.match(childRegExp);
if (rule.selector !== parent.selector && childMatch !== null) {
return true;
}
return false;
}
/**
* Identify critical CSS destinations.
*
* @param {object} PostCSS rule.
* @return {array} string corresponding to output destination.
*/
function getDest(selector) {
var dest = 'critical.css';
selector.walkDecls('critical-filename', function (decl) {
dest = decl.value.replace(/['"]*/g, '');
decl.remove();
});
return dest;
}
/**
* Primary plugin function.
*
* @param {object} array of options.
* @return {function} function for PostCSS plugin.
*/
function buildCritical(options) {
var args = Object.assign({}, {
outputPath: process.cwd(),
preserve: true,
minify: true,
dryRun: false
}, options);
return function (css, result) {
var criticalOutput = getCriticalRules(css, args.preserve);
var _loop = function _loop(fileName) {
var criticalCSS = postcss.parse('');
var critical = '';
var rules = [];
var plugins = args.minify ? [cssnano()] : [];
rules = criticalOutput[fileName].reduce(function (init, rule) {
rule.walkDecls('critical', function (decl) {
decl.remove();
});
criticalCSS.append(rule);
if (rule.type === 'rule' && !args.preserve) {
rule.remove();
}
return criticalOutput[fileName];
}, {});
postcss(plugins).process(criticalCSS).then(function (result) {
if (!args.dryRun) {
fs.writeFileSync(path.join(args.outputPath, fileName), result);
} else {
console.log(chalk.green('\nCritical CSS result is:\n' + chalk.yellow(result.css)));
return '\nCritical CSS result is:\n' + result.css;
}
});
};
for (var fileName in criticalOutput) {
_loop(fileName);
}
};
}
module.exports = postcss.plugin('postcss-critical', buildCritical);
module.exports = require('./lib');
{
"name": "postcss-critical-css",
"version": "1.0.3",
"version": "2.0.0",
"description": "Generate critical CSS using PostCSS",

@@ -12,6 +12,8 @@ "main": "index.js",

"postcss-plugin",
"postcss plugin",
"postcss",
"critical-css",
"critical",
"css"
"css",
"critical css"
],

@@ -23,14 +25,34 @@ "author": "Zach Green",

"babel-cli": "^6.11.4",
"babel-eslint": "^6.1.2",
"babel-plugin-transform-flow-strip-types": "^6.8.0",
"babel-plugin-transform-object-rest-spread": "^6.20.2",
"babel-preset-es2015": "^6.13.2",
"chalk": "^1.1.3",
"cssnano": "^3.5.2",
"eslint": "^3.3.1",
"eslint-config-airbnb": "^10.0.1",
"eslint-config-standard": "^6.2.1",
"eslint-plugin-flowtype": "^2.7.1",
"eslint-plugin-import": "^1.13.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^2.0.1",
"flow-bin": "^0.30.0",
"postcss": "^5.0.5",
"postcss-cli": "^2.6.0",
"tape": "^4.6.0"
},
"scripts": {
"build": "babel src/index.js --out-file index.js",
"start": "babel src/index.js --out-file index.js --watch",
"build": "eslint src/** && npm run flow && babel src --out-dir lib",
"example": "./node_modules/.bin/babel-node example/example.js",
"flow": "flow; test $? -eq 0 -o $? -eq 2",
"eslint": "eslint src/**",
"start": "eslint src/** && npm run flow && babel src/index.js --out-file index.js --watch",
"pretest": "./node_modules/.bin/babel-node test/preTest.js",
"test": "npm run pretest && tape test"
},
"dependencies": {
"chalk": "^1.1.3",
"cssnano": "^3.7.4",
"postcss": "^5.1.2"
}
}
# PostCSS Critical CSS
This plugin allows you to define and output critical CSS using custom CSS properties.
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 or within the CSS.

@@ -9,8 +9,13 @@ ## Install

## Example
## Examples
A live example is available in this repo. See the `/example` directory, and use the command `npm run example` to test it out.
### Using the `@critical` atRule
```css
/* In foo.css */
@critical;
.foo {
critical-selector: this;
border: 3px solid gray;

@@ -31,9 +36,9 @@ display: flex;

Note that in the above example, the selector is rendered as it is written in the
module. This may not be desireable, so you can alternatively identify the
selector you'd like to use in your `critical.css`;
### Using the `@critical` atRule with a custom file path
```css
/* In foo.css */
@critical bar;
.foo {
critical-selector: .custom-selector;
border: 3px solid gray;

@@ -46,4 +51,4 @@ display: flex;

```css
/* In critical.css */
.custom-selector {
/* In bar.css */
.foo {
border: 3px solid gray;

@@ -55,7 +60,7 @@ display: flex;

If you'd like to ouptut the entire scope of a module, including children, you can!
### Using the `@critical` atRule with a subset of styles
```css
/* in foo.css */
/* In foo.css */
.foo {
critical-selector: scope;
border: 3px solid gray;

@@ -66,5 +71,7 @@ display: flex;

.foo a {
color: blue;
text-decoration: none;
@critical {
.bar {
border: 10px solid gold;
color: gold;
}
}

@@ -74,2 +81,22 @@ ```

```css
/* In bar.css */
.bar {
border: 10px solid gold;
color: gold;
}
```
### Using the custom property, `critical-selector`
```css
/* In foo.css */
.foo {
critical-selector: this;
border: 3px solid gray;
display: flex;
padding: 1em;
}
```
Will output:
```css
/* In critical.css */

@@ -81,12 +108,27 @@ .foo {

}
```
.foo a {
color: blue;
text-decoration: none;
### Using the custom property, `critical-selector`, with a custom selector.
```css
/* In foo.css */
.foo {
critical-selector: .bar;
border: 3px solid gray;
display: flex;
padding: 1em;
}
```
Will output:
```css
/* In critical.css */
.bar {
border: 3px solid gray;
display: flex;
padding: 1em;
}
```
And what if you need to output multiple critical CSS files
(for example, if you have two different templates that do not share styles)?
You can do that as well.
### Using the custom property, `critical-filename`
```css

@@ -112,22 +154,48 @@ /* in foo.css */

## Options
### Using the custom property, `critical-selector`, with value `scope`
**outputPath**
Path to which critical CSS should be output
Default: current working directory
This allows the user to output the entire scope of a module, including children.
**preserve**
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.
WARNING: this is a destructive option and may break styles relying on the cascade!
Default: true
```css
/* in foo.css */
.foo {
critical-selector: scope;
border: 3px solid gray;
display: flex;
padding: 1em;
}
**minify**
Minify output CSS
Default: true
.foo a {
color: blue;
text-decoration: none;
}
```
Will output:
```css
/* In critical.css */
.foo {
border: 3px solid gray;
display: flex;
padding: 1em;
}
.foo a {
color: blue;
text-decoration: none;
}
```
## Plugin options
The plugin takes a single object as its only parameter. The following properties are valid:
| Arg | Type | Description | Default |
| ------------ | --------- | ------------------------------------------- | ------------------------- |
| `outputPath` | `string` | Path to which critical CSS should be output | Current working directory |
| `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` |
| `minify` | `boolean` | Minify output CSS? | `true` |
## To Dos
- Tests
- More tests
- More robust warnings

@@ -1,161 +0,11 @@

'use strict';
// @flow
const chalk = require('chalk');
var postcss = require('postcss');
var cssnano = require('cssnano');
var fs = require('fs');
var path = require('path');
import chalk from 'chalk'
import postcss from 'postcss'
import cssnano from 'cssnano'
import fs from 'fs'
import path from 'path'
import { getCriticalRules } from './getCriticalRules'
/**
* Throw a warning if a critical selector is used more than once.
*
* @param {array} Array of critical CSS rules.
* @param {selector} Selector to check for.
* @return {string} Console warning.
*/
function selectorReuseWarning(rules, selector) {
return rules.reduce((init, rule) => {
if (rule.selector === selector) {
console.warn(`Warning: Selector ${selector} is used more than once.`);
}
return rules;
}, []);
}
/**
* Identify critical CSS selectors
*
* @param {obj} PostCSS CSS object.
* @return {object} Object containing critical rules, organized by output destination
*/
function getCriticalRules(css, preserve) {
let critical = {};
css.walkDecls('critical-selector', decl => {
let dest = getDest(decl.parent);
if ('undefined' === typeof critical[dest]) {
critical[dest] = [];
}
switch (decl.value) {
case 'scope':
let childRules = getChildRules(css, decl.parent, preserve);
selectorReuseWarning(critical[dest], decl.parent.selector);
// Make sure the parent selector contains declarations
if (decl.parent.nodes.length > 1) {
critical[dest].push(decl.parent);
}
// Push all child rules
if (childRules !== null && childRules.length) {
childRules.forEach(rule => {
critical[dest].push(rule);
});
}
break;
case 'this':
selectorReuseWarning(critical[dest], decl.parent.selector);
critical[dest].push(decl.parent);
break;
default:
const container = decl.parent;
container.selector = decl.value.replace(/['"]*/g, '');
selectorReuseWarning(critical[dest], container.selector);
critical[dest].push(container);
break;
}
decl.remove();
});
return critical;
}
/**
* Get rules for selectors nested within parent node
*
* @param {obj} PostCSS CSS object
* @param {object} Parent rule for which children should be included
* @param {bool} Whether or not to keep the critical rule in the stylesheet
*/
function getChildRules(css, parent, preserve) {
let ruleList = [];
let selectorRegExp = new RegExp(parent.selector);
// Walk all rules to mach child selectors
css.walkRules(selectorRegExp, rule => {
let childRule = matchChild(parent, rule);
if (childRule) {
ruleList.push(rule);
}
});
// Walk all at-rules to match nested child selectors
css.walkAtRules(atRule => {
atRule.walkRules(selectorRegExp, rule => {
let childRule = matchChild(parent, rule);
// Create new at-rule to append only necessary selector to critical
let criticalAtRule = postcss.atRule({
name: atRule.name,
params: atRule.params
});
// Should append even if parent selector
if (rule.selector === parent.selector || childRule) {
criticalAtRule.append(rule);
ruleList.push(criticalAtRule);
if (!preserve) {
rule.remove();
}
}
});
});
return ruleList;
}
/**
* Get rules for selectors nested within parent node
*
* @param {obj} PostCSS CSS object
* @return {object} Parent rule for which children should be included
*/
function matchChild(parent, rule) {
let childRegExp = new RegExp('(, )?(' + parent.selector + ' [^,\s]*),?.*');
let childMatch = rule.selector.match(childRegExp);
if (rule.selector !== parent.selector && childMatch !== null) {
return true;
}
return false;
}
/**
* Identify critical CSS destinations.
*
* @param {object} PostCSS rule.
* @return {array} string corresponding to output destination.
*/
function getDest(selector) {
let dest = 'critical.css';
selector.walkDecls('critical-filename', decl => {
dest = decl.value.replace(/['"]*/g, '');
decl.remove();
});
return dest;
}
/**
* Primary plugin function.

@@ -166,52 +16,43 @@ *

*/
function buildCritical(options) {
const args = Object.assign({},
{
type ArgsType = {
outputPath: string,
preserve: boolean,
minify: boolean,
dryRun: boolean
}
function buildCritical (options: ArgsType): Function {
const args = {
outputPath: process.cwd(),
preserve: true,
minify: true,
dryRun: false
}, options);
return (css, result) => {
let criticalOutput = getCriticalRules(css, args.preserve);
for (let fileName in criticalOutput) {
let criticalCSS = postcss.parse('');
let critical = '';
let rules = [];
let plugins = args.minify ? [cssnano()] : [];
rules = criticalOutput[fileName].reduce((init, rule) => {
rule.walkDecls('critical', (decl) => {
decl.remove();
});
criticalCSS.append(rule);
if (rule.type === 'rule' && !args.preserve) {
rule.remove();
}
return criticalOutput[fileName];
}, {});
postcss(plugins)
dryRun: false,
...options
}
return (css: Object): Object => {
let criticalOutput = getCriticalRules(css, args.preserve)
return Object.keys(criticalOutput).reduce((init: Object, cur: string): Object => {
const criticalCSS = postcss.root()
criticalCSS.append(criticalOutput[cur])
postcss(args.minify ? [cssnano()] : [])
.process(criticalCSS)
.then(result => {
.then((result: Object) => {
if (!args.dryRun) {
fs.writeFileSync(path.join(args.outputPath, fileName), result);
fs.writeFile(
path.join(args.outputPath, cur),
result.css
)
} else {
console.log(
console.log( // eslint-disable-line no-console
chalk.green(`
Critical CSS result is:
${chalk.yellow(result.css)}`)
);
return `
Critical CSS result is:
${result.css}`;
)
}
});
}
})
return criticalOutput
}, {})
}
}
module.exports = postcss.plugin('postcss-critical', buildCritical);
module.exports = postcss.plugin('postcss-critical', buildCritical)

@@ -10,46 +10,62 @@ #!/usr/bin/env node

function fixturePath(name) {
return 'test/fixtures/' + name + '.css';
function compareCritical(t, name, testNonCritical) {
t.equal(
fs.readFileSync(
`${basePath}/${name}.${testNonCritical ? 'non-critical.actual' : 'critical.actual'}.css`, 'utf8'
).trim(),
fs.readFileSync(
`${basePath}/${name}.${testNonCritical ? 'non-critical.expected' : 'critical.expected'}.css`, 'utf8'
).trim(),
`processed fixture ${chalk.bold(name)} should be equal to expected output`
);
}
function fixture(name) {
return fs.readFileSync(fixturePath(name), 'utf8').trim();
}
test('Testing "this" critical result', function(t) {
compareCritical(t, 'this');
t.end();
});
function resolveFixture(name, options) {
return postcss(postcssCriticalCSS(options))
.process(fixture(name), {from: fixturePath(name)});
}
test('Testing "this" non-critical result', function(t) {
compareCritical(t, 'this', true);
t.end();
});
function compareFixtures(t, name, options) {
var postcssResult = resolveFixture(name, options);
var actual = postcssResult.css.trim();
test('Testing "atRule" critical result', function(t) {
compareCritical(t, 'atRule');
t.end();
});
fs.writeFile(fixturePath(name + '.actual'), actual);
test('Testing "atRule" non-critical result', function(t) {
compareCritical(t, 'atRule', true);
t.end();
});
var expected = fixture(name + '.expected');
t.equal(
actual, expected,
'processed fixture ' + chalk.bold(name) + ' should be equal to expected output'
);
test(chalk.yellow(`Testing ${chalk.bold('atRule.wrapping')} critical result`), function(t) {
compareCritical(t, 'atRule-wrapping');
t.end();
});
return postcssResult;
}
test(chalk.yellow(`Testing ${chalk.bold('atRule.wrapping')} non-critical result`), function(t) {
compareCritical(t, 'atRule-wrapping', true);
t.end();
});
function compareCritical(t, name) {
t.equal(
fs.readFileSync(`${basePath}/${name}.critical.actual.css`, 'utf8').trim(),
fs.readFileSync(`${basePath}/${name}.critical.expected.css`, 'utf8').trim(),
`processed fixture ${chalk.bold(name)} should be equal to expected output`
);
}
test('Testing "media" critical result', function(t) {
compareCritical(t, 'media');
t.end();
});
test('Testing "this" critical result', function(t) {
compareCritical(t, 'this');
test('Testing "media" non-critical result', function(t) {
compareCritical(t, 'media', true);
t.end();
});
test('Testing "this" non-critical result', function(t) {
compareFixtures(t, 'this');
test(chalk.yellow(`Testing ${chalk.bold('scope')} critical result`), function(t) {
compareCritical(t, 'scope');
t.end();
});
test(chalk.yellow(`Testing ${chalk.bold('scope')} non-critical result`), function(t) {
compareCritical(t, 'scope', true);
t.end();
});

@@ -8,9 +8,28 @@ #!/usr/bin/env node

const basePath = `${process.cwd()}/test/fixtures`;
const files = fs.readdirSync(basePath, 'utf8');
files.forEach(function(file) {
if (file.indexOf('.expected') === -1 && file.indexOf('.actual') === -1) {
postcss(postcssCriticalCSS({outputPath: basePath}))
.process(fs.readFileSync(`${basePath}/${file}`, 'utf8')
.trim());
function cb (files) {
function useFileData (data, file) {
postcss([postcssCriticalCSS({outputPath: basePath})])
.process(data)
.then(result => fs.writeFile(`${basePath}/${file.split('.')[0]}.non-critical.actual.css`, result.css))
}
});
files.forEach(function(file) {
if (file.indexOf('.actual') !== -1) {
fs.unlink(`${basePath}/${file}`)
}
if (file.indexOf('.expected') === -1 && file.indexOf('.actual') === -1) {
fs.readFile(`${basePath}/${file}`, 'utf8', (err, data) => {
if (err) {
throw new Error(err)
}
useFileData(data, file)
})
}
})
}
fs.readdir(basePath, 'utf8', (err, files) => {
if (err) {
throw new Error(err)
}
cb(files)
})

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc