├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── index.js ├── package.json └── test.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: Node ${{ matrix.node-version }} 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | node-version: [18] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - run: npm install 17 | - run: npm test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | dist 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-prune-var 2 | 3 | PostCSS plugin to remove all unused variables in a CSS file. 4 | 5 | A `--variable` is considered unused if it, or any other variables consuming it are not accessed by a single `var()` statement in the whole CSS file. 6 | 7 | This is unsafe if there is a possibility of a second CSS file accessing variables from the first one. 8 | 9 | #### How does it work, and how is it different from postcss-unused-var? 10 | 11 | - `postcss-unused-var` is outdated, deprecated, and didn't work right. 12 | - Treats all variables as global, because they are. 13 | - Removes variables from everywhere, including `:root`. 14 | - Checks and prevents removal of variables only used by other variables that are accessed with `var()` by following variable dependency graph. 15 | - At the same time, if a variable is only used by other unused variables, it will also be removed. 16 | - Way faster. 17 | 18 | ## Install 19 | 20 | ``` 21 | npm install postcss-prune-var --save-dev 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```js 27 | const pruneVar = require('postcss-prune-var'); 28 | 29 | const yourConfig = { 30 | plugins: [pruneVar()], 31 | }; 32 | ``` 33 | 34 | ## Options 35 | 36 | ### skip 37 | 38 | Use this option to exclude certain files or folders that would otherwise be scanned. 39 | 40 | ```js 41 | const pruneVar = require('postcss-prune-var'); 42 | 43 | const yourConfig = { 44 | plugins: [pruneVar({skip: ['node_modules/**']})], 45 | }; 46 | ``` 47 | ## Example 48 | 49 | Input: 50 | 51 | ```css 52 | :root { 53 | --root-unused: red; 54 | --root-unused-proxy: var(--root-unused); 55 | --root-used: blue; 56 | } 57 | 58 | .foo { 59 | --unused: red; 60 | --unused-proxy: var(--unused); 61 | --proxied: pink; 62 | --proxy: var(--proxied); 63 | --used: green; 64 | color: var(--root-used); 65 | background: linear-gradient(to bottom, var(--used), var(--proxy)); 66 | } 67 | ``` 68 | 69 | Output 70 | 71 | ```css 72 | :root { 73 | --root-used: blue; 74 | } 75 | 76 | .foo { 77 | --proxied: pink; 78 | --proxy: var(--proxied); 79 | --used: green; 80 | color: var(--root-used); 81 | background: linear-gradient(to bottom, var(--used), var(--proxy)); 82 | } 83 | ``` 84 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const {minimatch} = require("minimatch"); 2 | const path = require("path"); 3 | 4 | /** 5 | * @typedef {object} UseRecord 6 | * @property {number} uses 7 | * @property {Set} declarations 8 | * @property {Set} dependencies 9 | */ 10 | 11 | module.exports = (options = {}) => { 12 | return { 13 | postcssPlugin: 'postcss-prune-var', 14 | Once(root) { 15 | const {skip = []} = options; 16 | if (skip.some((s) => minimatch(path.relative(process.cwd(), root.source.input.from), s, {dot: true}))) { 17 | return; 18 | } 19 | /** @type Map */ 20 | const records = new Map(); 21 | /** @type Set */ 22 | const usedVars = new Set(); 23 | 24 | /** @type {(variable: string) => UseRecord} */ 25 | const getRecord = (variable) => { 26 | let record = records.get(variable); 27 | if (!record) { 28 | record = {uses: 0, dependencies: new Set(), declarations: new Set()}; 29 | records.set(variable, record); 30 | } 31 | return record; 32 | }; 33 | 34 | /** @type {(variable: string, ignoreList?: Set) => void} */ 35 | const registerUse = (variable, ignoreList = new Set()) => { 36 | const record = getRecord(variable); 37 | record.uses++; 38 | ignoreList.add(variable); 39 | for (const dependency of record.dependencies) { 40 | if (!ignoreList.has(dependency)) registerUse(dependency, ignoreList); 41 | } 42 | }; 43 | 44 | /** @type {(variable: string, dependency: string) => void} */ 45 | const registerDependency = (variable, dependency) => { 46 | const record = getRecord(variable); 47 | record.dependencies.add(dependency); 48 | }; 49 | 50 | // Detect variable uses 51 | root.walkDecls((decl) => { 52 | const isVar = decl.prop.startsWith('--'); 53 | 54 | // Initiate record 55 | if (isVar) getRecord(decl.prop).declarations.add(decl); 56 | 57 | if (!decl.value.includes('var(')) return; 58 | 59 | for (const match of decl.value.matchAll(/var\(\s*(?--[^ ,\);]+)/g)) { 60 | const variable = match.groups.name.trim(); 61 | if (isVar) { 62 | registerDependency(decl.prop, variable); 63 | } else { 64 | usedVars.add(variable); 65 | } 66 | } 67 | }); 68 | 69 | // We register variable uses only after all variables have been entered into the graph, 70 | // otherwise we remove variables that were defined in CSS file after some property used them. 71 | for (const variable of usedVars) { 72 | registerUse(variable); 73 | } 74 | 75 | // Remove unused variables 76 | for (const {uses, declarations} of records.values()) { 77 | if (uses === 0) { 78 | for (let decl of declarations) decl.remove(); 79 | } 80 | } 81 | }, 82 | }; 83 | }; 84 | 85 | module.exports.postcss = true 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-prune-var", 3 | "version": "1.1.2", 4 | "description": "PostCSS plugin to remove unused variables", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js", 8 | "git-push": "git push", 9 | "npm-publish": "npm publish", 10 | "postversion": "npm-run-all git-push npm-publish" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/tomasklaen/postcss-prune-var.git" 15 | }, 16 | "keywords": [ 17 | "postcss", 18 | "postcss-plugin", 19 | "unused", 20 | "prune", 21 | "remove", 22 | "var", 23 | "variable" 24 | ], 25 | "author": "tomasklaen", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/tomasklaen/postcss-prune-var/issues" 29 | }, 30 | "homepage": "https://github.com/tomasklaen/postcss-prune-var#readme", 31 | "dependencies": { 32 | "minimatch": "^9.0.3" 33 | }, 34 | "devDependencies": { 35 | "npm-run-all": "^4.1.5", 36 | "postcss": "^8.4.35" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const postcss = require('postcss').default; 3 | const pruneVar = require('./'); 4 | 5 | const tests = []; 6 | const test = (name, fn) => (fn.displayName = name) && tests.push(fn); 7 | 8 | test('base', async () => { 9 | const input = ` 10 | :root { 11 | --root-unused: red; 12 | --root-unused-proxy: var(--root-unused); 13 | --root-used: blue; 14 | --unused-duplicate: yellow; 15 | --circular: var(--circular); 16 | color: var(--circular); 17 | } 18 | .foo { 19 | --unused: red; 20 | --unused-proxy: var(--unused); 21 | --proxied: pink; 22 | --proxy: var(--proxied); 23 | --used: green; 24 | --unused-duplicate: yellow; 25 | color: var(--root-used); 26 | background: linear-gradient(to bottom, var(--used), var(--proxy)); 27 | } 28 | `; 29 | const output = ` 30 | :root { 31 | --root-used: blue; 32 | --circular: var(--circular); 33 | color: var(--circular); 34 | } 35 | .foo { 36 | --proxied: pink; 37 | --proxy: var(--proxied); 38 | --used: green; 39 | color: var(--root-used); 40 | background: linear-gradient(to bottom, var(--used), var(--proxy)); 41 | } 42 | `; 43 | const result = await postcss([pruneVar()]).process(input, {from: 'test.css', to: 'out.css'}); 44 | assert.equal(result.css, output, `Result different than expected`); 45 | }); 46 | 47 | test('duplicate var definitions', async () => { 48 | const input = ` 49 | :root { 50 | --color-state-bg-striped: green; 51 | } 52 | 53 | .example { 54 | --color-component-bg: red; 55 | --color-state-bg: var(--color-component-bg); 56 | 57 | .cell { 58 | background-color: var(--color-state-bg); 59 | } 60 | 61 | .child > .row:nth-of-type(odd) > * { 62 | --color-state-bg: var(--color-state-bg-striped); 63 | } 64 | } 65 | `; 66 | const result = await postcss([pruneVar()]).process(input, {from: 'test.css', to: 'out.css'}); 67 | assert.equal(result.css, input, `Result different than expected`); 68 | }); 69 | 70 | test('file filtering', async () => { 71 | const output = ` 72 | :root { 73 | --root-unused: red; 74 | --root-unused-proxy: var(--root-unused); 75 | --root-used: blue; 76 | --unused-duplicate: yellow; 77 | --circular: var(--circular); 78 | color: var(--circular); 79 | } 80 | .foo { 81 | --unused: red; 82 | --unused-proxy: var(--unused); 83 | --proxied: pink; 84 | --proxy: var(--proxied); 85 | --used: green; 86 | --unused-duplicate: yellow; 87 | color: var(--root-used); 88 | background: linear-gradient(to bottom, var(--used), var(--proxy)); 89 | } 90 | `; 91 | 92 | const result = await postcss([pruneVar({skip: ['fixtures/**']})]).process(output, { 93 | from: 'fixtures/test.css', 94 | to: 'out.css', 95 | }); 96 | assert.strictEqual(result.css, output, `Result different than expected`); 97 | }); 98 | 99 | (async function () { 100 | for (const test of tests) { 101 | try { 102 | await test(); 103 | console.log(`✔ ${test.displayName}`); 104 | } catch (error) { 105 | console.log(`❌ ${test.displayName}`); 106 | console.error(error); 107 | } 108 | } 109 | })(); 110 | --------------------------------------------------------------------------------