├── .editorconfig
├── .eslintrc
├── .github
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── config
└── index.js
├── functions
├── getContent.js
├── getContent.test.js
├── getFiles.js
├── getFiles.test.js
├── getLibFiles.js
├── getLibFiles.test.js
├── getWords.js
├── getWords.test.js
├── includesPart.js
└── includesPart.test.js
├── index.js
├── lists
├── html-tags.js
├── pseudo.js
└── pseudoWithArgs.js
├── package-lock.json
├── package.json
├── plugin.js
├── testData
├── getContent
│ ├── index.html
│ └── index.js
├── getFiles
│ ├── deeperFolder
│ │ └── index.js
│ ├── index.haml
│ ├── index.html
│ ├── index.js
│ └── index.ts
└── plugin
│ ├── attributeSelectors
│ └── links
│ │ └── index.html
│ ├── basic
│ ├── index.html
│ └── index.js
│ ├── bootstrap
│ └── index.html
│ ├── combinatorsSelectors
│ └── index.html
│ ├── dynamicSelectors
│ └── index.html
│ ├── fewLibs
│ ├── first
│ │ └── index.js
│ └── index.html
│ ├── fewPaths
│ ├── first
│ │ └── first.html
│ └── second
│ │ └── index.html
│ ├── groupingSelectors
│ ├── index.html
│ └── many
│ │ └── index.html
│ └── pseudoElements
│ └── index.html
└── tests
├── basic.test.js
├── bootstrap.test.js
├── dynamicSelectors.test.js
├── removeMediaQueries.test.js
├── selectors
├── attributeSelectors.test.js
├── combinatorsSelectors.test.js
├── groupingSelectors.test.js
└── pseudoClasses.test.js
├── utils
├── run.js
└── runWithConfigs.js
├── whiteList.test.js
├── withFewLibs.test.js
└── withFewPathes.test.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_style = space
3 | indent_size = 2
4 | tab_width = 2
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "commonjs": true,
4 | "es6": true,
5 | "node": true,
6 | "jest/globals": true
7 | },
8 | "extends": [
9 | "airbnb-base",
10 | "plugin:jsdoc/recommended",
11 | "prettier"
12 | ],
13 | "globals": {
14 | "Atomics": "readonly",
15 | "SharedArrayBuffer": "readonly"
16 | },
17 | "parserOptions": {
18 | "ecmaVersion": 2018
19 | },
20 | "plugins": [
21 | "jest",
22 | "jsdoc"
23 | ],
24 | "rules": {
25 | "jest/no-disabled-tests": "warn",
26 | "jest/no-focused-tests": "error",
27 | "jest/no-identical-title": "error",
28 | "jest/prefer-to-have-length": "warn",
29 | "jest/valid-expect": "error"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | strategy:
11 | matrix:
12 | node-version: [8.x, 10.x, 12.x]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - run: npm install
21 | - run: npm run build --if-present
22 | - run: npm run lint
23 | - run: npm test
24 | env:
25 | CI: true
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | .idea
107 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github
2 | .idea
3 | tests
4 | coverage
5 | /**/*.test.*
6 | testData
7 | .travis.yml
8 | .editorconfig
9 | .eslintrc
10 | .gitignore
11 | LICENSE
12 | README.md
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | dist: bionic
3 |
4 | node_js:
5 | - 10
6 |
7 | install:
8 | - npm ci
9 |
10 | cache: npm
11 |
12 | script:
13 | - npm audit
14 | - npm run lint
15 | - npm run test
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Bohdan Onatsky
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 | 
2 |
3 | # This plugin will be kill your unused css
4 |
5 | ## Why do you need this?
6 |
7 | Your css is increasing after each iteration? Coverage of pages smaller 5%? This plugin try to clear all trash in your styles.
8 |
9 | ### Example
10 |
11 | Your html(haml, jsx, js, etc) have only classes `.container`, `.row`, `col-12`:
12 | ```html
13 |
14 |
15 |
16 |
Hello!
17 |
18 |
19 |
20 | ```
21 | But in styles you import all bootstrap grid
22 | ```scss
23 | @import '~bootstrap/scss/grid';
24 | ```
25 | In result you have 99% useless css code.
26 | If you add this plugin to your `postcss` config in result you have only styles, that you use:
27 |
28 | ```css
29 | .container {
30 | /* ...code... */
31 | }
32 | .row {
33 | /* ...code... */
34 | }
35 | .col-12 {
36 | /* ...code... */
37 | }
38 | @media screen and (max-width: 300px) {
39 | .container {
40 | /* ...code... */
41 | }
42 | .row {
43 | /* ...code... */
44 | }
45 | .col-12 {
46 | /* ...code... */
47 | }
48 | }
49 | /* etc */
50 | ```
51 |
52 |
53 | ## 1. Installation
54 |
55 | ```sh
56 | npm i -D postcss-trash-killer
57 | ```
58 | ```sh
59 | yarn add --save postcss-trash-killer
60 | ```
61 |
62 | ## 2. Configuration
63 |
64 | After **install** add this plugin to your plugin 'pipeline'
65 | ```js
66 | // postcss.config.js
67 | const autoprefixer = require('autoprefixer');
68 | const cssTrashKiller = require('postcss-trash-killer');
69 |
70 | const configForCleaner = {
71 | tagSelectors: false, // default true, include all simple tag selectors(html, body, *, h1, but not `.className h1`
72 | fileExtensions: ['.haml', '.js'], // File types for scanning selectors
73 | paths: ['app/view/', 'some/second/path'], // Paths with your view files
74 | libs: ['slick-carousel'], // Include lib, who work with view and installed via npm(yarn) and located in node_modules in root dir
75 | libsExtension: ['.js'], // File types for libraries
76 | whitelist: ['dontTouchSelector'], // not removable selectors
77 | dynamicSelectors: ['theme-color'] // If you use dynamic selectors, insert here part of selector. Not removed all selectors if contains 'theme-color' part
78 | };
79 |
80 | module.exports = {
81 | plugins: [
82 | cssTrashKiller(configForCleaner),
83 | autoprefixer
84 | ]
85 | }
86 | ```
87 |
88 | ## Note!
89 | This plugin in addition remove empty media queries!
90 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {{regex: RegExp, regexForBrackets: RegExp}}
3 | */
4 | const config = {
5 | regex: /[^\da-zA-Z\-_]/g,
6 | regexForAttributeSelector: /(.*)\[+(.)+[*$~]+=+(.)+]+(.*)/
7 | };
8 |
9 | module.exports = config;
10 |
--------------------------------------------------------------------------------
/functions/getContent.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | /**
4 | * @param {string[]} files - array of files
5 | * @returns {string} return all text from files array
6 | */
7 | const getContent = files =>
8 | files.map(file => fs.readFileSync(file, 'utf8')).join();
9 |
10 | module.exports = getContent;
11 |
--------------------------------------------------------------------------------
/functions/getContent.test.js:
--------------------------------------------------------------------------------
1 | const getContent = require('./getContent');
2 |
3 | describe('Check getContent function', () => {
4 | test('Return all content', () => {
5 | expect(
6 | getContent([
7 | 'testData/getContent/index.js',
8 | 'testData/getContent/index.html'
9 | ])
10 | ).toEqual(`function sum(a, b) {
11 | return a + b;
12 | }
13 |
14 | sum(4, 5);
15 | ,
16 |
17 |
18 | Yo, man:)
19 |
20 |
21 |
22 |
23 |
24 | `);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/functions/getFiles.js:
--------------------------------------------------------------------------------
1 | const flatMap = require('array.prototype.flatmap');
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 |
6 | /**
7 | * @param {string} pathName - path to files for parse
8 | * @param {string[]} exts - array of extension files. Example: ['.js', '.ts']
9 | * @returns {string[]} - array files with extensions
10 | */
11 | const getFiles = (pathName, exts) => {
12 | // if (fs.existsSync(pathName)) {
13 | const files = fs.readdirSync(pathName);
14 |
15 | return flatMap(files, file => {
16 | const filename = path.join(pathName, file);
17 | if (
18 | fs.lstatSync(filename).isDirectory() &&
19 | !filename.includes('node_modules')
20 | ) {
21 | return getFiles(filename, exts);
22 | }
23 | if (exts.includes(path.extname(filename))) {
24 | return [filename];
25 | }
26 | return [];
27 | });
28 | // }
29 |
30 | // throw new Error('One or more your path does not exist');
31 | };
32 |
33 | module.exports = getFiles;
34 |
--------------------------------------------------------------------------------
/functions/getFiles.test.js:
--------------------------------------------------------------------------------
1 | const getFiles = require('./getFiles');
2 |
3 | describe('Testing getFiles function', () => {
4 | let mustBe = [
5 | 'testData/getFiles/index.html',
6 | 'testData/getFiles/index.ts',
7 | 'testData/getFiles/index.haml',
8 | 'testData/getFiles/index.js',
9 | 'testData/getFiles/deeperFolder/index.js'
10 | ].sort();
11 |
12 | beforeAll(() => {
13 | if (process.platform === 'win32') {
14 | mustBe = [
15 | 'testData\\getFiles\\index.html',
16 | 'testData\\getFiles\\index.ts',
17 | 'testData\\getFiles\\index.haml',
18 | 'testData\\getFiles\\index.js',
19 | 'testData\\getFiles\\deeperFolder\\index.js'
20 | ].sort();
21 | }
22 | });
23 |
24 | test('Get all files in entered filetypes', () => {
25 | const result = getFiles('./testData/getFiles/', [
26 | '.js',
27 | '.ts',
28 | '.haml',
29 | '.html'
30 | ]);
31 |
32 | expect(result.sort()).toEqual(mustBe);
33 | });
34 |
35 | test('Get all js files', () => {
36 | const result = getFiles('./testData/getFiles/', ['.js']);
37 | const jsFiles = [mustBe[0], mustBe[3]];
38 |
39 | expect(result.sort()).toEqual(jsFiles);
40 | });
41 |
42 | test('Function return empty array, if target node_modules', () => {
43 | const result = getFiles('./node_modules/', [
44 | '.js',
45 | '.ts',
46 | '.haml',
47 | '.html'
48 | ]);
49 |
50 | expect(result.sort()).toEqual([]);
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/functions/getLibFiles.js:
--------------------------------------------------------------------------------
1 | const flatMap = require('array.prototype.flatmap');
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 |
6 | /**
7 | * @param {string} lib path to lib exclude node_modules
8 | * @param {string[]} exts file extension for files
9 | * @returns {string[]} all files that match
10 | */
11 | const wrapper = (lib, exts) => {
12 | /**
13 | * @param {string} lib path to lib exclude node_modules
14 | * @param {string[]} exts file extension for files
15 | * @returns {string[]} all files that match
16 | */
17 | // eslint-disable-next-line no-shadow
18 | function getLibFiles(lib, exts) {
19 | const files = fs.readdirSync(lib);
20 |
21 | return flatMap(files, file => {
22 | const filename = path.join(`${lib}`, file);
23 | if (
24 | fs.lstatSync(filename).isDirectory() &&
25 | filename.match(/node_modules/g).length <= 1
26 | ) {
27 | return getLibFiles(filename, exts);
28 | }
29 | if (exts.includes(path.extname(filename))) {
30 | return [filename];
31 | }
32 | return [];
33 | });
34 | }
35 |
36 | return getLibFiles(`node_modules/${lib}`, exts);
37 | };
38 |
39 | module.exports = wrapper;
40 |
--------------------------------------------------------------------------------
/functions/getLibFiles.test.js:
--------------------------------------------------------------------------------
1 | const getLibFiles = require('./getLibFiles');
2 |
3 | describe('Check getLibFiles', () => {
4 | let slickArray = [
5 | 'node_modules/slick-carousel/slick/slick.js',
6 | 'node_modules/slick-carousel/slick/slick.min.js'
7 | ].sort();
8 |
9 | if (process.platform === 'win32') {
10 | slickArray = [
11 | 'node_modules\\slick-carousel\\slick\\slick.js',
12 | 'node_modules\\slick-carousel\\slick\\slick.min.js'
13 | ].sort();
14 | }
15 |
16 | test('Must return array of list files inside package', () => {
17 | expect(getLibFiles('slick-carousel', ['.js']).sort()).toEqual(slickArray);
18 | });
19 |
20 | test('Must return empty array if we get deeper 1 node_modules', () => {
21 | expect(getLibFiles('base/node_modules/', ['.js']).sort()).toEqual([]);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/functions/getWords.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {string} content text content
3 | * @param {RegExp} regex regex for split on selectors
4 | * @returns {string[]} array selectors
5 | */
6 | const getWords = (content, regex) =>
7 | content
8 | .toLowerCase()
9 | .split(regex)
10 | .filter(Boolean);
11 |
12 | module.exports = getWords;
13 |
--------------------------------------------------------------------------------
/functions/getWords.test.js:
--------------------------------------------------------------------------------
1 | const getWords = require('./getWords');
2 | const { regex } = require('../config/index');
3 |
4 | describe('Function must be return array of words in string', () => {
5 | const testData = [
6 | ['string lol work .class many', ['string', 'lol', 'work', 'class', 'many']],
7 | [
8 | 'selector__bem selecing-item__what',
9 | ['selector__bem', 'selecing-item__what']
10 | ],
11 | ['col-md-1 offset-lg-3', ['col-md-1', 'offset-lg-3']]
12 | ];
13 |
14 | test.each(testData)('String %i must return %i', (expected, mustBe) => {
15 | expect(getWords(expected, regex)).toEqual(mustBe);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/functions/includesPart.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {string[]} arr parts selectors array
3 | * @param {string} word word, where we search any part of strings
4 | * @returns {boolean} return boolean, if word contains in array
5 | */
6 | const includesPart = (arr, word) => {
7 | return arr.some(item => word.indexOf(item) !== -1);
8 | };
9 |
10 | module.exports = includesPart;
11 |
--------------------------------------------------------------------------------
/functions/includesPart.test.js:
--------------------------------------------------------------------------------
1 | const includesPart = require('./includesPart');
2 |
3 | describe('Check includesPart in array', () => {
4 | test('Return true if exist part word in array', () => {
5 | const arr = ['color--', 'bg--', 'header__description-bg-image'];
6 |
7 | expect(includesPart(arr, 'color--red')).toStrictEqual(true);
8 | expect(includesPart(arr, 'web-dev-')).toStrictEqual(false);
9 | expect(includesPart(arr, 'not')).toStrictEqual(false);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const postcss = require('postcss');
2 |
3 | const plugin = require('./plugin');
4 |
5 | module.exports = postcss.plugin('postcss-trash-killer', plugin);
6 |
--------------------------------------------------------------------------------
/lists/html-tags.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | 'a',
3 | 'abbr',
4 | 'acronym',
5 | 'address',
6 | 'applet',
7 | 'area',
8 | 'article',
9 | 'aside',
10 | 'audio',
11 | 'b',
12 | 'base',
13 | 'basefont',
14 | 'bdi',
15 | 'bdo',
16 | 'bgsound',
17 | 'big',
18 | 'blink',
19 | 'blockquote',
20 | 'body',
21 | 'br',
22 | 'button',
23 | 'canvas',
24 | 'caption',
25 | 'center',
26 | 'cite',
27 | 'code',
28 | 'col',
29 | 'colgroup',
30 | 'content',
31 | 'data',
32 | 'datalist',
33 | 'dd',
34 | 'decorator',
35 | 'del',
36 | 'details',
37 | 'dfn',
38 | 'dir',
39 | 'div',
40 | 'dl',
41 | 'dt',
42 | 'element',
43 | 'em',
44 | 'embed',
45 | 'fieldset',
46 | 'figcaption',
47 | 'figure',
48 | 'font',
49 | 'footer',
50 | 'form',
51 | 'frame',
52 | 'frameset',
53 | 'h1',
54 | 'h2',
55 | 'h3',
56 | 'h4',
57 | 'h5',
58 | 'h6',
59 | 'head',
60 | 'header',
61 | 'hgroup',
62 | 'hr',
63 | 'html',
64 | 'i',
65 | 'iframe',
66 | 'img',
67 | 'input',
68 | 'ins',
69 | 'isindex',
70 | 'kbd',
71 | 'keygen',
72 | 'label',
73 | 'legend',
74 | 'li',
75 | 'link',
76 | 'listing',
77 | 'main',
78 | 'map',
79 | 'mark',
80 | 'marquee',
81 | 'menu',
82 | 'menuitem',
83 | 'meta',
84 | 'meter',
85 | 'nav',
86 | 'nobr',
87 | 'noframes',
88 | 'noscript',
89 | 'object',
90 | 'ol',
91 | 'optgroup',
92 | 'option',
93 | 'output',
94 | 'p',
95 | 'param',
96 | 'plaintext',
97 | 'pre',
98 | 'progress',
99 | 'q',
100 | 'rp',
101 | 'rt',
102 | 'ruby',
103 | 's',
104 | 'samp',
105 | 'script',
106 | 'section',
107 | 'select',
108 | 'shadow',
109 | 'small',
110 | 'source',
111 | 'spacer',
112 | 'span',
113 | 'strike',
114 | 'strong',
115 | 'style',
116 | 'sub',
117 | 'summary',
118 | 'sup',
119 | 'table',
120 | 'tbody',
121 | 'td',
122 | 'template',
123 | 'textarea',
124 | 'tfoot',
125 | 'th',
126 | 'thead',
127 | 'time',
128 | 'title',
129 | 'tr',
130 | 'track',
131 | 'tt',
132 | 'u',
133 | 'ul',
134 | 'var',
135 | 'video',
136 | 'wbr',
137 | 'xmp',
138 | 'odd',
139 | 'even',
140 | 'n'
141 | ];
142 |
--------------------------------------------------------------------------------
/lists/pseudo.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | 'after',
3 | 'first-line',
4 | 'backdrop',
5 | 'before',
6 | 'cue',
7 | 'cue-region',
8 | 'first-letter',
9 | 'first-line',
10 | 'grammar-error',
11 | 'marker',
12 | 'part',
13 | 'placeholder',
14 | 'selection',
15 | 'slotted',
16 | 'spelling-error',
17 | 'active',
18 | 'checked',
19 | 'default',
20 | 'defined',
21 | 'disabled',
22 | 'empty',
23 | 'enabled',
24 | 'first',
25 | 'first-child',
26 | 'first-of-type',
27 | 'fullscreen',
28 | 'focus',
29 | 'focus-within',
30 | 'hover',
31 | 'indeterminate',
32 | 'in-range',
33 | 'invalid',
34 | 'last-child',
35 | 'last-of-type',
36 | 'left',
37 | 'link',
38 | 'only-child',
39 | 'only-of-type',
40 | 'optional',
41 | 'out-of-range',
42 | 'placeholder-shown',
43 | 'read-only',
44 | 'read-write',
45 | 'required',
46 | 'right',
47 | 'root',
48 | 'scope',
49 | 'target',
50 | 'valid',
51 | 'visited',
52 | 'where',
53 | 'is',
54 | 'matches',
55 | 'any',
56 | 'host-context',
57 | 'has',
58 | 'focus-visible',
59 | 'dir',
60 | 'blank',
61 | 'any-link',
62 | '-moz-progress-bar',
63 | '-moz-range-progress',
64 | '-moz-range-thumb',
65 | '-moz-range-track',
66 | '-ms-browse',
67 | '-ms-check',
68 | '-ms-clear',
69 | '-ms-expand',
70 | '-ms-fill',
71 | '-ms-fill-lower',
72 | '-ms-fill-upper',
73 | '-ms-reveal',
74 | '-ms-thumb',
75 | '-ms-ticks-after',
76 | '-ms-ticks-before',
77 | '-ms-tooltip',
78 | '-ms-track',
79 | '-ms-value',
80 | '-webkit-progress-bar',
81 | '-webkit-progress-value',
82 | '-webkit-slider-runnable-track',
83 | '-webkit-slider-thumb'
84 | ];
85 |
--------------------------------------------------------------------------------
/lists/pseudoWithArgs.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | 'host',
3 | 'lang',
4 | 'not',
5 | 'nth-child',
6 | 'nth-last-child',
7 | 'nth-last-of-type',
8 | 'nth-of-type'
9 | ];
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "postcss-trash-killer",
3 | "version": "1.0.5",
4 | "description": "PostCSS plugin for removing useless css",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest --coverage",
8 | "lint": "npm run lint:js",
9 | "lint:js": "eslint --fix \"**/*.js\""
10 | },
11 | "engines": {
12 | "node": ">=8.0.0"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/Rammfall/postcss-trash-killer.git"
17 | },
18 | "keywords": [
19 | "postcss",
20 | "plugin",
21 | "css",
22 | "optimization"
23 | ],
24 | "author": "Bohdan Onatskyi",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/Rammfall/postcss-trash-killer/issues"
28 | },
29 | "homepage": "https://github.com/Rammfall/postcss-trash-killer#readme",
30 | "devDependencies": {
31 | "bootstrap": "4.4.1",
32 | "eslint": "^6.8.0",
33 | "eslint-config-airbnb-base": "^14.0.0",
34 | "eslint-config-prettier": "^6.10.0",
35 | "eslint-plugin-import": "^2.20.1",
36 | "eslint-plugin-jest": "^23.7.0",
37 | "eslint-plugin-jsdoc": "^22.1.0",
38 | "husky": "^4.2.3",
39 | "jest": "^25.1.0",
40 | "lint-staged": "^10.0.7",
41 | "postcss": "^7.0.27",
42 | "prettier": "^1.19.1",
43 | "slick-carousel": "1.8.1"
44 | },
45 | "dependencies": {
46 | "array.prototype.flatmap": "^1.2.3"
47 | },
48 | "peerDependencies": {
49 | "postcss": ">=7.0.0"
50 | },
51 | "prettier": {
52 | "singleQuote": true
53 | },
54 | "lint-staged": {
55 | ".js": [
56 | "prettier --write",
57 | "eslint --fix"
58 | ]
59 | },
60 | "husky": {
61 | "hooks": {
62 | "pre-commit": "lint-staged"
63 | }
64 | },
65 | "jest": {
66 | "coverageThreshold": {
67 | "global": {
68 | "statements": 100
69 | }
70 | }
71 | },
72 | "eslintIgnore": [
73 | "/testData"
74 | ]
75 | }
76 |
--------------------------------------------------------------------------------
/plugin.js:
--------------------------------------------------------------------------------
1 | const getFiles = require('./functions/getFiles');
2 | const getContent = require('./functions/getContent');
3 | const getWords = require('./functions/getWords');
4 | const getLibFiles = require('./functions/getLibFiles');
5 | const includesPart = require('./functions/includesPart');
6 |
7 | const { regex, regexForAttributeSelector } = require('./config/index');
8 | const htmlTags = require('./lists/html-tags');
9 | const pseudo = require('./lists/pseudo');
10 | const pseudoWithArgs = require('./lists/pseudoWithArgs');
11 |
12 | const DEFAULT_OPTIONS = {
13 | paths: [],
14 | fileExtensions: [],
15 | whitelist: [],
16 | tagSelectors: true,
17 | libs: [],
18 | libsExtensions: [],
19 | dynamicSelectors: []
20 | };
21 |
22 | /**
23 | * @param {object} options Object params
24 | * @param {string[]} options.paths Paths to files whats needs check on selectors
25 | * @param {string[]} options.fileExtensions File extensions for paths
26 | * @param {string[]} options.whitelist White list selectors which not need to remove
27 | * @param {boolean} options.tagSelectors Support all tag selector
28 | * @param {string[]} options.libs Paths to libs (exclude 'node_modules' path)
29 | * @param {string[]} options.libsExtension Extensions to libs
30 | * @param {string[]} options.dynamicSelectors Extensions to libs
31 | * @returns {function(...[*]=)} returns postcss plugin
32 | */
33 | const plugin = options => css => {
34 | const {
35 | paths,
36 | fileExtensions,
37 | whitelist,
38 | tagSelectors,
39 | libs,
40 | libsExtensions,
41 | dynamicSelectors
42 | } = Object.assign(DEFAULT_OPTIONS, options);
43 | let allSelectors = [].concat(whitelist);
44 |
45 | // add to selectors pseudoclasses and pseudoelements
46 | allSelectors = allSelectors.concat(pseudo);
47 | // if turn on html tags - add them to list
48 | if (tagSelectors === true) {
49 | allSelectors = allSelectors.concat(htmlTags);
50 | }
51 | let dynamic = dynamicSelectors.map(item => item.toLowerCase());
52 |
53 | // all content from project files
54 | let content = paths.reduce(
55 | (prev, current) => {
56 | const result = new Set(
57 | getWords(getContent(getFiles(current, fileExtensions)), regex).concat(
58 | prev
59 | )
60 | );
61 |
62 | return [...result];
63 | },
64 | allSelectors.map(item => item.toLowerCase())
65 | );
66 |
67 | // add content from project libs
68 | content = libs.reduce((prev, current) => {
69 | const result = new Set(
70 | getWords(getContent(getLibFiles(current, libsExtensions)), regex).concat(
71 | prev
72 | )
73 | );
74 |
75 | return [...result];
76 | }, content);
77 |
78 | css.walkRules(rule => {
79 | // eslint-disable-next-line no-param-reassign
80 | rule.selectors = rule.selectors.filter(selector => {
81 | let res;
82 | if (!regexForAttributeSelector.test(selector)) {
83 | if (includesPart(dynamic, selector)) {
84 | return true;
85 | }
86 | // TODO: refactor this part
87 | res = getWords(selector, regex).every((word, index, array) => {
88 | if (index > 0 && includesPart(pseudoWithArgs, selector)) {
89 | return array.slice(1).some(item => pseudoWithArgs.includes(item));
90 | }
91 | return content.includes(word);
92 | });
93 | } else {
94 | res = true;
95 | }
96 |
97 | return res;
98 | });
99 |
100 | // remove empty rules
101 | if (rule.selectors.length === 1 && rule.selectors[0] === '') {
102 | rule.remove();
103 | }
104 | });
105 |
106 | // if media rule have 0 rules - remove it
107 | css.walkAtRules('media', atRule => {
108 | if (atRule.nodes.length === 0) {
109 | atRule.remove();
110 | }
111 | });
112 | };
113 |
114 | module.exports = plugin;
115 |
--------------------------------------------------------------------------------
/testData/getContent/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Yo, man:)
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/testData/getContent/index.js:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | sum(4, 5);
6 |
--------------------------------------------------------------------------------
/testData/getFiles/deeperFolder/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rammfall/postcss-trash-killer/92ac2328a54e5138ee65c36572743b63aa6d1d8e/testData/getFiles/deeperFolder/index.js
--------------------------------------------------------------------------------
/testData/getFiles/index.haml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rammfall/postcss-trash-killer/92ac2328a54e5138ee65c36572743b63aa6d1d8e/testData/getFiles/index.haml
--------------------------------------------------------------------------------
/testData/getFiles/index.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rammfall/postcss-trash-killer/92ac2328a54e5138ee65c36572743b63aa6d1d8e/testData/getFiles/index.html
--------------------------------------------------------------------------------
/testData/getFiles/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rammfall/postcss-trash-killer/92ac2328a54e5138ee65c36572743b63aa6d1d8e/testData/getFiles/index.js
--------------------------------------------------------------------------------
/testData/getFiles/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rammfall/postcss-trash-killer/92ac2328a54e5138ee65c36572743b63aa6d1d8e/testData/getFiles/index.ts
--------------------------------------------------------------------------------
/testData/plugin/attributeSelectors/links/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/testData/plugin/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
12 | Test link
13 |
14 | name
15 |
16 |
17 |
--------------------------------------------------------------------------------
/testData/plugin/basic/index.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const link = document.querySelector('.link');
3 |
4 | link.addEventListener('click', () => {
5 | link.classList.toggle('lint__red');
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/testData/plugin/bootstrap/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/testData/plugin/combinatorsSelectors/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/testData/plugin/dynamicSelectors/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
12 |
13 | Hello
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/testData/plugin/fewLibs/first/index.js:
--------------------------------------------------------------------------------
1 | $('.slick-selector').slick();
2 |
--------------------------------------------------------------------------------
/testData/plugin/fewLibs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/testData/plugin/fewPaths/first/first.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/testData/plugin/fewPaths/second/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/testData/plugin/groupingSelectors/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/testData/plugin/groupingSelectors/many/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/testData/plugin/pseudoElements/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/basic.test.js:
--------------------------------------------------------------------------------
1 | const run = require('./utils/run');
2 |
3 | describe('Basic usage', () => {
4 | test('Must remove useless selector with rule on selector with mediaquery', () => {
5 | run(
6 | '.test {color: red;} .notest {color: black;} @media screen and (max-width: 320px) { .notest {color: black;} }',
7 | '.test {color: red;}'
8 | );
9 | });
10 |
11 | test('Must remove useless selector with rule', () => {
12 | run(
13 | '.test {color: red;} .notest.test {color: black;}',
14 | '.test {color: red;}'
15 | );
16 |
17 | run(
18 | 'div.test {color: red;} .notest.test {color: black;}',
19 | 'div.test {color: red;}'
20 | );
21 |
22 | run(
23 | '.link { color: red; } a.lint__red { height: 100vh; }',
24 | '.link { color: red; } a.lint__red { height: 100vh; }'
25 | );
26 | });
27 | });
28 |
29 | describe('Check plugin with html tagSelectors false', () => {
30 | test('Must dont remove rules with html tags', () => {
31 | run(
32 | 'body { color: red; } html { height: 100vh; }',
33 | 'body { color: red; } html { height: 100vh; }',
34 | 'basic',
35 | false
36 | );
37 | });
38 |
39 | test('Must remove with false argument', () => {
40 | run('h3 { color: red; } h4 { height: 100vh; }', '', 'basic', false);
41 | });
42 |
43 | test('Must not remove difficult selectors with usage', () => {
44 | run('h3.test { color: red; } h4 { height: 100vh; }', '', 'basic', false);
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/tests/bootstrap.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const run = require('./utils/run');
4 |
5 | const bootstrap = fs.readFileSync(
6 | './node_modules/bootstrap/dist/css/bootstrap-grid.css',
7 | 'utf8'
8 | );
9 |
10 | describe('Test with using bootstrap', () => {
11 | test('Using bootstrap grid, in css will be only usable classes', () => {
12 | run(
13 | bootstrap,
14 | `/*!
15 | * Bootstrap Grid v4.4.1 (https://getbootstrap.com/)
16 | * Copyright 2011-2019 The Bootstrap Authors
17 | * Copyright 2011-2019 Twitter, Inc.
18 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
19 | */
20 | html {
21 | box-sizing: border-box;
22 | -ms-overflow-style: scrollbar;
23 | }
24 |
25 | *,
26 | *::before,
27 | *::after {
28 | box-sizing: inherit;
29 | }
30 |
31 | .container {
32 | width: 100%;
33 | padding-right: 15px;
34 | padding-left: 15px;
35 | margin-right: auto;
36 | margin-left: auto;
37 | }
38 |
39 | @media (min-width: 576px) {
40 | .container {
41 | max-width: 540px;
42 | }
43 | }
44 |
45 | @media (min-width: 768px) {
46 | .container {
47 | max-width: 720px;
48 | }
49 | }
50 |
51 | @media (min-width: 992px) {
52 | .container {
53 | max-width: 960px;
54 | }
55 | }
56 |
57 | @media (min-width: 1200px) {
58 | .container {
59 | max-width: 1140px;
60 | }
61 | }
62 |
63 | @media (min-width: 576px) {
64 | .container {
65 | max-width: 540px;
66 | }
67 | }
68 |
69 | @media (min-width: 768px) {
70 | .container {
71 | max-width: 720px;
72 | }
73 | }
74 |
75 | @media (min-width: 992px) {
76 | .container {
77 | max-width: 960px;
78 | }
79 | }
80 |
81 | @media (min-width: 1200px) {
82 | .container {
83 | max-width: 1140px;
84 | }
85 | }
86 |
87 | .row {
88 | display: -ms-flexbox;
89 | display: flex;
90 | -ms-flex-wrap: wrap;
91 | flex-wrap: wrap;
92 | margin-right: -15px;
93 | margin-left: -15px;
94 | }
95 |
96 | .no-gutters {
97 | margin-right: 0;
98 | margin-left: 0;
99 | }
100 |
101 | .no-gutters > [class*="col-"] {
102 | padding-right: 0;
103 | padding-left: 0;
104 | }
105 |
106 | .col-12, .col-md-10 {
107 | position: relative;
108 | width: 100%;
109 | padding-right: 15px;
110 | padding-left: 15px;
111 | }
112 |
113 | .col-12 {
114 | -ms-flex: 0 0 100%;
115 | flex: 0 0 100%;
116 | max-width: 100%;
117 | }
118 |
119 | @media (min-width: 768px) {
120 | .col-md-10 {
121 | -ms-flex: 0 0 83.333333%;
122 | flex: 0 0 83.333333%;
123 | max-width: 83.333333%;
124 | }
125 | .offset-md-1 {
126 | margin-left: 8.333333%;
127 | }
128 | }`,
129 | 'bootstrap',
130 | false
131 | );
132 |
133 | /* Duplicates selectors in this test related in bootstrap. Bootstrap have this duplicates.
134 | * Im sure cssnano clean this:) */
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/tests/dynamicSelectors.test.js:
--------------------------------------------------------------------------------
1 | const run = require('./utils/runWithConfigs');
2 |
3 | describe('Check removing empty mediaqueries', () => {
4 | test('Plugin remove useless mediaqueries where was removed selector', () => {
5 | run(
6 | '.test {color: red;} @media screen and (max-width: 200px) {.notest {color: red;}}',
7 | '.test {color: red;} @media screen and (max-width: 200px) {.notest {color: red;}}',
8 | ['basic'],
9 | [],
10 | [],
11 | [],
12 | ['notest']
13 | );
14 | });
15 |
16 | test('Plugin not remove selector if his has part in dynamic selectors', () => {
17 | run(
18 | '.qa-lifecycle-points__item:last-of-type { background: red; } .lololo(4) { background: red; }',
19 | '.qa-lifecycle-points__item:last-of-type { background: red; } .lololo(4) { background: red; }',
20 | ['dynamicSelectors'],
21 | [],
22 | [],
23 | [],
24 | ['lol']
25 | );
26 | });
27 |
28 | test('Plugin remove selector if his has part not exist in dynamic selectors', () => {
29 | run(
30 | '.qa-lifecycle-points__item:last-of-type { background: red; } .lololo(4) { background: red; }',
31 | '.qa-lifecycle-points__item:last-of-type { background: red; }',
32 | ['dynamicSelectors'],
33 | [],
34 | [],
35 | [],
36 | ['hohoho']
37 | );
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/tests/removeMediaQueries.test.js:
--------------------------------------------------------------------------------
1 | const run = require('./utils/runWithConfigs');
2 |
3 | describe('Check removing empty mediaqueries', () => {
4 | test('Plugin remove useless mediaqueries where was removed selector', () => {
5 | run(
6 | '.test {color: red;} @media screen and (max-width: 200px) {.notest {color: red;}}',
7 | '.test {color: red;}'
8 | );
9 | });
10 |
11 | test('Plugin remove empty mediaquery', () => {
12 | run('@media screen and (max-width: 200px) {}', '');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/tests/selectors/attributeSelectors.test.js:
--------------------------------------------------------------------------------
1 | const run = require('../utils/run');
2 |
3 | describe('Checks attribute selectors', () => {
4 | test('Must remove selector with attribute if attribute not exist(Simple attribute selector)', () => {
5 | run(
6 | 'a[href="#"] {color: red;} h1[data-attr="#"] {color: red;}',
7 | 'a[href="#"] {color: red;}',
8 | 'attributeSelectors/links'
9 | );
10 |
11 | run(
12 | 'a[href] {color: red;} h1[data-noattr] {color: red;}',
13 | 'a[href] {color: red;} h1[data-noattr] {color: red;}',
14 | 'attributeSelectors/links'
15 | );
16 | });
17 |
18 | test('Not remove dynamic selectors', () => {
19 | run(
20 | 'a[href~="test"] {color: red;} h1[data-noattr] {color: red;}',
21 | 'a[href~="test"] {color: red;} h1[data-noattr] {color: red;}',
22 | 'attributeSelectors/links'
23 | );
24 |
25 | run(
26 | 'a[href$="test"] {color: red;} h1[data-noattr] {color: red;}',
27 | 'a[href$="test"] {color: red;} h1[data-noattr] {color: red;}',
28 | 'attributeSelectors/links'
29 | );
30 |
31 | run(
32 | 'a[href*="test"] {color: red;} h1[data-noattr] {color: red;}',
33 | 'a[href*="test"] {color: red;} h1[data-noattr] {color: red;}',
34 | 'attributeSelectors/links'
35 | );
36 |
37 | run(
38 | 'a[href*="notExistName"] {color: red;} h1[data-noattr] {color: red;}',
39 | 'a[href*="notExistName"] {color: red;} h1[data-noattr] {color: red;}',
40 | 'attributeSelectors/links'
41 | );
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/tests/selectors/combinatorsSelectors.test.js:
--------------------------------------------------------------------------------
1 | const run = require('../utils/run');
2 |
3 | describe('Checks combinator selectors', () => {
4 | test('Adjacent sibling combinator', () => {
5 | run(
6 | '.selector + .selectorNested {color: red;} .selector + .notExistedClass {color: red;}',
7 | '.selector + .selectorNested {color: red;}',
8 | 'combinatorsSelectors'
9 | );
10 | });
11 |
12 | test('General sibling combinator', () => {
13 | run(
14 | '.selector ~ .selectorNested {color: red;} .selector ~ .notExistedClass {color: red;}',
15 | '.selector ~ .selectorNested {color: red;}',
16 | 'combinatorsSelectors'
17 | );
18 | });
19 |
20 | test('Child combinator', () => {
21 | run(
22 | '.selector > .selectorTripleNested {color: red;} .selector > .notExistedClass {color: red;}',
23 | '.selector > .selectorTripleNested {color: red;}',
24 | 'combinatorsSelectors'
25 | );
26 | });
27 |
28 | test('Descendant combinator', () => {
29 | run(
30 | '.selector .selectorNested {color: red;} .selector .notExistedClass {color: red;}',
31 | '.selector .selectorNested {color: red;}',
32 | 'combinatorsSelectors'
33 | );
34 | });
35 |
36 | test('Column combinator', () => {
37 | run(
38 | '.selector || .selectorNested {color: red;} .selector || .notExistedClass {color: red;}',
39 | '.selector || .selectorNested {color: red;}',
40 | 'combinatorsSelectors'
41 | );
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/tests/selectors/groupingSelectors.test.js:
--------------------------------------------------------------------------------
1 | const run = require('../utils/run');
2 |
3 | describe('Checks grouping selectors', () => {
4 | test('Must remove useless selector but existing must be stay (double selector)', () => {
5 | run(
6 | '.groupFirst, .groupSecond {color: red;}',
7 | '.groupFirst {color: red;}',
8 | 'groupingSelectors'
9 | );
10 | });
11 |
12 | test('Remove useless rule with selector', () => {
13 | run('.grouspFirst, .groupSecond {color: red;}', '', 'groupingSelectors');
14 | });
15 |
16 | test('Test with media query. Must remove useless rule with media', () => {
17 | run(
18 | '.groupFirst, .groupSecond {color: red;} @media screen {.groupFirst, .groupSecond {color: blue;}}',
19 | '.groupFirst {color: red;} @media screen {.groupFirst {color: blue;}}',
20 | 'groupingSelectors'
21 | );
22 |
23 | run(
24 | '.first, .second, .third, .some, .based {color: red;} @media screen {.first, .second, .third, .some, .based {color: blue;}}',
25 | '.first, .second, .third {color: red;} @media screen {.first, .second, .third {color: blue;}}',
26 | 'groupingSelectors/many'
27 | );
28 | });
29 |
30 | test('Must remove useless selector but existing must be stay (many selectors)', () => {
31 | run(
32 | '.first, .second, .third, .some, .based {color: red;}',
33 | '.first, .second, .third {color: red;}',
34 | 'groupingSelectors/many'
35 | );
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/tests/selectors/pseudoClasses.test.js:
--------------------------------------------------------------------------------
1 | const run = require('../utils/run');
2 |
3 | describe('Checks pseudo classes', () => {
4 | test('Check with pseudo class', () => {
5 | run(
6 | '.selector:after {color: red;} .uselessSelector:after {color: red;}',
7 | '.selector:after {color: red;}',
8 | 'combinatorsSelectors'
9 | );
10 |
11 | run(
12 | '.selector::after {color: red;} .uselessSelector::after {color: red;}',
13 | '.selector::after {color: red;}',
14 | 'combinatorsSelectors'
15 | );
16 |
17 | run(
18 | '.selector:before {color: red;} .uselessSelector:before {color: red;}',
19 | '.selector:before {color: red;}',
20 | 'combinatorsSelectors'
21 | );
22 |
23 | run(
24 | '.selector::before {color: red;} .uselessSelector::before {color: red;}',
25 | '.selector::before {color: red;}',
26 | 'combinatorsSelectors'
27 | );
28 |
29 | run(
30 | '.selector:link {color: red;} .uselessSelector::before {color: red;}',
31 | '.selector:link {color: red;}',
32 | 'combinatorsSelectors'
33 | );
34 | });
35 |
36 | test('Check with dynamic classes', () => {
37 | run(
38 | '.selector:link {color: red;} .selector:nth-child(3) {color: red;} .uselessSelector::before {color: red;}',
39 | '.selector:link {color: red;} .selector:nth-child(3) {color: red;}',
40 | 'combinatorsSelectors'
41 | );
42 | });
43 |
44 | test('Check with difficult dynamic classes 3n + 1', () => {
45 | run(
46 | '.selector:nth-child(3n + 2) {color: red;}',
47 | '.selector:nth-child(3n + 2) {color: red;}',
48 | 'combinatorsSelectors'
49 | );
50 | });
51 |
52 | test('Check with wtf difficult dynamic classes 3n + 1n - 5', () => {
53 | run(
54 | '.selector:nth-child(3n + 1n - 5) {color: red;}',
55 | '.selector:nth-child(3n + 1n - 5) {color: red;}',
56 | 'combinatorsSelectors'
57 | );
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/tests/utils/run.js:
--------------------------------------------------------------------------------
1 | const postcss = require('postcss');
2 |
3 | const plugin = require('../../index');
4 |
5 | /**
6 | * @param {string} input plain css text
7 | * @param {string} output plain css text
8 | * @param {string} folder folder in test folder(default basic)
9 | * @param {boolean} tagSelectors Support tag selector
10 | */
11 | function run(input, output, folder = 'basic', tagSelectors = true) {
12 | const options = {
13 | paths: [`testData/plugin/${folder}/`],
14 | fileExtensions: ['.html', '.js'],
15 | tagSelectors
16 | };
17 | expect(postcss([plugin(options)]).process(input).css).toBe(output);
18 | }
19 |
20 | module.exports = run;
21 |
--------------------------------------------------------------------------------
/tests/utils/runWithConfigs.js:
--------------------------------------------------------------------------------
1 | const postcss = require('postcss');
2 |
3 | const plugin = require('../../index');
4 |
5 | /**
6 | * @param {string} input plain css text
7 | * @param {string} output plain css text
8 | * @param {string[]} folder folder in test folder(default basic)
9 | * @param {string[]} libs folder in test folder(default basic)
10 | * @param {string[]} libsExtensions folder in test folder(default basic)
11 | * @param {string[]} whitelist Selectors that must be been in project
12 | * @param {string[]} dynamicSelectors Selectors that must be been in project
13 | * @param {boolean} tagSelectors Support tag selector
14 | */
15 | function run(
16 | input,
17 | output,
18 | folder = ['basic'],
19 | libs = [],
20 | libsExtensions = [],
21 | whitelist = [],
22 | dynamicSelectors = [],
23 | tagSelectors = true
24 | ) {
25 | const paths = folder.map(item => `testData/plugin/${item}/`);
26 | const options = {
27 | paths,
28 | fileExtensions: ['.html', '.js'],
29 | tagSelectors,
30 | libs,
31 | libsExtensions,
32 | whitelist,
33 | dynamicSelectors
34 | };
35 |
36 | expect(postcss([plugin(options)]).process(input).css).toBe(output);
37 | }
38 |
39 | module.exports = run;
40 |
--------------------------------------------------------------------------------
/tests/whiteList.test.js:
--------------------------------------------------------------------------------
1 | const run = require('./utils/runWithConfigs');
2 |
3 | describe('Check whitelist in plugin', () => {
4 | test('Not remove whitelist selector', () => {
5 | run(
6 | '.whiteSelector {color: red;} @media screen and (max-width: 200px) {.trueWhiteSelector {color: red;}}',
7 | '.whiteSelector {color: red;} @media screen and (max-width: 200px) {.trueWhiteSelector {color: red;}}',
8 | ['basic'],
9 | [],
10 | [],
11 | ['whiteSelector', 'trueWhiteSelector']
12 | );
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/tests/withFewLibs.test.js:
--------------------------------------------------------------------------------
1 | const run = require('./utils/runWithConfigs');
2 |
3 | describe('Check with libs', () => {
4 | test('Not remove slick slider classes', () => {
5 | run(
6 | '.slick-track {display: none;} .slick-active {color: red;} .noUserSelector {color: red;}',
7 | '.slick-track {display: none;} .slick-active {color: red;}',
8 | ['fewLibs'],
9 | ['slick-carousel'],
10 | ['.js']
11 | );
12 | });
13 |
14 | test('Remove slick classes if we set json extension', () => {
15 | run(
16 | '.slick-track {display: none;} .slick-active {color: red;} .noUserSelector {color: red;}',
17 | '',
18 | ['fewLibs'],
19 | ['slick-carousel'],
20 | ['.json']
21 | );
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/tests/withFewPathes.test.js:
--------------------------------------------------------------------------------
1 | const run = require('./utils/runWithConfigs');
2 |
3 | describe('Check with few paths', () => {
4 | test('Must contains selectors all paths', () => {
5 | run(
6 | '.fix {color: red;} .firstest {color: blue;} .firstpath {color: red} .firstsubpath {color: red} .secondpath {color: red} .secondsubpath {color: red}',
7 | '.firstpath {color: red} .firstsubpath {color: red} .secondpath {color: red} .secondsubpath {color: red}',
8 | ['fewPaths/first', 'fewPaths/second']
9 | );
10 | });
11 | });
12 |
--------------------------------------------------------------------------------