Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
komachi committed Apr 7, 2016
0 parents commit 1bb4a2e
Show file tree
Hide file tree
Showing 11 changed files with 476 additions and 0 deletions.
39 changes: 39 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

# Created by https://www.gitignore.io/api/node

### Node ###
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history

7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
language: node_js
node_js:
- "node"
- "iojs"
- "4.3"
- "0.12"

5 changes: 5 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Copyright (c) 2016, Anton Nesterov

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# usedcss

>PostCSS plugin which helps you extract only used styles. Unlike [uncss](https://github.com/giakki/uncss) and others does not render your pages to find used classes, but instead parse it statically, which can be beneficial in some cases. Also support simple Angular's ng-class parsing. And also, due to awesomeness of PostCSS, it works with LESS and SCSS via PostCSS syntaxes.
## Installation

```
npm i usedcss --save
```

## Options

### html

Type: `array` of [globs](https://github.com/isaacs/node-glob)
*Required option*

You must specify html files to check css selector usage against them.

### ignore

Type: `array` of `strings`

Saves ignored selectors even if they are not presented in DOM.

## ignoreRegexp

Type: `array` of `regexps`

Use it to save selectors based on regexp.

## ngclass

Type: `boolean`

Default: `false`

Parse or not to parse `ng-class` statements.

## ignoreNesting

Type: `boolean`

Default: `false`

Ignore nesting so `.class1 .class2` will be saved even if there is element with `class2`, but it's not nested with `class1`. Useful if you use templates.

## Usage

Check out [PostCSS documentation](https://github.com/postcss/postcss#usage) on how to use PostCSS plugins.
144 changes: 144 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use strict';
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
const glob = Promise.promisify(require('multi-glob').glob);
const postcss = require('postcss');
const noop = require('node-noop').noop;
const cheerio = require('cheerio');
const expressions = require('angular-expressions');
const isRegex = require('is-regex');

module.exports = postcss.plugin('usedcss', (options) => {
var htmls = [];
return function(css) {
return new Promise((resolve, reject) => {
if (!options.html) {
reject('No html files specified.');
return;
}
if (options.ignore && !Array.isArray(options.ignore)) {
reject('ignore option should be an array.');
return;
}
if (options.ignoreRegexp && !Array.isArray(options.ignoreRegexp)) {
reject('ignoreRegexp option should be an array.');
return;
}
if (options.ignoreRegexp && !options.ignoreRegexp.every(isRegex)) {
reject('ignoreRegexp option should contain regular expressions.');
return;
}
if (options.ngclass && typeof options.ngclass != 'boolean') {
reject('ngclass option should be boolean.');
return;
}
if (options.ignoreNesting && typeof options.ignoreNesting != 'boolean') {
reject('ignoreNesting option should be boolean.');
return;
}
var promise;
if (options.ignoreNesting && options.ignore) {
promise = Promise.map(options.ignore, (item, i) => {
options.ignore[i] = item.replace(/^.*( |>|<)/g, '');
});
} else {
promise = Promise.resolve();
}
promise.then(() => {
return glob(options.html)
.then((files) => {
return Promise.map(files, (file) => {
return fs.readFileAsync(file).then((content) => {
htmls.push(cheerio.load(content.toString()));
return Promise.resolve();
});
});
})
.then(() => {
if (options.ngclass) {
return Promise.map(htmls, (html) => {
html('[ng-class], [data-ng-class]').each((i, el) => {
var cls = [];
var ngcl = html(el).attr('ng-class');
if (ngcl) {
cls = cls.concat(
Object.keys(expressions.compile(ngcl)())
);
}
var datang = html(el).attr('data-ng-class');
if (datang) {
cls = cls.concat(
Object.keys(expressions.compile(datang)())
);
}
cls.forEach((cl) => {
html(el).addClass(cl);
});
});
return Promise.resolve();
});
}
return Promise.resolve();
})
.then(() => {
var promises = [];
css.walkRules((rule) => {
// ignore keyframes
if (
rule.parent.type === 'atrule' &&
/keyframes/.test(rule.parent.name)
) {
return;
}

// if we found an element, we reject the promise and do nothing
// promise is resolved if we found nothing after iteration
// in this case, we remove a rule
// sounds hacky, but it works
promises.push(
Promise.map(rule.selectors, (selector) => {
var promise;
if (options.ignoreRegexp) {
promise = Promise.map(options.ignoreRegexp, (item) => {
if (item.test(selector)) {
return Promise.reject();
}
});
} else {
promise = Promise.resolve();
}
return promise.then(() => {
// remove pseudo-classes from selectors
selector = selector.replace(/::?[a-zA-Z-]*$/g, '');
if (options.ignoreNesting) {
selector = selector.replace(/^.*( |>|<)/g, '')
}
return Promise.map(htmls, (html) => {
if (
(html(selector).length > 0 ||
(
options.ignore &&
options.ignore.indexOf(selector) > -1)
)
) {
return Promise.reject();
}
return Promise.resolve();
});
});
})
.then(() => {
rule.remove();
return Promise.resolve();
})
.catch(noop)
);
});
return Promise.all(promises);
});
})
.then(resolve)
.catch(reject);
});
};
});
59 changes: 59 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "usedcss",
"version": "1.0.0",
"description": "Extract only styles presented in your html files.",
"main": "index.js",
"scripts": {
"test": "./node_modules/.bin/eslint *.js tests/*.js && ./node_modules/jscs/bin/jscs *.js tests/*.js && ./node_modules/mocha/bin/mocha tests/*.tests.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/komachi/usedcss.git"
},
"keywords": [
"css",
"postcss",
"less",
"scss",
"sass",
"postcss-plugin",
"optimization"
],
"author": "Anton Nesterov",
"license": "ISC",
"bugs": {
"url": "https://github.com/komachi/usedcss/issues"
},
"homepage": "https://nesterov.pw/usedcss",
"eslintConfig": {
"extends": "defaults",
"env": {
"node": true,
"es6": true,
"mocha": true
}
},
"jscsConfig": {
"preset": "node-style-guide",
"esnext": true,
"verbose": true,
"requireTrailingComma": null,
"requireCapitalizedComments": null
},
"dependencies": {
"angular-expressions": "^0.3.0",
"bluebird": "^3.3.4",
"cheerio": "^0.20.0",
"is-regex": "^1.0.3",
"multi-glob": "^1.0.1",
"node-noop": "^1.0.0",
"postcss": "^5.0.19"
},
"devDependencies": {
"eslint": "^2.7.0",
"eslint-config-defaults": "^9.0.0",
"expect": "^1.16.0",
"jscs": "^2.11.0",
"mocha": "^2.4.5"
}
}
48 changes: 48 additions & 0 deletions tests/errors.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const expect = require('expect');
const runTest = require('./runTest.helper.js');

describe('errors', () => {
it('Should retun error if there is no html files specified.', (done) => {
runTest({html: null}).catch((err) => {
expect(err).toBe('No html files specified.');
done();
});
});

it('Should retun error if ignore is not an array', (done) => {
runTest({ignore: 'test'}).catch((err) => {
expect(err).toBe('ignore option should be an array.');
done();
});
});

it('Should retun error if ignoreRegexp is not an array', (done) => {
runTest({ignoreRegexp: 'test'}).catch((err) => {
expect(err).toBe('ignoreRegexp option should be an array.');
done();
});
});

it('Should retun error if ignoreRegexp contains not a regexp', (done) => {
runTest({ignoreRegexp: ['test']}).catch((err) => {
expect(err).toBe(
'ignoreRegexp option should contain regular expressions.'
);
done();
});
});

it('Should retun error if ngclass is not boolean', (done) => {
runTest({ngclass: 'test'}).catch((err) => {
expect(err).toBe('ngclass option should be boolean.');
done();
});
});

it('Should retun error if ignoreNesting is not boolean', (done) => {
runTest({ignoreNesting: 'test'}).catch((err) => {
expect(err).toBe('ignoreNesting option should be boolean.');
done();
});
});
});
Loading

0 comments on commit 1bb4a2e

Please sign in to comment.