├── .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 | ![Travis CI on linux](https://img.shields.io/travis/com/rammfall/postcss-trash-killer?style=for-the-badge) 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 |
12 |
13 |
14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /testData/plugin/combinatorsSelectors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 |
13 |
14 |
15 |
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 |
12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /testData/plugin/fewPaths/second/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 |
13 |
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 |
12 |
13 |
14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /testData/plugin/pseudoElements/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 |
13 |
14 |
15 |
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 | --------------------------------------------------------------------------------