├── .husky ├── .gitignore └── pre-commit ├── packages ├── grunt-purgecss │ ├── __tests__ │ │ ├── fixtures │ │ │ ├── expected │ │ │ │ ├── footer.css │ │ │ │ ├── menu.css │ │ │ │ ├── profile.css │ │ │ │ └── simple.css │ │ │ └── src │ │ │ │ ├── menu.css │ │ │ │ ├── footer.css │ │ │ │ ├── profile.css │ │ │ │ └── simple │ │ │ │ ├── simple.html │ │ │ │ └── simple.css │ │ └── index.test.ts │ ├── src │ │ ├── types │ │ │ └── index.d.ts │ │ └── index.ts │ ├── jest.config.ts │ ├── tsconfig.json │ ├── build.ts │ ├── Gruntfile.js │ └── package.json ├── purgecss │ ├── __tests__ │ │ ├── test_examples │ │ │ ├── keyframes │ │ │ │ ├── index.html │ │ │ │ ├── keyframes.html │ │ │ │ ├── index.css │ │ │ │ └── keyframes.css │ │ │ ├── pseudo-class │ │ │ │ ├── pseudo_class.js │ │ │ │ ├── not.html │ │ │ │ ├── not.css │ │ │ │ ├── pseudo_class.css │ │ │ │ ├── nth_child.html │ │ │ │ ├── is.html │ │ │ │ ├── nth_child.css │ │ │ │ ├── where.html │ │ │ │ ├── pseudo_selector.css │ │ │ │ ├── where.css │ │ │ │ ├── is.css │ │ │ │ └── pseudo_selector.html │ │ │ ├── rejectedCss │ │ │ │ ├── simple.js │ │ │ │ ├── empty-parent-node.js │ │ │ │ ├── simple.css │ │ │ │ └── empty-parent-node.css │ │ │ ├── others │ │ │ │ ├── remove_unused.js │ │ │ │ ├── special_characters.js │ │ │ │ ├── remove_unused.css │ │ │ │ └── special_characters.css │ │ │ ├── cli │ │ │ │ └── simple │ │ │ │ │ └── src │ │ │ │ │ ├── index.js │ │ │ │ │ ├── style2.css │ │ │ │ │ ├── style.css │ │ │ │ │ └── content.html │ │ │ ├── delimited │ │ │ │ ├── delimited.html │ │ │ │ └── delimited.css │ │ │ ├── pseudo-elements │ │ │ │ ├── pseudo-elements.html │ │ │ │ └── pseudo-elements.css │ │ │ ├── css-variables │ │ │ │ ├── variables.html │ │ │ │ └── variables.css │ │ │ ├── safelist │ │ │ │ ├── safelist_keyframes.html │ │ │ │ ├── safelist_css_variables.html │ │ │ │ ├── safelist.html │ │ │ │ ├── safelist_patterns_greedy.html │ │ │ │ ├── safelist_patterns_children.css │ │ │ │ ├── safelist_patterns_greedy.css │ │ │ │ ├── safelist_patterns_children.html │ │ │ │ ├── blocklist.html │ │ │ │ ├── safelist.css │ │ │ │ ├── blocklist.css │ │ │ │ ├── safelist_css_variables.css │ │ │ │ └── safelist_keyframes.css │ │ │ ├── skipped-content │ │ │ │ ├── skippedFolder │ │ │ │ │ └── skipped.html │ │ │ │ ├── unskipped.html │ │ │ │ └── simple.css │ │ │ ├── rejected │ │ │ │ ├── simple.js │ │ │ │ └── simple.css │ │ │ ├── comments │ │ │ │ ├── ignore_comment.html │ │ │ │ ├── ignore_comment_range.html │ │ │ │ ├── ignore_comment.css │ │ │ │ └── ignore_comment_range.css │ │ │ ├── media-queries │ │ │ │ ├── media_queries.html │ │ │ │ └── media_queries.css │ │ │ ├── font-faces │ │ │ │ ├── font_face.html │ │ │ │ └── font_face.css │ │ │ ├── attributes │ │ │ │ ├── attribute_selector.html │ │ │ │ └── attribute_selector.css │ │ │ └── chaining-rules │ │ │ │ ├── index.html │ │ │ │ └── index.css │ │ ├── purgecss.config.js │ │ ├── utils.ts │ │ ├── chaining-rules.test.ts │ │ ├── skipped-content.test.ts │ │ ├── globs.test.ts │ │ ├── cli │ │ │ ├── cli-console-output.test.ts │ │ │ ├── cli-file-output.test.ts │ │ │ ├── cli-options.test.ts │ │ │ └── cli-multiple-files-output.test.ts │ │ ├── font-faces.test.ts │ │ ├── delimited.test.ts │ │ ├── performance.test.ts │ │ ├── sourcemap.test.ts │ │ ├── media-queries.test.ts │ │ ├── pseudo-elements.test.ts │ │ ├── css-variables.test.ts │ │ ├── raw-css-name.test.ts │ │ ├── comments.test.ts │ │ ├── rejectedCss.test.ts │ │ └── rejected.test.ts │ ├── src │ │ ├── internal-safelist.ts │ │ ├── constants.ts │ │ ├── options.ts │ │ └── ExtractorResultSets.ts │ ├── jest.config.ts │ ├── tsconfig.json │ ├── package.json │ ├── build.ts │ └── README.md ├── vue-cli-plugin-purgecss │ ├── index.js │ ├── logo.png │ ├── generator │ │ ├── index.js │ │ └── templates │ │ │ └── postcss.config.js │ ├── package.json │ └── README.md ├── purgecss-webpack-plugin │ ├── __tests__ │ │ ├── cases │ │ │ ├── simple │ │ │ │ ├── src │ │ │ │ │ ├── index.js │ │ │ │ │ ├── style.css │ │ │ │ │ └── content.html │ │ │ │ ├── expected │ │ │ │ │ └── styles.css │ │ │ │ ├── purgecss.config.js │ │ │ │ └── webpack.config.js │ │ │ ├── simple-with-exclusion │ │ │ │ ├── src │ │ │ │ │ ├── legacy.js │ │ │ │ │ ├── style.css │ │ │ │ │ ├── style_that_we_want_to_purge.css │ │ │ │ │ └── index.js │ │ │ │ ├── expected │ │ │ │ │ ├── legacy.css │ │ │ │ │ └── bundle.css │ │ │ │ └── webpack.config.js │ │ │ └── path-and-safelist-functions │ │ │ │ ├── src │ │ │ │ ├── index.js │ │ │ │ ├── content.html │ │ │ │ └── style.css │ │ │ │ ├── expected │ │ │ │ └── styles.css │ │ │ │ └── webpack.config.js │ │ └── index.test.ts │ ├── jest.config.ts │ ├── tsconfig.json │ ├── build.ts │ ├── src │ │ └── types │ │ │ └── index.ts │ └── package.json ├── postcss-purgecss │ ├── __tests__ │ │ └── fixtures │ │ │ ├── expected │ │ │ ├── simple.css │ │ │ ├── other-plugins.css │ │ │ └── font-keyframes.css │ │ │ └── src │ │ │ ├── config-test │ │ │ ├── purgecss.config.js │ │ │ ├── index.html │ │ │ ├── expected.css │ │ │ └── index.css │ │ │ ├── simple │ │ │ ├── simple.html │ │ │ └── simple.css │ │ │ ├── other-plugins │ │ │ ├── other-plugins.html │ │ │ └── other-plugins.css │ │ │ └── font-keyframes │ │ │ ├── font-keyframes.html │ │ │ └── font-keyframes.css │ ├── jest.config.ts │ ├── tsconfig.json │ ├── build.ts │ ├── src │ │ └── types │ │ │ └── index.ts │ └── package.json ├── gulp-purgecss │ ├── __tests__ │ │ ├── test.html │ │ ├── buffer.test.ts │ │ └── stream.test.ts │ ├── jest.config.ts │ ├── src │ │ └── types │ │ │ └── index.ts │ ├── tsconfig.json │ ├── build.ts │ ├── package.json │ └── README.md ├── purgecss-from-jsx │ ├── jest.config.ts │ ├── README.md │ ├── tsconfig.json │ ├── build.ts │ ├── __tests__ │ │ ├── data.ts │ │ └── index.test.ts │ └── package.json ├── purgecss-from-pug │ ├── jest.config.ts │ ├── tsconfig.json │ ├── build.ts │ ├── README.md │ ├── package.json │ ├── __tests__ │ │ ├── data.ts │ │ └── index.test.ts │ └── src │ │ └── index.ts ├── purgecss-from-tsx │ ├── jest.config.ts │ ├── README.md │ ├── tsconfig.json │ ├── build.ts │ ├── src │ │ └── index.ts │ ├── __tests__ │ │ ├── data.ts │ │ └── index.test.ts │ └── package.json ├── rollup-plugin-purgecss │ ├── __tests__ │ │ ├── assets │ │ │ ├── actual_a.css │ │ │ ├── expect_a.css │ │ │ ├── test_a.css │ │ │ └── test_a.html │ │ ├── fixtures │ │ │ └── basic │ │ │ │ └── index.js │ │ └── index.test.ts │ ├── jest.config.ts │ ├── tsconfig.json │ ├── build.ts │ ├── src │ │ ├── types │ │ │ └── index.ts │ │ └── index.ts │ ├── package.json │ └── README.md ├── purgecss-from-html │ ├── jest.config.ts │ ├── README.md │ ├── tsconfig.json │ ├── build.ts │ ├── package.json │ └── __tests__ │ │ ├── data.ts │ │ └── index.test.ts └── purgecss-with-wordpress │ ├── package.json │ ├── index.js │ ├── LICENSE │ └── README.md ├── types ├── vinyl-sourcemaps-apply │ └── index.d.ts └── acorn-jsx-walk │ └── index.d.ts ├── lerna.json ├── docs ├── .vuepress │ ├── public │ │ ├── favicon.ico │ │ ├── full-human.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── mstile-150x150.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── browserconfig.xml │ │ ├── site.webmanifest │ │ └── safari-pinned-tab.svg │ ├── client.ts │ └── theme │ │ ├── client.ts │ │ ├── layouts │ │ └── Layout.vue │ │ ├── index.ts │ │ └── components │ │ └── CarbonAds.vue ├── plugins │ ├── gulp.md │ └── postcss.md ├── guides │ ├── wordpress.md │ └── vue.md ├── introduction.md ├── getting-started.md ├── api.md ├── README.md └── ant_design.md ├── firebase.json ├── scripts └── verify-commit.ts ├── .github ├── dependabot.yml ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.yml ├── workflows │ ├── stale-issues.yml │ └── build-and-test.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── tsconfig.json ├── .vscode └── launch.json ├── SECURITY.md ├── jest.config.ts ├── LICENSE ├── eslint.config.mjs ├── .gitignore ├── CONTRIBUTING.md └── package.json /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run prettier 2 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/__tests__/fixtures/expected/footer.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/__tests__/fixtures/expected/menu.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/__tests__/fixtures/expected/profile.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import "grunt"; 2 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/keyframes/index.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/pseudo_class.js: -------------------------------------------------------------------------------- 1 | "div"; 2 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/rejectedCss/simple.js: -------------------------------------------------------------------------------- 1 | 2 | "critical" 3 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/others/remove_unused.js: -------------------------------------------------------------------------------- 1 | ".used-class"; 2 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/rejectedCss/empty-parent-node.js: -------------------------------------------------------------------------------- 1 | "used-class" -------------------------------------------------------------------------------- /packages/vue-cli-plugin-purgecss/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, options) => {}; 2 | -------------------------------------------------------------------------------- /types/vinyl-sourcemaps-apply/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "vinyl-sourcemaps-apply"; 2 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple/src/index.js: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/cli/simple/src/index.js: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/not.html: -------------------------------------------------------------------------------- 1 |

anything

-------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/delimited/delimited.html: -------------------------------------------------------------------------------- 1 |

hello

2 |

world

3 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-elements/pseudo-elements.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /packages/grunt-purgecss/__tests__/fixtures/expected/simple.css: -------------------------------------------------------------------------------- 1 | .used-class { 2 | color: black; 3 | } 4 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/expected/simple.css: -------------------------------------------------------------------------------- 1 | .used-class { 2 | color: black; 3 | } 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "7.0.2" 4 | } 5 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/__tests__/fixtures/src/menu.css: -------------------------------------------------------------------------------- 1 | .menu-unused-class { 2 | background: red; 3 | } 4 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple-with-exclusion/src/legacy.js: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/css-variables/variables.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/safelist_keyframes.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | -------------------------------------------------------------------------------- /packages/purgecss/src/internal-safelist.ts: -------------------------------------------------------------------------------- 1 | export const CSS_SAFELIST = ["*", ":root", ":after", ":before"]; 2 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/__tests__/fixtures/src/footer.css: -------------------------------------------------------------------------------- 1 | .footer-unused-class { 2 | background: black; 3 | } 4 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/path-and-safelist-functions/src/index.js: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/skipped-content/skippedFolder/skipped.html: -------------------------------------------------------------------------------- 1 |

anything

2 | -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FullHuman/purgecss/HEAD/docs/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /packages/grunt-purgecss/__tests__/fixtures/src/profile.css: -------------------------------------------------------------------------------- 1 | .profile-unused-class { 2 | background: hotpink; 3 | } 4 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/delimited/delimited.css: -------------------------------------------------------------------------------- 1 | .unused-class-name, h1, p a{ 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/safelist_css_variables.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.vuepress/public/full-human.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FullHuman/purgecss/HEAD/docs/.vuepress/public/full-human.png -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/expected/other-plugins.css: -------------------------------------------------------------------------------- 1 | .prefixed-used-class { 2 | color: black; 3 | } 4 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/purgecss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['index.html'], 3 | css: ['style.css'] 4 | } -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FullHuman/purgecss/HEAD/docs/.vuepress/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FullHuman/purgecss/HEAD/docs/.vuepress/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/rejected/simple.js: -------------------------------------------------------------------------------- 1 | "single"; 2 | 3 | "double-class"; 4 | 5 | "triple-simple-class"; 6 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/skipped-content/unskipped.html: -------------------------------------------------------------------------------- 1 |

anything

2 |

anything

-------------------------------------------------------------------------------- /docs/.vuepress/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FullHuman/purgecss/HEAD/docs/.vuepress/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/.vuepress/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FullHuman/purgecss/HEAD/docs/.vuepress/public/mstile-150x150.png -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/safelist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Hmmmm

5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/vue-cli-plugin-purgecss/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FullHuman/purgecss/HEAD/packages/vue-cli-plugin-purgecss/logo.png -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/comments/ignore_comment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Hmmmm

5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/keyframes/keyframes.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/others/special_characters.js: -------------------------------------------------------------------------------- 1 | "@home"; 2 | 3 | "+rounded"; 4 | 5 | "button"; 6 | 7 | ".md:w-1/3"; 8 | -------------------------------------------------------------------------------- /packages/purgecss/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./../../jest.config"; 2 | export default createConfig(__dirname, "purgecss"); 3 | -------------------------------------------------------------------------------- /docs/.vuepress/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FullHuman/purgecss/HEAD/docs/.vuepress/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/.vuepress/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FullHuman/purgecss/HEAD/docs/.vuepress/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/cli/simple/src/style2.css: -------------------------------------------------------------------------------- 1 | .world { 2 | color: green; 3 | } 4 | 5 | .unused { 6 | color: blue; 7 | } 8 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/comments/ignore_comment_range.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Hmmmm

5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/rejectedCss/simple.css: -------------------------------------------------------------------------------- 1 | .critical { 2 | color: red; 3 | } 4 | 5 | .rejected { 6 | color: blue; 7 | } 8 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./../../jest.config"; 2 | export default createConfig(__dirname, "grunt-purgecss"); 3 | -------------------------------------------------------------------------------- /packages/gulp-purgecss/__tests__/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/gulp-purgecss/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./../../jest.config"; 2 | export default createConfig(__dirname, "gulp-purgecss"); 3 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/not.css: -------------------------------------------------------------------------------- 1 | .bar:not(.foo) { 2 | color: red; 3 | } 4 | .bar:not(.foo-bar) { 5 | color: blue; 6 | } 7 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./../../jest.config"; 2 | export default createConfig(__dirname, "postcss-purgecss"); 3 | -------------------------------------------------------------------------------- /packages/purgecss-from-jsx/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./../../jest.config"; 2 | export default createConfig(__dirname, "purgecss-from-jsx"); 3 | -------------------------------------------------------------------------------- /packages/purgecss-from-pug/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./../../jest.config"; 2 | export default createConfig(__dirname, "purgecss-from-pug"); 3 | -------------------------------------------------------------------------------- /packages/purgecss-from-tsx/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./../../jest.config"; 2 | export default createConfig(__dirname, "purgecss-from-tsx"); 3 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/pseudo_class.css: -------------------------------------------------------------------------------- 1 | div:before { 2 | color: black; 3 | } 4 | 5 | .row:after { 6 | color: black; 7 | } 8 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/safelist_patterns_greedy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/__tests__/assets/actual_a.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | color: red; 3 | } 4 | 5 | .used, 6 | .there { 7 | color: green; 8 | } 9 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/__tests__/assets/expect_a.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | color: red; 3 | } 4 | 5 | .used, 6 | .there { 7 | color: green; 8 | } 9 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/__tests__/fixtures/src/simple/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/src/config-test/purgecss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [`${__dirname}/index.html`], 3 | fontFace: true 4 | } -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/src/simple/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/purgecss-from-html/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./../../jest.config"; 2 | export default createConfig(__dirname, "purgecss-from-html"); 3 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/__tests__/fixtures/basic/index.js: -------------------------------------------------------------------------------- 1 | import "../../assets/test_a.css"; 2 | 3 | export default function noop() { 4 | return; 5 | } 6 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/media-queries/media_queries.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 'alone' 4 | 5 | 'id-in-media' 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./../../jest.config"; 2 | export default createConfig(__dirname, "rollup-plugin-purgecss"); 3 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./../../jest.config"; 2 | export default createConfig(__dirname, "purgecss-webpack-plugin"); 3 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/nth_child.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
-------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/rejectedCss/empty-parent-node.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 66666px) { 2 | .used-class, .unused-class { 3 | color: black; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/src/config-test/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
-------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/font-faces/font_face.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /types/acorn-jsx-walk/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "acorn-jsx-walk" { 2 | import * as walk from "acorn-walk"; 3 | export function extend(base: walk.RecursiveVisitors): void; 4 | } 5 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/src/other-plugins/other-plugins.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/.vuepress/client.ts: -------------------------------------------------------------------------------- 1 | import { defineClientConfig } from "@vuepress/client"; 2 | 3 | export default defineClientConfig({ 4 | enhance() {}, 5 | setup() {}, 6 | rootComponents: [], 7 | }); 8 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/src/font-keyframes/font-keyframes.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
-------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/skipped-content/simple.css: -------------------------------------------------------------------------------- 1 | .black { 2 | color: black; 3 | } 4 | 5 | .red { 6 | color: red; 7 | } 8 | 9 | .green { 10 | color: green; 11 | } 12 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/is.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
-------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/rejected/simple.css: -------------------------------------------------------------------------------- 1 | .single { 2 | color: black; 3 | } 4 | 5 | .double-class { 6 | color: black; 7 | } 8 | 9 | .triple-simple-class { 10 | color: black; 11 | } 12 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/safelist_patterns_children.css: -------------------------------------------------------------------------------- 1 | .card {} 2 | .title {} 3 | .card .content {} 4 | 5 | .btn {} 6 | .btn .red {} 7 | .btngreen {} 8 | 9 | .card .btn .yellow {} 10 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/__tests__/assets/test_a.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | color: red; 3 | } 4 | 5 | .used, 6 | .there { 7 | color: green; 8 | } 9 | 10 | .unused { 11 | color: #ff00ff; 12 | } 13 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/nth_child.css: -------------------------------------------------------------------------------- 1 | .some-item:nth-child(2n){ 2 | color: green 3 | } 4 | .some-item:nth-child(2n+1){ 5 | color: blue 6 | } 7 | canvas { 8 | display: none 9 | } 10 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/where.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
-------------------------------------------------------------------------------- /docs/.vuepress/theme/client.ts: -------------------------------------------------------------------------------- 1 | import { defineClientConfig } from "@vuepress/client"; 2 | import Layout from "./layouts/Layout.vue"; 3 | 4 | export default defineClientConfig({ 5 | layouts: { 6 | Layout, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/__tests__/fixtures/src/simple/simple.css: -------------------------------------------------------------------------------- 1 | .used-class { 2 | color: black; 3 | } 4 | 5 | .unused-class { 6 | color: black; 7 | } 8 | 9 | .another-one-not-found { 10 | color: black; 11 | } 12 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/src/simple/simple.css: -------------------------------------------------------------------------------- 1 | .used-class { 2 | color: black; 3 | } 4 | 5 | .unused-class { 6 | color: black; 7 | } 8 | 9 | .another-one-not-found { 10 | color: black; 11 | } 12 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/comments/ignore_comment.css: -------------------------------------------------------------------------------- 1 | /* purgecss ignore */ 2 | h1 { 3 | color: blue; 4 | 5 | } 6 | 7 | h3 { 8 | /* purgecss ignore current */ 9 | color: red; 10 | } 11 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/others/remove_unused.css: -------------------------------------------------------------------------------- 1 | .used-class { 2 | color: black; 3 | } 4 | 5 | .unused-class { 6 | color: black; 7 | } 8 | 9 | .another-one-not-found { 10 | color: black; 11 | } 12 | -------------------------------------------------------------------------------- /packages/purgecss-from-html/README.md: -------------------------------------------------------------------------------- 1 | # `purgecss-from-html` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const purgecssFromHtml = require('purgecss-from-html'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/purgecss-from-jsx/README.md: -------------------------------------------------------------------------------- 1 | # `purgecss-from-jsx` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const purgecssFromJsx = require('purgecss-from-jsx'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/purgecss-from-tsx/README.md: -------------------------------------------------------------------------------- 1 | # `purgecss-from-tsx` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const purgecssFromTsx = require('purgecss-from-tsx'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple-with-exclusion/src/style.css: -------------------------------------------------------------------------------- 1 | .legacy-unused-class { 2 | color: red; 3 | } 4 | 5 | .unused-too { 6 | color: blue; 7 | } 8 | 9 | .hello { 10 | color: red; 11 | } 12 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "cleanUrls": true, 4 | "public": "docs/.vuepress/dist", 5 | "ignore": [ 6 | "firebase.json", 7 | "**/.*", 8 | "**/node_modules/**" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/src/other-plugins/other-plugins.css: -------------------------------------------------------------------------------- 1 | .used-class { 2 | color: black; 3 | } 4 | 5 | .unused-class { 6 | color: black; 7 | } 8 | 9 | .another-one-not-found { 10 | color: black; 11 | } 12 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/safelist_patterns_greedy.css: -------------------------------------------------------------------------------- 1 | .card {} 2 | .card[data-v-test] {} 3 | .card.card--large {} 4 | .card[data-v-test].card--large {} 5 | .card .card-content {} 6 | .card[data-v-test] .card-content {} 7 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/__tests__/assets/test_a.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello 4 |
5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/cli/simple/src/style.css: -------------------------------------------------------------------------------- 1 | .hello { 2 | color: red; 3 | } 4 | 5 | .unused { 6 | color: blue; 7 | } 8 | 9 | .safelisted { 10 | color: green; 11 | } 12 | 13 | md\:w-2\/3 { 14 | color: red; 15 | } 16 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple/src/style.css: -------------------------------------------------------------------------------- 1 | .hello { 2 | color: red; 3 | } 4 | 5 | .unused { 6 | color: blue; 7 | } 8 | 9 | .safelisted { 10 | color: green; 11 | } 12 | 13 | md\:w-2\/3 { 14 | color: red; 15 | } 16 | -------------------------------------------------------------------------------- /packages/vue-cli-plugin-purgecss/generator/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, options) => { 2 | api.extendPackage({ 3 | devDependencies: { 4 | "@fullhuman/postcss-purgecss": "^4.0.0", 5 | }, 6 | }); 7 | api.render("./templates", options); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/others/special_characters.css: -------------------------------------------------------------------------------- 1 | .\@home { 2 | color: black; 3 | } 4 | 5 | .button.\+rounded { 6 | color: black; 7 | } 8 | 9 | .md\:w-1\/3 { 10 | color: green; 11 | } 12 | 13 | .\32 -panels { 14 | color: red; 15 | } 16 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple/src/content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Purgecss webpack test 5 | 6 | 7 | 8 |
9 |
10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple-with-exclusion/src/style_that_we_want_to_purge.css: -------------------------------------------------------------------------------- 1 | .hello { 2 | color: red; 3 | } 4 | 5 | .unused { 6 | color: blue; 7 | } 8 | 9 | .safelisted { 10 | color: green; 11 | } 12 | 13 | md\:w-2\/3 { 14 | color: red; 15 | } 16 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/cli/simple/src/content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Purgecss webpack test 5 | 6 | 7 | 8 |
9 |
10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/safelist_patterns_children.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |
7 |
8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/gulp-purgecss/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { UserDefinedOptions as PurgeCSSUserDefinedOptions } from "purgecss"; 2 | 3 | /** 4 | * @public 5 | */ 6 | export interface UserDefinedOptions 7 | extends Omit { 8 | content: string[]; 9 | } 10 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/path-and-safelist-functions/src/content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Purgecss webpack test 5 | 6 | 7 | 8 |
9 |
10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/purgecss-from-html/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "declarationDir": "./.temp/", 7 | "baseUrl": ".", 8 | }, 9 | "include": ["src"], 10 | "exclude": ["./lib"] 11 | } -------------------------------------------------------------------------------- /packages/grunt-purgecss/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "baseUrl": ".", 6 | "paths": { 7 | "purgecss": ["../purgecss/src"], 8 | } 9 | }, 10 | "include": ["./src/**/*", "./../purgecss/src/*"] 11 | } -------------------------------------------------------------------------------- /docs/.vuepress/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/blocklist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Title

7 |
random
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/purgecss-from-pug/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "declarationDir": "./.temp/", 7 | "baseUrl": ".", 8 | }, 9 | "include": ["./src/**/*", "./rollup.config.ts"], 10 | "exclude": ["./lib"] 11 | } -------------------------------------------------------------------------------- /docs/.vuepress/theme/layouts/Layout.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple-with-exclusion/src/index.js: -------------------------------------------------------------------------------- 1 | import "./style_that_we_want_to_purge.css"; 2 | 3 | function component() { 4 | const element = document.createElement("div"); 5 | 6 | element.classList.add("hello"); 7 | element.classList.add("md:w-2/3"); 8 | return element; 9 | } 10 | 11 | document.body.appendChild(component()); 12 | -------------------------------------------------------------------------------- /packages/purgecss-from-jsx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "declarationDir": "./.temp/", 7 | "baseUrl": ".", 8 | "typeRoots": ["./../../types", "./../../node_modules/@types"] 9 | }, 10 | "include": ["src", "./../../types/**/*"], 11 | "exclude": ["./lib"] 12 | } -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/attributes/attribute_selector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | pdf 6 | go to website 7 | hello 8 |
hello
9 | 10 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "declarationDir": "./.temp/", 7 | "baseUrl": ".", 8 | "paths": { 9 | "purgecss": ["../purgecss/src"], 10 | } 11 | }, 12 | "include": ["src", "./../purgecss/src/*"], 13 | "exclude": ["./lib"] 14 | } -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/path-and-safelist-functions/src/style.css: -------------------------------------------------------------------------------- 1 | .hello { 2 | color: red; 3 | } 4 | 5 | .unused { 6 | color: blue; 7 | } 8 | 9 | .safelisted { 10 | color: green; 11 | } 12 | 13 | .safelistedPattern { 14 | color: rebeccapurple; 15 | } 16 | 17 | .safelistedPatternChildren > p a { 18 | color: orange; 19 | } 20 | 21 | md\:w-2\/3 { 22 | color: red; 23 | } 24 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/safelist.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: blue; 3 | } 4 | 5 | .random { 6 | color: green; 7 | } 8 | 9 | #yep { 10 | color: red; 11 | } 12 | 13 | button { 14 | color: rebeccapurple; 15 | } 16 | 17 | .nav-blue { 18 | background-color: blue; 19 | } 20 | 21 | .nav-red { 22 | background-color: red; 23 | } 24 | 25 | [data-v-test] { 26 | color: green; 27 | } 28 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "declarationDir": "./.temp/", 7 | "baseUrl": ".", 8 | "paths": { 9 | "purgecss": ["../purgecss/src"], 10 | } 11 | }, 12 | "include": ["./src/**/*", "./../purgecss/src/*"], 13 | "exclude": ["./lib"] 14 | } -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "declarationDir": "./.temp/", 7 | "baseUrl": ".", 8 | "paths": { 9 | "purgecss": ["../purgecss/src"], 10 | } 11 | }, 12 | "include": ["./src/**/*", "./../purgecss/src/*"], 13 | "exclude": ["./lib"] 14 | } -------------------------------------------------------------------------------- /scripts/verify-commit.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | const message = fs.readFileSync(process.env.HUSKY_GIT_PARAMS!, "utf-8").trim(); 4 | 5 | const commitRE = 6 | /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip)(\(.+\))?: .{1,50}/; 7 | 8 | if (!commitRE.test(message)) { 9 | console.log(); 10 | console.error(`invalid commit message format.`); 11 | process.exit(1); 12 | } 13 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/comments/ignore_comment_range.css: -------------------------------------------------------------------------------- 1 | /* purgecss start ignore */ 2 | h1 { 3 | color: blue; 4 | } 5 | 6 | h3 { 7 | color: green; 8 | } 9 | /* purgecss end ignore */ 10 | 11 | h4 { 12 | color: purple; 13 | } 14 | 15 | /* purgecss start ignore */ 16 | 17 | /* do not purge */ 18 | 19 | h5 { 20 | color: pink; 21 | } 22 | 23 | h6 { 24 | color: lightcoral; 25 | } 26 | 27 | /* purgecss end ignore */ -------------------------------------------------------------------------------- /packages/purgecss/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const IGNORE_ANNOTATION_CURRENT = "purgecss ignore current"; 2 | export const IGNORE_ANNOTATION_NEXT = "purgecss ignore"; 3 | export const IGNORE_ANNOTATION_START = "purgecss start ignore"; 4 | export const IGNORE_ANNOTATION_END = "purgecss end ignore"; 5 | export const CONFIG_FILENAME = "purgecss.config.js"; 6 | 7 | // Error Message 8 | export const ERROR_CONFIG_FILE_LOADING = "Error loading the config file"; 9 | -------------------------------------------------------------------------------- /packages/purgecss/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "declarationDir": ".temp/", 7 | "baseUrl": ".", 8 | "paths": { 9 | "purgecss-from-html": ["../purgecss-from-html/src"], 10 | } 11 | }, 12 | "include": ["src", "package.json", "__tests__"], 13 | "exclude": ["lib"] 14 | } -------------------------------------------------------------------------------- /packages/purgecss-from-tsx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "declarationDir": "./.temp/", 7 | "baseUrl": ".", 8 | "rootDir": "./../../", 9 | "paths": { 10 | "@fullhuman/purgecss-from-jsx": ["../purgecss-from-jsx/src"] 11 | } 12 | }, 13 | "include": ["src", "../purgecss-from-jsx/src", "./../../types/**/*"], 14 | "exclude": ["./lib"] 15 | } -------------------------------------------------------------------------------- /packages/gulp-purgecss/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "declarationDir": "./.temp/", 7 | "baseUrl": ".", 8 | "paths": { 9 | "purgecss": ["../purgecss/src"], 10 | }, 11 | "typeRoots": ["./../../types", "./../../node_modules/@types"] 12 | }, 13 | "include": ["src", "./../purgecss/src/*", "./../../types/**/*"], 14 | "exclude": ["./lib"] 15 | } -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple-with-exclusion/expected/legacy.css: -------------------------------------------------------------------------------- 1 | /*!******************************************************************************!*\ 2 | !*** css ../../../../../node_modules/css-loader/dist/cjs.js!./src/style.css ***! 3 | \******************************************************************************/ 4 | .legacy-unused-class { 5 | color: red; 6 | } 7 | 8 | .unused-too { 9 | color: blue; 10 | } 11 | 12 | .hello { 13 | color: red; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/blocklist.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: blue; 3 | } 4 | 5 | .random { 6 | color: green; 7 | } 8 | 9 | #yep { 10 | color: red; 11 | } 12 | 13 | button { 14 | color: rebeccapurple; 15 | } 16 | 17 | .nav-blue { 18 | background-color: blue; 19 | } 20 | 21 | .nav-red { 22 | background-color: red; 23 | } 24 | 25 | [data-v-test] { 26 | color: green; 27 | } 28 | 29 | .nav-blue .random { 30 | color: green; 31 | } -------------------------------------------------------------------------------- /docs/.vuepress/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { getDirname, path } from "@vuepress/utils"; 2 | import { defaultTheme, DefaultThemeOptions } from "@vuepress/theme-default"; 3 | 4 | const __dirname = getDirname(import.meta.url); 5 | 6 | const localTheme = (options: DefaultThemeOptions) => { 7 | return { 8 | name: "vuepress-theme-local", 9 | extends: defaultTheme(options), 10 | // path to the client config of your theme 11 | clientConfigFile: path.resolve(__dirname, "client.ts"), 12 | }; 13 | }; 14 | 15 | export default localTheme; 16 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple-with-exclusion/expected/bundle.css: -------------------------------------------------------------------------------- 1 | /*!****************************************************************************************************!*\ 2 | !*** css ../../../../../node_modules/css-loader/dist/cjs.js!./src/style_that_we_want_to_purge.css ***! 3 | \****************************************************************************************************/ 4 | .hello { 5 | color: red; 6 | } 7 | 8 | .safelisted { 9 | color: green; 10 | } 11 | 12 | md\:w-2\/3 { 13 | color: red; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple/expected/styles.css: -------------------------------------------------------------------------------- 1 | /*!******************************************************************************!*\ 2 | !*** css ../../../../../node_modules/css-loader/dist/cjs.js!./src/style.css ***! 3 | \******************************************************************************/ 4 | .hello { 5 | color: red; 6 | } 7 | 8 | .safelisted { 9 | color: green; 10 | } 11 | 12 | md\:w-2\/3 { 13 | color: red; 14 | } 15 | 16 | 17 | /*# sourceMappingURL=styles.css.map */ 18 | /*# sourceMappingURL=styles.css.map*/ -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | reviewers: 9 | - Ffloriel 10 | ignore: 11 | - dependency-name: commander 12 | versions: 13 | - 7.0.0 14 | - 7.1.0 15 | - dependency-name: eslint 16 | versions: 17 | - 7.21.0 18 | - dependency-name: "@typescript-eslint/parser" 19 | versions: 20 | - 4.14.1 21 | - package-ecosystem: github-actions 22 | directory: "/" 23 | schedule: 24 | interval: monthly 25 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple/purgecss.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const glob = require("fast-glob"); 3 | 4 | const customExtractor = (content) => { 5 | const res = content.match(/[A-z0-9-:/]+/g) || []; 6 | return res; 7 | }; 8 | 9 | const PATHS = { 10 | src: path.join(__dirname, "src"), 11 | }; 12 | 13 | module.exports = { 14 | paths: glob.sync(`${PATHS.src}/*`), 15 | safelist: ["safelisted"], 16 | extractors: [ 17 | { 18 | extractor: customExtractor, 19 | extensions: ["html", "js"], 20 | }, 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/pseudo_selector.css: -------------------------------------------------------------------------------- 1 | .some-item:nth-child(2n + 1) { 2 | color: green; 3 | } 4 | 5 | .some-item:nth-child(2n) { 6 | font-size: 1rem; 7 | } 8 | 9 | .some-item:nth-of-type(n+3) { 10 | color: goldenrod; 11 | } 12 | 13 | .some-item:nth-of-type(-1n+6) { 14 | color: brown; 15 | } 16 | 17 | .some-item:nth-of-type(-n+6) { 18 | color: brown; 19 | } 20 | 21 | .unused:only-child() { 22 | color: red; 23 | } 24 | 25 | .used:only-child() { 26 | color: blue; 27 | } 28 | 29 | .odd-item:nth-child(odd) { 30 | color: grey; 31 | } 32 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/chaining-rules/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TODO supply a title 5 | 6 | 7 | 8 | 9 | 10 | 11 |
TODO write content
12 |
13 | 14 | Here is an a. 15 | 16 |
17 | pancakes; 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/where.css: -------------------------------------------------------------------------------- 1 | .root :where(.a, .b) .unused { 2 | color: red; 3 | } 4 | 5 | .root :where(.a, .unused) .c { 6 | color: blue; 7 | } 8 | 9 | .root :where(.a, .b) .c :where(.unused, .unused2) { 10 | color: green; 11 | } 12 | 13 | .root:where(.unused) .c { 14 | color: rebeccapurple; 15 | } 16 | 17 | .root:where(.a) .c { 18 | color: cyan; 19 | } 20 | 21 | .root :where(.unused) .c, 22 | .root :where(.a, .b) .c :where(.unused, .unused2) { 23 | display: flex; 24 | } 25 | 26 | .\[\&\:where\(\.a\)\]\:text-black:where(.a) { 27 | color: black; 28 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "sourceMap": true, 5 | "target": "es2019", 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "allowJs": false, 10 | "noUnusedLocals": true, 11 | "experimentalDecorators": true, 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "removeComments": false, 15 | "lib": ["esnext", "dom"], 16 | "types": ["jest"], 17 | "rootDir": "." 18 | }, 19 | "ts-node": { 20 | "compilerOptions": { 21 | "module": "CommonJS" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/path-and-safelist-functions/expected/styles.css: -------------------------------------------------------------------------------- 1 | /*!******************************************************************************!*\ 2 | !*** css ../../../../../node_modules/css-loader/dist/cjs.js!./src/style.css ***! 3 | \******************************************************************************/ 4 | .hello { 5 | color: red; 6 | } 7 | 8 | .safelisted { 9 | color: green; 10 | } 11 | 12 | .safelistedPattern { 13 | color: rebeccapurple; 14 | } 15 | 16 | .safelistedPatternChildren > p a { 17 | color: orange; 18 | } 19 | 20 | md\:w-2\/3 { 21 | color: red; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/media-queries/media_queries.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 600px) { 2 | div.media-class { 3 | color: black; 4 | } 5 | 6 | .alone, .unused-class { 7 | color: black; 8 | } 9 | 10 | #id-in-media { 11 | color: black; 12 | } 13 | 14 | body { 15 | color: black; 16 | } 17 | } 18 | 19 | @media (max-width: 960px){ 20 | .alone, .unused-class { 21 | color: black; 22 | } 23 | *, 24 | :before, 25 | :after { 26 | background: black; 27 | } 28 | } 29 | 30 | @media (max-width: 66666px){ 31 | .unused-class, .unused-class2 { 32 | color: black; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/purgecss-from-pug/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildRollup, 3 | createRollupConfig, 4 | extractAPI, 5 | } from "../../scripts/build"; 6 | import { promises as asyncFs } from "fs"; 7 | import * as path from "path"; 8 | 9 | (async () => { 10 | await asyncFs.rm(path.resolve(__dirname, "lib"), { 11 | recursive: true, 12 | force: true, 13 | }); 14 | const rollupConfig = createRollupConfig("purgecss-from-pug", ["pug-lexer"]); 15 | await buildRollup(rollupConfig); 16 | await extractAPI(__dirname); 17 | await asyncFs.rm(path.resolve(__dirname, "lib", ".temp"), { 18 | recursive: true, 19 | force: true, 20 | }); 21 | })(); 22 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/is.css: -------------------------------------------------------------------------------- 1 | .root :is(.a, .b) .unused { 2 | color: red; 3 | } 4 | 5 | .root :is(.a, .unused) .c { 6 | color: blue; 7 | } 8 | 9 | .root :is(.a, .b) .c :is(.unused, .unused2) { 10 | color: green; 11 | } 12 | 13 | .root:is(.unused) .c { 14 | color: rebeccapurple; 15 | } 16 | 17 | .root:is(.a) .c { 18 | color: cyan; 19 | } 20 | 21 | .root :is(.unused) .c, 22 | .root :is(.a, .b) .c :is(.unused, .unused2) { 23 | display: flex; 24 | } 25 | 26 | .\[\&\:is\(\.a\)\]\:text-black:is(.a) { 27 | color: black; 28 | } 29 | 30 | :is(.b) { 31 | color: black; 32 | } 33 | 34 | :is(.unused) { 35 | color: chartreuse; 36 | } -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-class/pseudo_selector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
    6 |
  • 7 |
  • 8 |
  • 9 |
  • 10 |
  • 11 |
  • 12 |
  • 13 |
14 |
15 |
16 |

test

17 |
18 |
    19 |
  • 1
  • 20 |
  • 2
  • 21 |
  • 3
  • 22 |
  • 4
  • 23 |
  • 5
  • 24 |
  • 6
  • 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ['Ffloriel'] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildRollup, 3 | createRollupConfig, 4 | extractAPI, 5 | } from "../../scripts/build"; 6 | import { promises as asyncFs } from "fs"; 7 | import * as path from "path"; 8 | 9 | (async () => { 10 | await asyncFs.rm(path.resolve(__dirname, "lib"), { 11 | recursive: true, 12 | force: true, 13 | }); 14 | const rollupConfig = createRollupConfig("postcss-purgecss", [ 15 | "postcss", 16 | "purgecss", 17 | "path", 18 | ]); 19 | await buildRollup(rollupConfig); 20 | await extractAPI(__dirname); 21 | await asyncFs.rm(path.resolve(__dirname, "lib", ".temp"), { 22 | recursive: true, 23 | force: true, 24 | }); 25 | })(); 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "args": [ 12 | "--runInBand" 13 | ], 14 | "cwd": "${workspaceFolder}", 15 | "console": "integratedTerminal", 16 | "internalConsoleOptions": "neverOpen", 17 | "disableOptimisticBPs": true, 18 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /packages/purgecss-from-html/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildRollup, 3 | createRollupConfig, 4 | extractAPI, 5 | } from "../../scripts/build"; 6 | import { promises as asyncFs } from "fs"; 7 | import * as path from "path"; 8 | 9 | (async () => { 10 | await asyncFs.rm(path.resolve(__dirname, "lib"), { 11 | recursive: true, 12 | force: true, 13 | }); 14 | const rollupConfig = createRollupConfig("purgecss-from-html", [ 15 | "parse5", 16 | "parse5-htmlparser2-tree-adapter", 17 | ]); 18 | await buildRollup(rollupConfig); 19 | await extractAPI(__dirname); 20 | await asyncFs.rm(path.resolve(__dirname, "lib", ".temp"), { 21 | recursive: true, 22 | force: true, 23 | }); 24 | })(); 25 | -------------------------------------------------------------------------------- /packages/purgecss-from-pug/README.md: -------------------------------------------------------------------------------- 1 | # `purgecss-from-pug` 2 | 3 | A PurgeCSS extractor for PUG files that automatically generates a list of used CSS selectors from your PUG files. This extractor can be used with PurgeCSS to eliminate unused CSS and reduce the size of your production builds. 4 | 5 | ## Usage 6 | 7 | ``` 8 | import { PurgeCSS } from 'purgecss' 9 | import { purgeCSSFromPug } from 'purgecss-from-pug' 10 | const options = { 11 | content: [], // files to extract the selectors from 12 | css: [], // css 13 | extractors: [ 14 | { 15 | extractor: purgeCSSFromPug, 16 | extensions: ['pug'] 17 | }, 18 | ] 19 | } 20 | 21 | const purgecss = await new PurgeCSS().purge(); 22 | ``` 23 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/chaining-rules/index.css: -------------------------------------------------------------------------------- 1 | .parent1 a{ 2 | color:red; 3 | } 4 | .parent1 p{ 5 | color:red; 6 | } 7 | .parent1 h1{ 8 | color:red; 9 | } 10 | .parent1.def{ 11 | color:red; 12 | } 13 | .parent1.d22222ef{ 14 | color:red; 15 | } 16 | .parent1.d222222222222222222ef{ 17 | color:red; 18 | } 19 | .parent.def1{ 20 | color:red; 21 | } 22 | .parent.def2{ 23 | color:red; 24 | } 25 | .parent.de1{ 26 | color:red; 27 | } 28 | .parent.d3ef1{ 29 | color:red; 30 | } 31 | .parent.d33ef1{ 32 | color:red; 33 | } 34 | .parent2.def{ 35 | color:red; 36 | } 37 | .parent3.def1{ 38 | color:red; 39 | } 40 | 41 | [href^='#'] { 42 | color: green; 43 | } -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/safelist_css_variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: blue; 3 | --secondary-color: indigo; 4 | --tertiary-color: aqua; 5 | --unused-color: violet; 6 | --used-color: rebeccapurple; 7 | --accent-color: orange; 8 | } 9 | 10 | .button { 11 | --button-color: var(--tertiary-color); 12 | --border-color: linear-gradient(to top, var(--secondary-color), var(--used-color, white)); 13 | 14 | background-color: var(--primary-color); 15 | color: var(--accent-color); 16 | border-color: var(--border-color); 17 | } 18 | 19 | .button:focus { 20 | background-color: var(--accent-color); 21 | color: var(--primary-color); 22 | } -------------------------------------------------------------------------------- /packages/purgecss/__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | export const ROOT_TEST_EXAMPLES = "./__tests__/test_examples/"; 4 | export const CLI_TEST_FOLDER = path.resolve( 5 | __dirname, 6 | "./test_examples/cli/simple/", 7 | ); 8 | 9 | export function findInCSS( 10 | expect: jest.Expect, 11 | selectors: string[], 12 | css: string, 13 | ): void { 14 | selectors.forEach((selector) => { 15 | expect(css.includes(selector)).toBe(true); 16 | }); 17 | } 18 | 19 | export function notFindInCSS( 20 | expect: jest.Expect, 21 | selectors: string[], 22 | css: string, 23 | ): void { 24 | selectors.forEach((selector) => { 25 | expect(css.includes(selector)).toBe(false); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildRollup, 3 | createRollupConfig, 4 | extractAPI, 5 | } from "../../scripts/build"; 6 | import { promises as asyncFs } from "fs"; 7 | import * as path from "path"; 8 | 9 | (async () => { 10 | await asyncFs.rm(path.resolve(__dirname, "lib"), { 11 | recursive: true, 12 | force: true, 13 | }); 14 | const rollupConfig = createRollupConfig("rollup-plugin-purgecss", [ 15 | "fs", 16 | "rollup-pluginutils", 17 | "purgecss", 18 | ]); 19 | await buildRollup(rollupConfig); 20 | await extractAPI(__dirname); 21 | await asyncFs.rm(path.resolve(__dirname, "lib", ".temp"), { 22 | recursive: true, 23 | force: true, 24 | }); 25 | })(); 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: Feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /packages/purgecss-from-tsx/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildRollup, 3 | createRollupConfig, 4 | extractAPI, 5 | } from "../../scripts/build"; 6 | import { promises as asyncFs } from "fs"; 7 | import * as path from "path"; 8 | 9 | (async () => { 10 | await asyncFs.rm(path.resolve(__dirname, "lib"), { 11 | recursive: true, 12 | force: true, 13 | }); 14 | const rollupConfig = createRollupConfig("purgecss-from-tsx", [ 15 | "acorn", 16 | "@fullhuman/purgecss-from-jsx", 17 | "typescript", 18 | ]); 19 | await buildRollup(rollupConfig); 20 | await extractAPI(__dirname); 21 | await asyncFs.rm(path.resolve(__dirname, "lib", ".temp"), { 22 | recursive: true, 23 | force: true, 24 | }); 25 | })(); 26 | -------------------------------------------------------------------------------- /packages/purgecss-from-jsx/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildRollup, 3 | createRollupConfig, 4 | extractAPI, 5 | } from "../../scripts/build"; 6 | import { promises as asyncFs } from "fs"; 7 | import * as path from "path"; 8 | 9 | (async () => { 10 | await asyncFs.rm(path.resolve(__dirname, "lib"), { 11 | recursive: true, 12 | force: true, 13 | }); 14 | const rollupConfig = createRollupConfig("purgecss-from-jsx", [ 15 | "acorn", 16 | "acorn-walk", 17 | "acorn-jsx", 18 | "acorn-jsx-walk", 19 | ]); 20 | await buildRollup(rollupConfig); 21 | await extractAPI(__dirname); 22 | await asyncFs.rm(path.resolve(__dirname, "lib", ".temp"), { 23 | recursive: true, 24 | force: true, 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /packages/purgecss-from-jsx/__tests__/data.ts: -------------------------------------------------------------------------------- 1 | export const TEST_1_CONTENT = ` 2 | import React from "react"; 3 | 4 | class MyComponent extends React.Component { 5 | render() { 6 | return ( 7 | 8 |
Well
9 |
10 | 11 | 12 |
13 | ); 14 | } 15 | } 16 | 17 | export default MyComponent; 18 | `; 19 | 20 | export const TEST_1_TAG = ["div", "a", "input"]; 21 | 22 | export const TEST_1_CLASS = ["test-container", "test-footer", "a-link"]; 23 | 24 | export const TEST_1_ID = ["a-link", "blo"]; 25 | -------------------------------------------------------------------------------- /packages/purgecss/src/options.ts: -------------------------------------------------------------------------------- 1 | import { ExtractorResult, Options } from "./types/"; 2 | 3 | /** 4 | * @public 5 | */ 6 | export const defaultOptions: Options = { 7 | css: [], 8 | content: [], 9 | defaultExtractor: (content: string): ExtractorResult => 10 | content.match(/[A-Za-z0-9_-]+/g) || [], 11 | extractors: [], 12 | fontFace: false, 13 | keyframes: false, 14 | rejected: false, 15 | rejectedCss: false, 16 | sourceMap: false, 17 | stdin: false, 18 | stdout: false, 19 | variables: false, 20 | safelist: { 21 | standard: [], 22 | deep: [], 23 | greedy: [], 24 | variables: [], 25 | keyframes: [], 26 | }, 27 | blocklist: [], 28 | skippedContentGlobs: [], 29 | dynamicAttributes: [], 30 | }; 31 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | This is the list of versions of PurgeCSS which are currently being supported with security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 5.0.x | :white_check_mark: | 10 | | < 5.0 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | To report a vulnerability, please report it directly on GitHub. If you are not able to report it on GitHub, 15 | you can send an email with the details to contact@full-human.com. The vulnerability report must include a 16 | proof-of-concept of the exploit, or at least a few pointers that can help us assess the risk level. 17 | Your report will be acknowledged within 2 business days. 18 | -------------------------------------------------------------------------------- /packages/gulp-purgecss/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildRollup, 3 | createRollupConfig, 4 | extractAPI, 5 | } from "../../scripts/build"; 6 | import { promises as asyncFs } from "fs"; 7 | import * as path from "path"; 8 | 9 | (async () => { 10 | await asyncFs.rm(path.resolve(__dirname, "lib"), { 11 | recursive: true, 12 | force: true, 13 | }); 14 | const rollupConfig = createRollupConfig("gulp-purgecss", [ 15 | "through2", 16 | "plugin-error", 17 | "purgecss", 18 | "glob", 19 | "vinyl-sourcemaps-apply", 20 | ]); 21 | await buildRollup(rollupConfig); 22 | await extractAPI(__dirname); 23 | await asyncFs.rm(path.resolve(__dirname, "lib", ".temp"), { 24 | recursive: true, 25 | force: true, 26 | }); 27 | })(); 28 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildRollup, 3 | createRollupConfig, 4 | extractAPI, 5 | } from "../../scripts/build"; 6 | import { promises as asyncFs } from "fs"; 7 | import * as path from "path"; 8 | 9 | (async () => { 10 | await asyncFs.rm(path.resolve(__dirname, "lib"), { 11 | recursive: true, 12 | force: true, 13 | }); 14 | const rollupConfig = createRollupConfig("purgecss-webpack-plugin", [ 15 | "fs", 16 | "path", 17 | "purgecss", 18 | "webpack", 19 | "webpack-sources", 20 | ]); 21 | await buildRollup(rollupConfig); 22 | await extractAPI(__dirname); 23 | await asyncFs.rm(path.resolve(__dirname, "lib", ".temp"), { 24 | recursive: true, 25 | force: true, 26 | }); 27 | })(); 28 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/chaining-rules.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "./../src/index"; 2 | 3 | import { ROOT_TEST_EXAMPLES, notFindInCSS } from "./utils"; 4 | 5 | describe("chaining rules", () => { 6 | let purgedCSS: string; 7 | beforeAll(async () => { 8 | const resultsPurge = await new PurgeCSS().purge({ 9 | content: [`${ROOT_TEST_EXAMPLES}chaining-rules/index.html`], 10 | css: [`${ROOT_TEST_EXAMPLES}chaining-rules/index.css`], 11 | }); 12 | purgedCSS = resultsPurge[0].css; 13 | }); 14 | it("keeps parent1 selector", () => { 15 | expect(purgedCSS.includes("parent1")).toBe(true); 16 | }); 17 | it("removes parent3, d33ef1, .parent2", () => { 18 | notFindInCSS(expect, ["parent3", "d33ef1", "parent2"], purgedCSS); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/keyframes/index.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes rotateAni { 2 | from { 3 | transform: rotate(0deg); 4 | } 5 | to { 6 | transform: rotate(359deg); 7 | } 8 | } 9 | @keyframes rotateAni { 10 | from { 11 | transform: rotate(0deg); 12 | } 13 | to { 14 | transform: rotate(359deg); 15 | } 16 | } 17 | 18 | .rotate { 19 | animation: rotateAni 200ms ease-out both; 20 | } 21 | 22 | @-webkit-keyframes flashAni 23 | { 24 | from, 50%, to { 25 | opacity: 1; 26 | } 27 | 28 | 25%, 75% { 29 | opacity: 0; 30 | } 31 | } 32 | @keyframes flashAni 33 | { 34 | from, 50%, to { 35 | opacity: 1; 36 | } 37 | 38 | 25%, 75% { 39 | opacity: 0; 40 | } 41 | } 42 | 43 | .flash { 44 | animation-name: flashAni; 45 | } -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v10 11 | with: 12 | stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 13 | stale-pr-message: 'This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 14 | days-before-stale: 180 15 | days-before-close: 5 16 | exempt-all-issue-assignees: true 17 | exempt-all-issue-milestones: true 18 | exempt-issue-labels: bug -------------------------------------------------------------------------------- /packages/postcss-purgecss/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UserDefinedOptions as PurgeCSSUserDefinedOptions, 3 | RawContent, 4 | } from "purgecss"; 5 | 6 | export { 7 | UserDefinedOptions as PurgeCSSUserDefinedOptions, 8 | RawContent, 9 | UserDefinedSafelist, 10 | ComplexSafelist, 11 | ExtractorFunction, 12 | ExtractorResult, 13 | StringRegExpArray, 14 | RawCSS, 15 | Extractors, 16 | ExtractorResultDetailed, 17 | } from "purgecss"; 18 | 19 | /** 20 | * {@inheritDoc purgecss#UserDefinedOptions} 21 | * 22 | * @public 23 | */ 24 | export interface UserDefinedOptions 25 | extends Omit { 26 | content?: PurgeCSSUserDefinedOptions["content"]; 27 | contentFunction?: (sourceFile: string) => Array; 28 | } 29 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | RawContent, 3 | UserDefinedOptions as PurgeCSSUserDefinedOptions, 4 | } from "purgecss"; 5 | 6 | /** 7 | * @public 8 | */ 9 | export type OutputFunction = (css: string, styles: string[]) => void; 10 | 11 | /** 12 | * {@inheritDoc purgecss#UserDefinedOptions} 13 | * 14 | * @public 15 | */ 16 | export type UserDefinedOptions = Omit< 17 | PurgeCSSUserDefinedOptions, 18 | "css" | "output" | "rejectedCss" 19 | > & { 20 | contentFunction?: (sourceFile: string) => Array; 21 | output: PurgeCSSUserDefinedOptions["output"] | OutputFunction | boolean; 22 | insert?: boolean; 23 | include?: string | RegExp | (string | RegExp)[]; 24 | exclude?: string | RegExp | (string | RegExp)[]; 25 | dest?: string; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/vue-cli-plugin-purgecss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fullhuman/vue-cli-plugin-purgecss", 3 | "version": "7.0.2", 4 | "description": "vue-cli plugin to add PurgeCSS", 5 | "author": "Ffloriel", 6 | "homepage": "https://purgecss.com", 7 | "main": "index.js", 8 | "keywords": [ 9 | "vue-cli", 10 | "vue-cli-3", 11 | "purgecss", 12 | "plugin", 13 | "remove", 14 | "unused", 15 | "css", 16 | "optimization" 17 | ], 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/FullHuman/purgecss.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/FullHuman/purgecss/issues" 25 | }, 26 | "publishConfig": { 27 | "access": "public", 28 | "registry": "https://registry.npmjs.org/" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from "ts-jest"; 2 | 3 | const config: JestConfigWithTsJest = { 4 | preset: "ts-jest", 5 | coverageDirectory: "coverage", 6 | coverageReporters: ["html", "lcov", "text"], 7 | collectCoverageFrom: ["/src/**/*.ts"], 8 | moduleFileExtensions: ["ts", "tsx", "js", "json"], 9 | moduleNameMapper: { 10 | "^purgecss-from-html$": "/../purgecss-from-html/src", 11 | }, 12 | testMatch: ["/__tests__/**/*test.ts"], 13 | transform: { 14 | "^.+.tsx?$": ["ts-jest", {}], 15 | }, 16 | }; 17 | 18 | export function createConfig( 19 | rootDir: string, 20 | displayName: string, 21 | ): JestConfigWithTsJest { 22 | return { 23 | ...config, 24 | rootDir, 25 | displayName, 26 | }; 27 | } 28 | 29 | export default config; 30 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/build.ts: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import { buildRollup, createRollupConfig } from "../../scripts/build"; 3 | import { promises as asyncFs } from "fs"; 4 | import * as path from "path"; 5 | import { RollupOptions } from "rollup"; 6 | 7 | (async () => { 8 | await asyncFs.rm(path.resolve(__dirname, "tasks"), { 9 | recursive: true, 10 | force: true, 11 | }); 12 | const rollupConfig: RollupOptions = { 13 | ...createRollupConfig("grunt-purgecss", ["purgecss"]), 14 | output: [ 15 | { 16 | exports: "auto", 17 | file: "./tasks/purgecss.js", 18 | format: "cjs", 19 | }, 20 | ], 21 | plugins: [ 22 | typescript({ 23 | outDir: "./tasks", 24 | }), 25 | ], 26 | }; 27 | await buildRollup(rollupConfig); 28 | })(); 29 | -------------------------------------------------------------------------------- /packages/purgecss-with-wordpress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purgecss-with-wordpress", 3 | "version": "7.0.2", 4 | "description": "PurgeCSS with wordpress", 5 | "author": "Ffloriel", 6 | "homepage": "https://purgecss.com", 7 | "keywords": [ 8 | "wordpress", 9 | "optimize", 10 | "optimization", 11 | "remove", 12 | "unused", 13 | "css", 14 | "html", 15 | "rules", 16 | "purge", 17 | "uncss", 18 | "purify" 19 | ], 20 | "license": "MIT", 21 | "main": "index.js", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/FullHuman/purgecss.git" 25 | }, 26 | "scripts": {}, 27 | "bugs": { 28 | "url": "https://github.com/FullHuman/purgecss/issues" 29 | }, 30 | "publishConfig": { 31 | "access": "public", 32 | "registry": "https://registry.npmjs.org/" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/purgecss-from-tsx/src/index.ts: -------------------------------------------------------------------------------- 1 | import purgeFromJsx from "@fullhuman/purgecss-from-jsx"; 2 | import acorn from "acorn"; 3 | import * as ts from "typescript"; 4 | 5 | /** 6 | * Create function to extract selectors from tsx code 7 | * 8 | * @param options - acorn options 9 | * @returns the function to extract selectors from tsx code 10 | * 11 | * @public 12 | */ 13 | function purgeFromTsx(options?: { 14 | acornOptions?: acorn.Options; 15 | tsOptions?: ts.CompilerOptions; 16 | }): (content: string) => string[] { 17 | return (content: string): string[] => { 18 | return purgeFromJsx(options?.acornOptions)( 19 | ts.transpileModule(content, { 20 | compilerOptions: { 21 | jsx: ts.JsxEmit.Preserve, 22 | ...options?.tsOptions, 23 | }, 24 | }).outputText, 25 | ); 26 | }; 27 | } 28 | 29 | export default purgeFromTsx; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Before you open an issue, please check if a similar issue already exists or has been closed before. 2 | 3 | ### When reporting a bug, please try to include the following: 4 | - [ ] A descriptive title 5 | - [ ] An *isolated* way to reproduce the behavior (example: GitHub repository with code isolated to the issue that anyone can clone to observe the problem) 6 | - [ ] What package and version you're using, and the platform(s) you're running it on 7 | - [ ] The behavior you expect to see, and the actual behavior 8 | 9 | ### When you open an issue for a feature request, please add as much detail as possible: 10 | - [ ] A descriptive title 11 | - [ ] A description of the problem you're trying to solve, including *why* you think this is a problem 12 | - [ ] An overview of the suggested solution 13 | - [ ] If the feature changes current behavior, reasons why your solution is better 14 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/skipped-content.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "./../src/index"; 2 | 3 | import { ROOT_TEST_EXAMPLES } from "./utils"; 4 | 5 | describe("skipped-content", () => { 6 | let purgedCSS: string; 7 | 8 | beforeAll(async () => { 9 | const resultsPurge = await new PurgeCSS().purge({ 10 | content: [`${ROOT_TEST_EXAMPLES}skipped-content/**/*.html`], 11 | css: [`${ROOT_TEST_EXAMPLES}skipped-content/simple.css`], 12 | skippedContentGlobs: [ 13 | `${ROOT_TEST_EXAMPLES}skipped-content/skippedFolder/**`, 14 | ], 15 | }); 16 | purgedCSS = resultsPurge[0].css; 17 | }); 18 | 19 | it("purges appropriate CSS rules when skippedContentGlobs is set", () => { 20 | expect(purgedCSS.includes(".red")).toBe(true); 21 | expect(purgedCSS.includes(".black")).toBe(true); 22 | expect(purgedCSS.includes(".green")).toBe(false); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = grunt => { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | // Configuration to be run (and then tested). 6 | purgecss: { 7 | simple: { 8 | options: { 9 | content: ['./__tests__/fixtures/src/simple/**/*.html'] 10 | }, 11 | files: { 12 | '__tests__/tmp/menu.css': ['__tests__/fixtures/src/menu.css'], 13 | '__tests__/tmp/profile.css': ['__tests__/fixtures/src/profile.css'], 14 | '__tests__/tmp/footer.css': ['__tests__/fixtures/src/footer.css'], 15 | '__tests__/tmp/simple.css': ['__tests__/fixtures/src/simple/simple.css'] 16 | } 17 | } 18 | } 19 | }); 20 | 21 | // Actually load this plugin's task(s). 22 | grunt.loadTasks('tasks'); 23 | 24 | // By default, lint and run all tests. 25 | grunt.registerTask('default', ['purgecss']); 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple/webpack.config.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 2 | const { PurgeCSSPlugin } = require("../../../src/"); 3 | 4 | module.exports = { 5 | mode: "development", 6 | devtool: "source-map", 7 | optimization: { 8 | splitChunks: { 9 | cacheGroups: { 10 | styles: { 11 | name: "styles", 12 | type: "css/mini-extract", 13 | test: /\.css$/, 14 | chunks: "all", 15 | enforce: true, 16 | }, 17 | }, 18 | }, 19 | }, 20 | entry: "./src/index.js", 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.css$/, 25 | use: [MiniCssExtractPlugin.loader, "css-loader"], 26 | }, 27 | ], 28 | }, 29 | plugins: [ 30 | new MiniCssExtractPlugin({ 31 | filename: "[name].css", 32 | }), 33 | new PurgeCSSPlugin({}), 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /packages/purgecss-from-pug/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purgecss-from-pug", 3 | "version": "7.0.2", 4 | "description": "Pug extractor for PurgeCSS", 5 | "author": "Ffloriel", 6 | "homepage": "https://github.com/FullHuman/purgecss#readme", 7 | "license": "ISC", 8 | "main": "lib/purgecss-from-pug.js", 9 | "directories": { 10 | "lib": "lib", 11 | "test": "__tests__" 12 | }, 13 | "files": [ 14 | "lib" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/FullHuman/purgecss.git" 19 | }, 20 | "scripts": { 21 | "build": "ts-node build.ts", 22 | "test": "jest" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/FullHuman/purgecss/issues" 26 | }, 27 | "devDependencies": { 28 | "pug-lexer": "^5.0.0", 29 | "pug-parser": "^6.0.0" 30 | }, 31 | "publishConfig": { 32 | "access": "public", 33 | "registry": "https://registry.npmjs.org/" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/globs.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "../src/"; 2 | import { ROOT_TEST_EXAMPLES } from "./utils"; 3 | 4 | describe("Glob", () => { 5 | it("glob expressions in content/css work", async () => { 6 | const resultsPurge = await new PurgeCSS().purge({ 7 | content: [`${ROOT_TEST_EXAMPLES}comments/**/*.{js,html,json,svg}`], 8 | css: [`${ROOT_TEST_EXAMPLES}comments/**/*.css`], 9 | }); 10 | 11 | expect(resultsPurge[0].file).toBe( 12 | `${ROOT_TEST_EXAMPLES}comments/ignore_comment.css`, 13 | ); 14 | expect(resultsPurge[0].css.includes("/* purgecss ignore */")).toBe(false); 15 | expect(resultsPurge[0].css.includes("/* purgecss ignore current */")).toBe( 16 | false, 17 | ); 18 | 19 | expect(resultsPurge[1].file).toBe( 20 | `${ROOT_TEST_EXAMPLES}comments/ignore_comment_range.css`, 21 | ); 22 | expect(resultsPurge[1].css.includes("h4")).toBe(false); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build / Test 2 | permissions: 3 | contents: read 4 | 5 | on: [push, pull_request] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [20.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v6 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v6 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: npm install, lerna bootstrap, run build and test 22 | run: | 23 | npm i -g npm 24 | npm i 25 | npm run build --if-present 26 | npm run lint 27 | npm run test -- -- --coverage 28 | - name: Coveralls GitHub Action 29 | uses: coverallsapp/github-action@v2.3.7 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | base-path: ./packages/purgecss 33 | env: 34 | CI: true 35 | 36 | -------------------------------------------------------------------------------- /packages/purgecss-from-html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purgecss-from-html", 3 | "version": "7.0.2", 4 | "description": "HTML extractor for PurgeCSS", 5 | "author": "Ffloriel", 6 | "homepage": "https://github.com/FullHuman/purgecss#readme", 7 | "license": "ISC", 8 | "main": "lib/purgecss-from-html.js", 9 | "directories": { 10 | "lib": "lib", 11 | "test": "__tests__" 12 | }, 13 | "files": [ 14 | "lib" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/FullHuman/purgecss.git" 19 | }, 20 | "scripts": { 21 | "build": "ts-node build.ts", 22 | "test": "jest" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/FullHuman/purgecss/issues" 26 | }, 27 | "dependencies": { 28 | "parse5": "^7.1.2", 29 | "parse5-htmlparser2-tree-adapter": "^7.0.0" 30 | }, 31 | "publishConfig": { 32 | "access": "public", 33 | "registry": "https://registry.npmjs.org/" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/cli/cli-console-output.test.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "commander"; 2 | import { parseCommandOptions, run } from "../../src/bin"; 3 | import { CLI_TEST_FOLDER } from "../utils"; 4 | 5 | describe("PurgeCSS CLI console output", () => { 6 | const program = parseCommandOptions(new Command()); 7 | 8 | it("should log the result if output is not specified", async () => { 9 | const originalConsoleLog = console.log; 10 | console.log = jest.fn(); 11 | program.parse([ 12 | "purgecss", 13 | "", 14 | "--content", 15 | `${CLI_TEST_FOLDER}/src/content.html`, 16 | `${CLI_TEST_FOLDER}/src/*.js`, 17 | "--css", 18 | `${CLI_TEST_FOLDER}/src/style.css`, 19 | ]); 20 | await run(program); 21 | expect(console.log).toHaveBeenCalledWith( 22 | `[{"css":".hello {\\n color: red;\\n}\\n","file":"${CLI_TEST_FOLDER}/src/style.css"}]`, 23 | ); 24 | console.log = originalConsoleLog; 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { rollup } from "rollup"; 4 | import purgecss from "./../src/"; 5 | 6 | describe("rollup-plugin-purgecss", () => { 7 | it("remove unused css", async () => { 8 | const bundle = await rollup({ 9 | input: path.resolve(__dirname, "fixtures/basic/index.js"), 10 | plugins: [ 11 | purgecss({ 12 | content: [path.resolve(__dirname, "assets/test_a.html")], 13 | output: path.resolve(__dirname, "assets/actual_a.css"), 14 | }), 15 | ], 16 | }); 17 | await bundle.generate({ format: "cjs", exports: "auto" }); 18 | 19 | const actualA = fs 20 | .readFileSync(path.resolve(__dirname, "assets/actual_a.css")) 21 | .toString(); 22 | const expectA = fs 23 | .readFileSync(path.resolve(__dirname, "assets/expect_a.css")) 24 | .toString(); 25 | expect(actualA).toEqual(expectA); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/font-faces.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "./../src/index"; 2 | import { ROOT_TEST_EXAMPLES } from "./utils"; 3 | 4 | describe("purge unused font-face", () => { 5 | let purgedCSS: string; 6 | beforeAll(async () => { 7 | const resultPurge = await new PurgeCSS().purge({ 8 | content: [`${ROOT_TEST_EXAMPLES}font-faces/font_face.html`], 9 | css: [`${ROOT_TEST_EXAMPLES}font-faces/font_face.css`], 10 | fontFace: true, 11 | }); 12 | purgedCSS = resultPurge[0].css; 13 | }); 14 | it("keep @font-face 'Cerebri Bold'", () => { 15 | expect( 16 | purgedCSS.includes(`src: url('../fonts/CerebriSans-Bold.eot?')`), 17 | ).toBe(true); 18 | }); 19 | it("keep @font-face 'Cerebri Sans'", () => { 20 | expect( 21 | purgedCSS.includes(`src: url('../fonts/CerebriSans-Regular.eot?')`), 22 | ).toBe(true); 23 | }); 24 | it("remove @font-face 'OtherFont'", () => { 25 | expect(purgedCSS.includes(`src: url('xxx')`)).toBe(false); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/delimited.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "./../src/index"; 2 | import { ROOT_TEST_EXAMPLES } from "./utils"; 3 | 4 | describe("delimited", () => { 5 | let purgedCSS: string; 6 | beforeAll(async () => { 7 | const resultPurge = await new PurgeCSS().purge({ 8 | content: [`${ROOT_TEST_EXAMPLES}delimited/delimited.html`], 9 | css: [`${ROOT_TEST_EXAMPLES}delimited/delimited.css`], 10 | }); 11 | purgedCSS = resultPurge[0].css; 12 | }); 13 | it("removes the extra comma", () => { 14 | const commaCount = purgedCSS 15 | .split("") 16 | .reduce((total, chr) => (chr === "," ? total + 1 : total), 0); 17 | 18 | expect(commaCount).toBe(0); 19 | }); 20 | 21 | it("finds h1", () => { 22 | expect(purgedCSS.includes("h1")).toBe(true); 23 | }); 24 | 25 | it("removes p", () => { 26 | expect(purgedCSS.includes("p")).toBe(false); 27 | }); 28 | 29 | it("removes .unused-class-name", () => { 30 | expect(purgedCSS.includes(".unused-class-name")).toBe(false); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/performance.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "../src/index"; 2 | 3 | describe("performance", () => { 4 | it("should not suffer from tons of content and css", function () { 5 | const start = Date.now(); 6 | return new PurgeCSS() 7 | .purge({ 8 | // Use all of the .js files in node_modules to purge all of the .css 9 | // files in __tests__/test_examples, including tailwind.css, which is 10 | // a whopping 908KB of CSS before purging. 11 | content: ["./node_modules/**/*.js"], 12 | css: ["./__tests__/test_examples/*/*.css"], 13 | }) 14 | .then((results) => { 15 | expect(results.length).toBeGreaterThanOrEqual(1); 16 | expect( 17 | results.some((result) => { 18 | return result.file && result.file.endsWith("/tailwind.css"); 19 | }), 20 | ).toBe(true); 21 | results.forEach((result) => expect(typeof result.css).toBe("string")); 22 | console.log("performance test took", Date.now() - start, "ms"); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/purgecss-from-tsx/__tests__/data.ts: -------------------------------------------------------------------------------- 1 | export const TEST_1_CONTENT = ` 2 | import PropTypes from "prop-types"; 3 | import React from "react"; 4 | 5 | type MyComponentProps = { 6 | login: any; 7 | }; 8 | 9 | type MyComponentState = { 10 | username: string; 11 | password: string; 12 | }; 13 | 14 | class MyComponent extends React.Component { 15 | static propTypes: any; 16 | 17 | render() { 18 | return ( 19 | 20 |
Well
21 |
22 | 23 | 24 |
25 | ); 26 | } 27 | } 28 | 29 | MyComponent.propTypes = { 30 | login: PropTypes.func.isRequired 31 | }; 32 | 33 | export default MyComponent; 34 | `; 35 | 36 | export const TEST_1_TAG = ["div", "a", "input"]; 37 | 38 | export const TEST_1_CLASS = ["test-container", "test-footer", "a-link"]; 39 | 40 | export const TEST_1_ID = ["a-link", "blo"]; 41 | -------------------------------------------------------------------------------- /packages/purgecss-from-tsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fullhuman/purgecss-from-tsx", 3 | "version": "7.0.2", 4 | "description": "TSX extractor for PurgeCSS", 5 | "author": "Ffloriel", 6 | "homepage": "https://github.com/FullHuman/purgecss#readme", 7 | "license": "ISC", 8 | "main": "./lib/purgecss-from-tsx.js", 9 | "module": "./lib/purgecss-from-tsx.esm.js", 10 | "types": "./lib/purgecss-from-tsx.d.ts", 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/FullHuman/purgecss.git" 21 | }, 22 | "scripts": { 23 | "build": "ts-node build.ts", 24 | "test": "jest" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/FullHuman/purgecss/issues" 28 | }, 29 | "dependencies": { 30 | "@fullhuman/purgecss-from-jsx": "^7.0.2", 31 | "acorn": "^8.7.0", 32 | "typescript": "^5.6.2" 33 | }, 34 | "publishConfig": { 35 | "access": "public", 36 | "registry": "https://registry.npmjs.org/" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/keyframes/keyframes.css: -------------------------------------------------------------------------------- 1 | @keyframes bounce { 2 | from, 20%, 53%, 80%, to { 3 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 1.000); 4 | transform: translate3d(1, 1, 0); 5 | } 6 | } 7 | 8 | .bounce { 9 | -webkit-animation-name: bounce; 10 | animation-name: bounce; 11 | -webkit-transform-origin: center bottom; 12 | transform-origin: center bottom; 13 | } 14 | 15 | @keyframes flash { 16 | from, 50%, to { 17 | opacity: 1; 18 | } 19 | 20 | 25%, 75% { 21 | opacity: 0.5; 22 | } 23 | } 24 | 25 | .flash { 26 | animation: flash 27 | } 28 | 29 | @keyframes scale { 30 | from { 31 | transform: scale(1); 32 | } 33 | 34 | to { 35 | transform: scale(2); 36 | } 37 | } 38 | 39 | @keyframes spin { 40 | from { 41 | transform: rotate(0deg); 42 | } 43 | 44 | to { 45 | transform: rotate(360deg); 46 | } 47 | } 48 | 49 | .scale-spin { 50 | animation: spin 300ms linear infinite forwards,scale 300ms linear infinite alternate; 51 | } 52 | -------------------------------------------------------------------------------- /packages/purgecss-with-wordpress/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | safelist: [ 3 | "rtl", 4 | "home", 5 | "blog", 6 | "archive", 7 | "date", 8 | "error404", 9 | "logged-in", 10 | "admin-bar", 11 | "no-customize-support", 12 | "custom-background", 13 | "wp-custom-logo", 14 | "alignnone", 15 | "alignright", 16 | "alignleft", 17 | "alignwide", 18 | "alignfull", 19 | "wp-caption", 20 | "wp-caption-text", 21 | "screen-reader-text", 22 | "comment-list", 23 | "wp-social-link", 24 | /^search(-.*)?$/, 25 | /^(.*)-template(-.*)?$/, 26 | /^(.*)?-?single(-.*)?$/, 27 | /^postid-(.*)?$/, 28 | /^attachmentid-(.*)?$/, 29 | /^attachment(-.*)?$/, 30 | /^page(-.*)?$/, 31 | /^(post-type-)?archive(-.*)?$/, 32 | /^author(-.*)?$/, 33 | /^category(-.*)?$/, 34 | /^tag(-.*)?$/, 35 | /^tax-(.*)?$/, 36 | /^term-(.*)?$/, 37 | /^(.*)?-?paged(-.*)?$/, 38 | /^wp-block-(.*)?$/, 39 | /^has-(.*)?$/, 40 | /^is-(.*)?$/, 41 | /^wp-embed-(.*)?$/, 42 | /^blocks-gallery-(.*)?$/, 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /packages/purgecss-from-jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fullhuman/purgecss-from-jsx", 3 | "version": "7.0.2", 4 | "description": "JSX extractor for PurgeCSS", 5 | "author": "Ffloriel", 6 | "homepage": "https://github.com/FullHuman/purgecss#readme", 7 | "license": "MIT", 8 | "main": "lib/purgecss-from-jsx.js", 9 | "module": "lib/purgecss-from-jsx.esm.js", 10 | "types": "lib/purgecss-from-jsx.d.ts", 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/FullHuman/purgecss.git" 21 | }, 22 | "scripts": { 23 | "build": "ts-node build.ts", 24 | "test": "jest" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/FullHuman/purgecss/issues" 28 | }, 29 | "dependencies": { 30 | "acorn": "^8.7.0", 31 | "acorn-jsx": "^5.3.1", 32 | "acorn-jsx-walk": "^2.0.0", 33 | "acorn-walk": "^8.2.0" 34 | }, 35 | "publishConfig": { 36 | "access": "public", 37 | "registry": "https://registry.npmjs.org/" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/purgecss-from-pug/__tests__/data.ts: -------------------------------------------------------------------------------- 1 | export const TEST_1_CONTENT = ` 2 | html 3 | head 4 | title It's just a test 5 | body 6 | div(class="test-container") Well 7 | div(class="test-footer" id="an-id") I see a div 8 | a(class="a-link" id="a-link" href="#") and a link 9 | div(class="first-class second-class") This div has two classes 10 | input#blo.enabled(type="text" disabled) 11 | `; 12 | 13 | export const TEST_1_TAG = [ 14 | "html", 15 | "head", 16 | "title", 17 | "body", 18 | "div", 19 | "a", 20 | "input", 21 | ]; 22 | 23 | export const TEST_1_CLASS = [ 24 | "test-container", 25 | "test-footer", 26 | "a-link", 27 | "first-class", 28 | "second-class", 29 | "enabled", 30 | ]; 31 | 32 | export const TEST_1_ID = ["a-link", "blo"]; 33 | 34 | export const TEST_1_ATTRIBUTE_NAMES = ["class", "id", "type", "disabled"]; 35 | 36 | export const TEST_1_ATTRIBUTE_VALUES = [ 37 | "test-container", 38 | "test-footer", 39 | "a-link", 40 | "first-class", 41 | "second-class", 42 | "enabled", 43 | "blo", 44 | "text", 45 | "true", 46 | "false", 47 | ]; 48 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/sourcemap.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "../src/"; 2 | import { ROOT_TEST_EXAMPLES } from "./utils"; 3 | 4 | describe("source map option", () => { 5 | it("contains the source map inlined in the CSS file", async () => { 6 | const resultsPurge = await new PurgeCSS().purge({ 7 | content: [`${ROOT_TEST_EXAMPLES}others/remove_unused.js`], 8 | css: [`${ROOT_TEST_EXAMPLES}others/remove_unused.css`], 9 | sourceMap: true, 10 | }); 11 | 12 | expect(resultsPurge[0].css).toContain( 13 | "sourceMappingURL=data:application/json;base64", 14 | ); 15 | }); 16 | 17 | it("contains the source map separately when setting inline to false", async () => { 18 | const resultsPurge = await new PurgeCSS().purge({ 19 | content: [`${ROOT_TEST_EXAMPLES}others/remove_unused.js`], 20 | css: [`${ROOT_TEST_EXAMPLES}others/remove_unused.css`], 21 | sourceMap: { 22 | inline: false, 23 | }, 24 | }); 25 | 26 | expect(resultsPurge[0].sourceMap).toContain( 27 | 'sources":["__tests__/test_examples/others/remove_unused.css"]', 28 | ); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Full Human 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 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fullhuman/postcss-purgecss", 3 | "version": "7.0.2", 4 | "description": "PostCSS plugin for PurgeCSS", 5 | "author": "FoundrySH ", 6 | "homepage": "https://purgecss.com", 7 | "license": "MIT", 8 | "main": "lib/postcss-purgecss.js", 9 | "module": "lib/postcss-purgecss.esm.js", 10 | "types": "lib/postcss-purgecss.d.ts", 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/FullHuman/purgecss.git" 21 | }, 22 | "scripts": { 23 | "build": "ts-node build.ts", 24 | "test": "jest" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/FullHuman/purgecss/issues" 28 | }, 29 | "dependencies": { 30 | "purgecss": "^7.0.2" 31 | }, 32 | "devDependencies": { 33 | "postcss": "^8.4.47" 34 | }, 35 | "peerDependencies": { 36 | "postcss": "^8.0.0" 37 | }, 38 | "publishConfig": { 39 | "access": "public", 40 | "registry": "https://registry.npmjs.org/" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup-plugin-purgecss", 3 | "version": "7.0.2", 4 | "description": "Rollup plugin for purgecss", 5 | "main": "lib/rollup-plugin-purgecss.js", 6 | "module": "./lib/rollup-plugin-purgecss.es.js", 7 | "jsnext:main": "./lib/rollup-plugin-purgecss.es.js", 8 | "directories": { 9 | "lib": "lib", 10 | "test": "__tests__" 11 | }, 12 | "scripts": { 13 | "build": "ts-node build.ts", 14 | "test": "jest" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/FullHuman/purgecss.git" 19 | }, 20 | "keywords": [ 21 | "rollup-plugin", 22 | "purgecss", 23 | "remove", 24 | "unused", 25 | "css" 26 | ], 27 | "author": "Ffloriel", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/FullHuman/purgecss/issues" 31 | }, 32 | "homepage": "https://purgecss.com", 33 | "dependencies": { 34 | "purgecss": "^5.0.0", 35 | "rollup-pluginutils": "^2.8.0" 36 | }, 37 | "publishConfig": { 38 | "access": "public", 39 | "registry": "https://registry.npmjs.org/" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-purgecss", 3 | "version": "7.0.2", 4 | "description": "Grunt plugin for PurgeCSS", 5 | "author": "Ffloriel", 6 | "homepage": "https://purgecss.com", 7 | "keywords": [ 8 | "optimize", 9 | "optimization", 10 | "remove", 11 | "unused", 12 | "css", 13 | "html", 14 | "rules", 15 | "purge", 16 | "uncss", 17 | "purify" 18 | ], 19 | "license": "MIT", 20 | "types": "./tasks/purgecss.d.ts", 21 | "files": [ 22 | "tasks" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/FullHuman/purgecss.git", 27 | "directory": "packages/grunt-purgecss" 28 | }, 29 | "scripts": { 30 | "build": "ts-node build.ts", 31 | "test": "jest" 32 | }, 33 | "dependencies": { 34 | "purgecss": "^5.0.0" 35 | }, 36 | "devDependencies": { 37 | "@types/grunt": "^0.4.25", 38 | "grunt": "~1.6.1" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/FullHuman/purgecss/issues" 42 | }, 43 | "publishConfig": { 44 | "access": "public", 45 | "registry": "https://registry.npmjs.org/" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/vue-cli-plugin-purgecss/generator/templates/postcss.config.js: -------------------------------------------------------------------------------- 1 | const IN_PRODUCTION = process.env.NODE_ENV === "production"; 2 | 3 | module.exports = { 4 | plugins: [ 5 | IN_PRODUCTION && 6 | require("@fullhuman/postcss-purgecss")({ 7 | content: [`./public/**/*.html`, `./src/**/*.vue`], 8 | defaultExtractor(content) { 9 | let previous; 10 | let contentWithoutStyleBlocks = content; 11 | do { 12 | previous = contentWithoutStyleBlocks; 13 | contentWithoutStyleBlocks = contentWithoutStyleBlocks.replace( 14 | //gi, 15 | "" 16 | ); 17 | } while (contentWithoutStyleBlocks !== previous); 18 | return ( 19 | contentWithoutStyleBlocks.match( 20 | /[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g 21 | ) || [] 22 | ); 23 | }, 24 | safelist: [ 25 | /-(leave|enter|appear)(|-(to|from|active))$/, 26 | /^(?!(|.*?:)cursor-move).+-move$/, 27 | /^router-link(|-exact)-active$/, 28 | /data-v-.*/, 29 | ], 30 | }), 31 | ], 32 | }; 33 | -------------------------------------------------------------------------------- /packages/purgecss-with-wordpress/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Full Human 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 | -------------------------------------------------------------------------------- /packages/gulp-purgecss/__tests__/buffer.test.ts: -------------------------------------------------------------------------------- 1 | import gulpPurgecss from "../src/"; 2 | import File from "vinyl"; 3 | import internal from "stream"; 4 | 5 | describe("gulp-purgecss with buffer", () => { 6 | let myGulpPurgecss: internal.Transform; 7 | let fileTest: File.BufferFile; 8 | beforeEach(() => { 9 | fileTest = new File({ 10 | contents: Buffer.from(".unused, .used, a { color: blue; }", "utf-8"), 11 | }); 12 | 13 | myGulpPurgecss = gulpPurgecss({ 14 | content: ["./__tests__/test.html"], 15 | }); 16 | }); 17 | 18 | it("returns a buffer", (done) => { 19 | myGulpPurgecss.write(fileTest); 20 | myGulpPurgecss.once("data", (file) => { 21 | expect(file.isBuffer()).toBe(true); 22 | done(); 23 | }); 24 | }); 25 | 26 | it("returns a purified css buffer", (done) => { 27 | myGulpPurgecss.write(fileTest); 28 | myGulpPurgecss.once("data", (file) => { 29 | const result = file.contents.toString("utf8"); 30 | expect(result.includes("used")).toBe(true); 31 | expect(result.includes("unused")).toBe(false); 32 | expect(result.includes("a")).toBe(true); 33 | done(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/media-queries.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "./../src/index"; 2 | import { ROOT_TEST_EXAMPLES } from "./utils"; 3 | 4 | describe("media queries", () => { 5 | let purgecssResult: string; 6 | beforeAll(async () => { 7 | const purgecss = await new PurgeCSS().purge({ 8 | content: [`${ROOT_TEST_EXAMPLES}media-queries/media_queries.html`], 9 | css: [`${ROOT_TEST_EXAMPLES}media-queries/media_queries.css`], 10 | }); 11 | purgecssResult = purgecss[0].css; 12 | }); 13 | it("finds .media-class", () => { 14 | expect(purgecssResult.includes(".media-class")).toBe(true); 15 | }); 16 | 17 | it("finds .alone", () => { 18 | expect(purgecssResult.includes(".alone")).toBe(true); 19 | }); 20 | 21 | it("finds #id-in-media", () => { 22 | expect(purgecssResult.includes("#id-in-media")).toBe(true); 23 | }); 24 | 25 | it("finds body", () => { 26 | expect(purgecssResult.includes("body")).toBe(true); 27 | }); 28 | 29 | it("removes .unused-class", () => { 30 | expect(purgecssResult.includes(".unused-class")).toBe(false); 31 | }); 32 | 33 | it("removes the empty media query", () => { 34 | expect(purgecssResult.includes("66666px")).toBe(false); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/font-faces/font_face.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Cerebri Sans'; 3 | font-weight: 400; 4 | font-style: normal; 5 | src: url('../fonts/CerebriSans-Regular.eot?') format('eot'), url('../fonts/CerebriSans-Regular.otf') format('opentype'), url('../fonts/CerebriSans-Regular.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Regular.ttf') format('truetype'), url('../fonts/CerebriSans-Regular.woff') format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Cerebri Bold'; 10 | font-weight: 400; 11 | font-style: normal; 12 | src: url('../fonts/CerebriSans-Bold.eot?') format('eot'), url('../fonts/CerebriSans-Bold.otf') format('opentype'), url('../fonts/CerebriSans-Bold.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Bold.ttf') format('truetype'), url('../fonts/CerebriSans-Bold.woff') format('woff'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'OtherFont'; 17 | font-weight: 400; 18 | font-style: normal; 19 | src: url('xxx') 20 | } 21 | 22 | .unused { 23 | color: black; 24 | } 25 | 26 | .used { 27 | color: red; 28 | font-family: 'Cerebri Sans'; 29 | } 30 | 31 | .used2 { 32 | color: blue; 33 | font-family: Cerebri Bold, serif; 34 | } 35 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | 5 | describe("Purgecss grunt plugin", () => { 6 | const cwd = process.cwd(); 7 | 8 | beforeAll(() => { 9 | process.chdir(__dirname); 10 | execSync("npx grunt"); 11 | }); 12 | 13 | function emptyFolder(directory: string) { 14 | fs.readdir(directory, (err, files) => { 15 | if (err) throw err; 16 | 17 | for (const file of files) { 18 | fs.unlink(path.join(directory, file), (err) => { 19 | if (err) throw err; 20 | }); 21 | } 22 | }); 23 | } 24 | 25 | afterAll(() => { 26 | emptyFolder(`${__dirname}/tmp`); 27 | process.chdir(cwd); 28 | }); 29 | 30 | const files = ["simple.css", "footer.css", "menu.css", "profile.css"]; 31 | for (const file of files) { 32 | it(`remove unused css successfully: ${file}`, () => { 33 | const actual = fs.readFileSync(`${__dirname}/tmp/${file}`).toString(); 34 | const expected = fs 35 | .readFileSync(`${__dirname}/fixtures/expected/${file}`) 36 | .toString(); 37 | expect(actual.replace(/\s/g, "")).toBe(expected.replace(/\s/g, "")); 38 | }); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/pseudo-elements.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "./../src/index"; 2 | import { ROOT_TEST_EXAMPLES } from "./utils"; 3 | 4 | describe("pseudo elements", () => { 5 | let purgedCSS: string; 6 | beforeAll(async () => { 7 | const resultsPurge = await new PurgeCSS().purge({ 8 | content: [`${ROOT_TEST_EXAMPLES}pseudo-elements/pseudo-elements.html`], 9 | css: [`${ROOT_TEST_EXAMPLES}pseudo-elements/pseudo-elements.css`], 10 | }); 11 | purgedCSS = resultsPurge[0].css; 12 | }); 13 | it("finds root pseudo-elements", () => { 14 | expect(purgedCSS.includes("::-webkit-file-upload-button")).toBe(true); 15 | expect(purgedCSS.includes("::grammar-error")).toBe(true); 16 | expect(purgedCSS.includes("::-webkit-datetime-edit-fields-wrapper")).toBe( 17 | true, 18 | ); 19 | expect(purgedCSS.includes("::-moz-focus-inner")).toBe(true); 20 | expect(purgedCSS.includes("::file-selector-button")).toBe(true); 21 | }); 22 | 23 | it("finds pseudo-elements on used class", () => { 24 | expect(purgedCSS.includes(".used::grammar-error")).toBe(true); 25 | }); 26 | 27 | it("removes pseudo-elements on unused class", () => { 28 | expect(purgedCSS.includes(".unused::grammar-error")).toBe(false); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/safelist/safelist_keyframes.css: -------------------------------------------------------------------------------- 1 | @keyframes bounce { 2 | from, 20%, 53%, 80%, to { 3 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 1.000); 4 | transform: translate3d(1, 1, 0); 5 | } 6 | } 7 | 8 | .bounce { 9 | -webkit-animation-name: bounce; 10 | animation-name: bounce; 11 | -webkit-transform-origin: center bottom; 12 | transform-origin: center bottom; 13 | } 14 | 15 | @keyframes flash { 16 | from, 50%, to { 17 | opacity: 1; 18 | } 19 | 20 | 25%, 75% { 21 | opacity: 0.5; 22 | } 23 | } 24 | 25 | .flash { 26 | animation: flash 27 | } 28 | 29 | @keyframes scale { 30 | from { 31 | transform: scale(1); 32 | } 33 | 34 | to { 35 | transform: scale(2); 36 | } 37 | } 38 | 39 | @keyframes scale-down { 40 | from { 41 | transform: scale(2); 42 | } 43 | 44 | to { 45 | transform: scale(1); 46 | } 47 | } 48 | 49 | @keyframes spin { 50 | from { 51 | transform: rotate(0deg); 52 | } 53 | 54 | to { 55 | transform: rotate(360deg); 56 | } 57 | } 58 | 59 | .scale-spin { 60 | animation: spin 300ms linear infinite forwards,scale 300ms linear infinite alternate; 61 | } 62 | -------------------------------------------------------------------------------- /packages/purgecss-from-tsx/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import purgeTsx from "../src/index"; 2 | 3 | import { TEST_1_CONTENT, TEST_1_TAG, TEST_1_CLASS, TEST_1_ID } from "./data"; 4 | 5 | const plugin = purgeTsx(); 6 | 7 | describe("purgeTsx", () => { 8 | describe("from a normal html document", () => { 9 | it("finds tag selectors", () => { 10 | const received = plugin(TEST_1_CONTENT); 11 | for (const item of TEST_1_TAG) { 12 | expect(received.includes(item)).toBe(true); 13 | } 14 | }); 15 | 16 | it("finds classes selectors", () => { 17 | const received = plugin(TEST_1_CONTENT); 18 | for (const item of TEST_1_CLASS) { 19 | expect(received.includes(item)).toBe(true); 20 | } 21 | }); 22 | 23 | it("finds id selectors", () => { 24 | const received = plugin(TEST_1_CONTENT); 25 | for (const item of TEST_1_ID) { 26 | expect(received.includes(item)).toBe(true); 27 | } 28 | }); 29 | 30 | it("finds all selectors", () => { 31 | const received = plugin(TEST_1_CONTENT); 32 | const selectors = [...TEST_1_TAG, ...TEST_1_CLASS, ...TEST_1_ID]; 33 | for (const item of selectors) { 34 | expect(received.includes(item)).toBe(true); 35 | } 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsdoc from "eslint-plugin-tsdoc"; 3 | import globals from "globals"; 4 | import tsParser from "@typescript-eslint/parser"; 5 | import path from "node:path"; 6 | import { fileURLToPath } from "node:url"; 7 | import js from "@eslint/js"; 8 | import { FlatCompat } from "@eslint/eslintrc"; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all 16 | }); 17 | 18 | export default [...compat.extends( 19 | "eslint:recommended", 20 | "plugin:@typescript-eslint/eslint-recommended", 21 | "plugin:@typescript-eslint/recommended", 22 | ).map(config => ({ 23 | ...config, 24 | files: ["**/*.ts"], 25 | })), { 26 | files: ["**/*.ts"], 27 | 28 | plugins: { 29 | "@typescript-eslint": typescriptEslint, 30 | tsdoc, 31 | }, 32 | 33 | languageOptions: { 34 | globals: { 35 | ...globals.jest, 36 | ...globals.node, 37 | }, 38 | 39 | parser: tsParser, 40 | }, 41 | 42 | rules: { 43 | "tsdoc/syntax": "warn", 44 | }, 45 | }]; -------------------------------------------------------------------------------- /packages/purgecss/__tests__/cli/cli-file-output.test.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "commander"; 2 | import { promises as asyncFs } from "fs"; 3 | import * as path from "path"; 4 | import { parseCommandOptions, run } from "../../src/bin"; 5 | import { CLI_TEST_FOLDER } from "../utils"; 6 | 7 | describe("PurgeCSS CLI file output", () => { 8 | const program = parseCommandOptions(new Command()); 9 | 10 | beforeAll(async () => { 11 | const tempFolder = path.resolve(CLI_TEST_FOLDER, ".temp"); 12 | try { 13 | await asyncFs.access(tempFolder); 14 | } catch { 15 | await asyncFs.mkdir(tempFolder, { recursive: true }); 16 | } 17 | }); 18 | 19 | it("should output the result into a file if there's one result", async () => { 20 | program.parse([ 21 | "purgecss", 22 | "", 23 | "--content", 24 | `${CLI_TEST_FOLDER}/src/content.html`, 25 | `${CLI_TEST_FOLDER}/src/*.js`, 26 | "--css", 27 | `${CLI_TEST_FOLDER}/src/style.css`, 28 | "--output", 29 | `${CLI_TEST_FOLDER}/.temp/output-style.css`, 30 | ]); 31 | await run(program); 32 | const actual = ( 33 | await asyncFs.readFile(`${CLI_TEST_FOLDER}/.temp/output-style.css`) 34 | ).toString(); 35 | expect(actual).toBe(".hello {\n color: red;\n}\n"); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/simple-with-exclusion/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const glob = require("fast-glob"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const { PurgeCSSPlugin } = require("../../../src/"); 5 | 6 | const customExtractor = (content) => content.match(/[A-z0-9-:/]+/g) || []; 7 | 8 | const PATHS = { 9 | src: path.join(__dirname, "src"), 10 | }; 11 | 12 | module.exports = { 13 | mode: "development", 14 | devtool: false, 15 | entry: { 16 | bundle: "./src/index.js", 17 | legacy: "./src/legacy.js", 18 | }, 19 | optimization: { 20 | splitChunks: { chunks: "all" }, 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.css$/, 26 | use: [MiniCssExtractPlugin.loader, "css-loader"], 27 | }, 28 | ], 29 | }, 30 | plugins: [ 31 | new MiniCssExtractPlugin({ 32 | filename: "[name].css", 33 | }), 34 | new PurgeCSSPlugin({ 35 | paths: glob.sync(`${PATHS.src}/*`), 36 | styleExtensions: [".css"], 37 | safelist: ["safelisted"], 38 | only: ["bundle"], 39 | extractors: [ 40 | { 41 | extractor: customExtractor, 42 | extensions: ["html", "js"], 43 | }, 44 | ], 45 | }), 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | 14 | ## Checklist 15 | 16 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 17 | 18 | - [ ] Lint and unit tests pass locally with my changes 19 | - [ ] I have added tests that prove my fix is effective or that my feature works 20 | - [ ] I have added necessary documentation (if appropriate) 21 | 22 | ## Further comments 23 | 24 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 25 | -------------------------------------------------------------------------------- /packages/purgecss-from-jsx/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import purgeJsx from "../src/index"; 2 | 3 | import { TEST_1_CONTENT, TEST_1_TAG, TEST_1_CLASS, TEST_1_ID } from "./data"; 4 | 5 | const plugin = purgeJsx({ 6 | ecmaVersion: "latest", 7 | sourceType: "module", 8 | }); 9 | 10 | describe("purgePug", () => { 11 | describe("from a normal html document", () => { 12 | it("finds tag selectors", () => { 13 | const received = plugin(TEST_1_CONTENT); 14 | for (const item of TEST_1_TAG) { 15 | expect(received.includes(item)).toBe(true); 16 | } 17 | }); 18 | 19 | it("finds classes selectors", () => { 20 | const received = plugin(TEST_1_CONTENT); 21 | for (const item of TEST_1_CLASS) { 22 | expect(received.includes(item)).toBe(true); 23 | } 24 | }); 25 | 26 | it("finds id selectors", () => { 27 | const received = plugin(TEST_1_CONTENT); 28 | for (const item of TEST_1_ID) { 29 | expect(received.includes(item)).toBe(true); 30 | } 31 | }); 32 | 33 | it("finds all selectors", () => { 34 | const received = plugin(TEST_1_CONTENT); 35 | const selectors = [...TEST_1_TAG, ...TEST_1_CLASS, ...TEST_1_ID]; 36 | for (const item of selectors) { 37 | expect(received.includes(item)).toBe(true); 38 | } 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/purgecss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purgecss", 3 | "version": "7.0.2", 4 | "description": "Remove unused css selectors", 5 | "author": "Ffloriel", 6 | "homepage": "https://purgecss.com", 7 | "keywords": [ 8 | "optimize", 9 | "optimization", 10 | "remove", 11 | "unused", 12 | "css", 13 | "html", 14 | "rules", 15 | "purge", 16 | "uncss", 17 | "purify" 18 | ], 19 | "license": "MIT", 20 | "main": "lib/purgecss.js", 21 | "module": "./lib/purgecss.esm.js", 22 | "types": "./lib/purgecss.d.ts", 23 | "bin": { 24 | "purgecss": "bin/purgecss.js" 25 | }, 26 | "directories": { 27 | "lib": "lib", 28 | "test": "__tests__" 29 | }, 30 | "files": [ 31 | "bin", 32 | "lib" 33 | ], 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/FullHuman/purgecss.git" 37 | }, 38 | "scripts": { 39 | "build": "ts-node build.ts", 40 | "test": "jest" 41 | }, 42 | "dependencies": { 43 | "commander": "^12.1.0", 44 | "fast-glob": "^3.3.2", 45 | "postcss": "^8.4.47", 46 | "postcss-selector-parser": "^7.0.0" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/FullHuman/purgecss/issues" 50 | }, 51 | "publishConfig": { 52 | "access": "public", 53 | "registry": "https://registry.npmjs.org/" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ComplexSafelist, 3 | StringRegExpArray, 4 | UserDefinedOptions as PurgeCSSUserDefinedOptions, 5 | } from "purgecss"; 6 | 7 | export { 8 | ComplexSafelist, 9 | StringRegExpArray, 10 | UserDefinedOptions as PurgeCSSUserDefinedOptions, 11 | RawContent, 12 | RawCSS, 13 | ExtractorFunction, 14 | Extractors, 15 | ExtractorResult, 16 | ExtractorResultDetailed, 17 | UserDefinedSafelist, 18 | } from "purgecss"; 19 | 20 | /** 21 | * @public 22 | */ 23 | export type PathFunction = () => string[]; 24 | /** 25 | * @public 26 | */ 27 | export type SafelistFunction = () => ComplexSafelist; 28 | /** 29 | * @public 30 | */ 31 | export type BlocklistFunction = () => StringRegExpArray; 32 | 33 | /** 34 | * @public 35 | */ 36 | export type PurgedStats = { 37 | [index: string]: string[]; 38 | }; 39 | 40 | /** 41 | * @public 42 | */ 43 | export type UserDefinedOptions = Omit< 44 | PurgeCSSUserDefinedOptions, 45 | "css" | "content" | "safelist" | "blocklist" | "sourceMap" 46 | > & { 47 | paths: string[] | PathFunction; 48 | moduleExtensions?: string[]; 49 | verbose?: boolean; 50 | safelist: PurgeCSSUserDefinedOptions["safelist"] | SafelistFunction; 51 | blocklist: PurgeCSSUserDefinedOptions["blocklist"] | BlocklistFunction; 52 | only?: string[]; 53 | }; 54 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/css-variables.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "./../src/index"; 2 | import { ROOT_TEST_EXAMPLES } from "./utils"; 3 | 4 | describe("purge unused css variables", () => { 5 | let purgedCSS: string; 6 | beforeAll(async () => { 7 | const resultPurge = await new PurgeCSS().purge({ 8 | content: [`${ROOT_TEST_EXAMPLES}css-variables/variables.html`], 9 | css: [`${ROOT_TEST_EXAMPLES}css-variables/variables.css`], 10 | variables: true, 11 | }); 12 | purgedCSS = resultPurge[0].css; 13 | }); 14 | it("keeps '--primary-color'", () => { 15 | expect(purgedCSS.includes("--primary-color:")).toBe(true); 16 | }); 17 | it("keeps '--accent-color', '--used-color'", () => { 18 | expect(purgedCSS.includes("--accent-color:")).toBe(true); 19 | expect(purgedCSS.includes("--used-color:")).toBe(true); 20 | }); 21 | it("removes '--tertiary-color', '--unused-color' and '--button-color'", () => { 22 | expect(purgedCSS.includes("--tertiary-color")).toBe(false); 23 | expect(purgedCSS.includes("--unused-color")).toBe(false); 24 | expect(purgedCSS.includes("--button-color")).toBe(false); 25 | }); 26 | it("keeps '--color-first:', '--wrong-order'", () => { 27 | expect(purgedCSS.includes("--color-first:")).toBe(true); 28 | expect(purgedCSS.includes("--wrong-order:")).toBe(true); 29 | }); 30 | it("keeps '--outline-color'", () => { 31 | expect(purgedCSS.includes("--outline-color:")).toBe(true); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/README.md: -------------------------------------------------------------------------------- 1 | # rollup-plugin-purgecss 2 | ![David](https://img.shields.io/david/FullHuman/purgecss?path=packages%2Frollup-plugin-purgecss&style=for-the-badge) 3 | ![David](https://img.shields.io/david/dev/FullHuman/purgecss?path=packages%2Frollup-plugin-purgecss&style=for-the-badge) 4 | ![Dependabot](https://img.shields.io/badge/dependabot-enabled-%23024ea4?style=for-the-badge) 5 | ![npm](https://img.shields.io/npm/v/rollup-plugin-purgecss?style=for-the-badge) 6 | ![npm](https://img.shields.io/npm/dw/rollup-plugin-purgecss?style=for-the-badge) 7 | ![GitHub](https://img.shields.io/github/license/FullHuman/purgecss?style=for-the-badge) 8 | 9 | [Rollup](https://github.com/rollup/rollup) plugin to remove unused css. 10 | 11 | ## Install 12 | 13 | ```sh 14 | npm i rollup-plugin-purgecss -D 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```js 20 | import { rollup } from 'rollup'; 21 | import purgecss from 'rollup-plugin-purgecss'; 22 | 23 | rollup({ 24 | entry: 'main.js', 25 | plugins: [ 26 | purgecss({ 27 | content: ["index.html"] 28 | }) 29 | ] 30 | }); 31 | ``` 32 | 33 | ## Contributing 34 | 35 | Please read [CONTRIBUTING.md](./../../CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 36 | 37 | ## Versioning 38 | 39 | We use [SemVer](http://semver.org/) for versioning. 40 | 41 | ## License 42 | 43 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/api-reference/ 2 | packages/*/lib/ 3 | packages/purgecss/bin/ 4 | packages/grunt-purgecss/tasks/ 5 | docs/.vuepress/dist/ 6 | .firebase/ 7 | .firebaserc 8 | 9 | .DS_Store 10 | 11 | packages/purgecss-webpack-plugin/__tests__/js 12 | packages/grunt-purgecss/__tests__/tmp 13 | 14 | .temp 15 | .cache 16 | 17 | npm-shrinkwrap.json 18 | yarn.lock 19 | 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Runtime data 28 | pids 29 | *.pid 30 | *.seed 31 | *.pid.lock 32 | 33 | # Directory for instrumented libs generated by jscoverage/JSCover 34 | lib-cov 35 | 36 | # Coverage directory used by tools like istanbul 37 | coverage 38 | 39 | # nyc test coverage 40 | .nyc_output 41 | 42 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | bower_components 47 | 48 | # node-waf configuration 49 | .lock-wscript 50 | 51 | # Compiled binary addons (http://nodejs.org/api/addons.html) 52 | build/Release 53 | 54 | # Dependency directories 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Typescript v1 declaration files 59 | typings/ 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/raw-css-name.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "../src/index"; 2 | 3 | describe("Raw CSS optional filename", () => { 4 | it("Should return the `name` in the result's `file` property when provided for raw CSS option", async () => { 5 | return new PurgeCSS() 6 | .purge({ 7 | content: [ 8 | { raw: "

", extension: "html" }, 9 | ], 10 | css: [ 11 | { raw: "body{margin:0;}", name: "test.css" }, 12 | { raw: "h1{margin:1;}" }, 13 | ], 14 | }) 15 | .then((results) => { 16 | expect(results.length).toBe(2); 17 | expect( 18 | results.some((result) => { 19 | return result.file && result.file.endsWith("test.css"); 20 | }), 21 | ).toBe(true); 22 | results.forEach((result) => expect(typeof result.css).toBe("string")); 23 | }); 24 | }); 25 | it("Should NOT return the `name` in the result's `file` property when NOT provided for raw CSS option", async () => { 26 | return new PurgeCSS() 27 | .purge({ 28 | content: [ 29 | { raw: "

", extension: "html" }, 30 | ], 31 | css: [{ raw: "body{margin:0;}" }], 32 | }) 33 | .then((results) => { 34 | expect(results.length).toBe(1); 35 | expect(results[0].file).toBe(undefined); 36 | results.forEach((result) => expect(typeof result.css).toBe("string")); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purgecss-webpack-plugin", 3 | "version": "7.0.2", 4 | "description": "PurgeCSS plugin for webpack - Remove unused css", 5 | "author": "Ffloriel", 6 | "homepage": "https://purgecss.com", 7 | "keywords": [ 8 | "webpack-plugin", 9 | "optimize", 10 | "optimization", 11 | "remove", 12 | "unused", 13 | "css", 14 | "html", 15 | "rules", 16 | "purge", 17 | "uncss", 18 | "purify" 19 | ], 20 | "license": "MIT", 21 | "main": "lib/purgecss-webpack-plugin.js", 22 | "module": "./lib/purgecss-webpack-plugin.esm.js", 23 | "types": "./lib/purgecss-webpack-plugin.d.ts", 24 | "directories": { 25 | "lib": "lib", 26 | "test": "__tests__" 27 | }, 28 | "files": [ 29 | "lib" 30 | ], 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/FullHuman/purgecss.git" 34 | }, 35 | "scripts": { 36 | "build": "ts-node build.ts", 37 | "test": "jest" 38 | }, 39 | "dependencies": { 40 | "purgecss": "^7.0.2", 41 | "webpack": ">=5.95.0" 42 | }, 43 | "bugs": { 44 | "url": "https://github.com/FullHuman/purgecss/issues" 45 | }, 46 | "devDependencies": { 47 | "@types/webpack-sources": "^3.2.3", 48 | "css-loader": "^7.1.2", 49 | "mini-css-extract-plugin": "^2.9.1" 50 | }, 51 | "peerDependencies": { 52 | "webpack": ">=5.0.0" 53 | }, 54 | "publishConfig": { 55 | "access": "public", 56 | "registry": "https://registry.npmjs.org/" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/cli/cli-options.test.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "commander"; 2 | import { defaultOptions, Options, standardizeSafelist } from "../../src"; 3 | import { parseCommandOptions, getOptions } from "../../src/bin"; 4 | 5 | describe("PurgeCSS CLI options", () => { 6 | const program = parseCommandOptions(new Command()); 7 | 8 | it("should set the options correctly", async () => { 9 | program.parse([ 10 | "purgecss", 11 | "", 12 | "--content", 13 | `expected-content`, 14 | "--css", 15 | `expected-css`, 16 | "--font-face", 17 | "--keyframes", 18 | "--variables", 19 | "--rejected", 20 | "--rejected-css", 21 | "--safelist", 22 | "expected-safelist", 23 | "--blocklist", 24 | "expected-blocklist", 25 | "--skippedContentGlobs", 26 | "expected-skipped-content-globs", 27 | "--output", 28 | "expected-output", 29 | ]); 30 | 31 | const options = await getOptions(program); 32 | const expectedOptions: Options = { 33 | ...defaultOptions, 34 | content: ["expected-content"], 35 | css: ["expected-css"], 36 | fontFace: true, 37 | keyframes: true, 38 | output: "expected-output", 39 | rejected: true, 40 | rejectedCss: true, 41 | stdin: false, 42 | variables: true, 43 | safelist: standardizeSafelist(["expected-safelist"]), 44 | blocklist: ["expected-blocklist"], 45 | }; 46 | expect(options).toEqual(expectedOptions); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/cli/cli-multiple-files-output.test.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "commander"; 2 | import { promises as asyncFs } from "fs"; 3 | import * as path from "path"; 4 | import { parseCommandOptions, run } from "../../src/bin"; 5 | import { CLI_TEST_FOLDER } from "../utils"; 6 | 7 | describe("PurgeCSS CLI file output", () => { 8 | const program = parseCommandOptions(new Command()); 9 | 10 | beforeAll(async () => { 11 | const tempFolder = path.resolve(CLI_TEST_FOLDER, ".temp"); 12 | try { 13 | await asyncFs.access(tempFolder); 14 | } catch { 15 | await asyncFs.mkdir(tempFolder, { recursive: true }); 16 | } 17 | }); 18 | 19 | it("should output the result into a file if there's one result", async () => { 20 | program.parse([ 21 | "purgecss", 22 | "", 23 | "--content", 24 | `${CLI_TEST_FOLDER}/src/content.html`, 25 | `${CLI_TEST_FOLDER}/src/*.js`, 26 | "--css", 27 | `${CLI_TEST_FOLDER}/src/style.css`, 28 | `${CLI_TEST_FOLDER}/src/style2.css`, 29 | "--output", 30 | `${CLI_TEST_FOLDER}/.temp`, 31 | ]); 32 | await run(program); 33 | const outputStyle = ( 34 | await asyncFs.readFile(`${CLI_TEST_FOLDER}/.temp/style.css`) 35 | ).toString(); 36 | const outputStyle2 = ( 37 | await asyncFs.readFile(`${CLI_TEST_FOLDER}/.temp/style2.css`) 38 | ).toString(); 39 | expect(outputStyle).toBe(".hello {\n color: red;\n}\n"); 40 | expect(outputStyle2).toBe(".world {\n color: green;\n}\n"); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/gulp-purgecss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-purgecss", 3 | "version": "7.0.2", 4 | "description": "Gulp plugin for purgecss", 5 | "author": "Ffloriel", 6 | "homepage": "https://purgecss.com", 7 | "keywords": [ 8 | "optimize", 9 | "optimization", 10 | "remove", 11 | "unused", 12 | "css", 13 | "gulpplugin", 14 | "gulp", 15 | "plugin", 16 | "purge", 17 | "uncss" 18 | ], 19 | "license": "MIT", 20 | "main": "lib/gulp-purgecss.js", 21 | "module": "./lib/gulp-purgecss.esm.js", 22 | "types": "./lib/gulp-purgecss.d.ts", 23 | "directories": { 24 | "lib": "lib", 25 | "test": "__tests__" 26 | }, 27 | "files": [ 28 | "lib" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/FullHuman/purgecss.git" 33 | }, 34 | "scripts": { 35 | "build": "ts-node build.ts", 36 | "test": "jest" 37 | }, 38 | "dependencies": { 39 | "fast-glob": "^3.3.2", 40 | "plugin-error": "^2.0.0", 41 | "purgecss": "^7.0.2", 42 | "through2": "^4.0.1", 43 | "vinyl-sourcemaps-apply": "^0.2.1" 44 | }, 45 | "devDependencies": { 46 | "@types/event-stream": "^4.0.0", 47 | "@types/glob": "^8.0.0", 48 | "@types/through2": "^2.0.34", 49 | "@types/vinyl": "^2.0.4", 50 | "event-stream": "^4.0.1", 51 | "vinyl": "^3.0.0" 52 | }, 53 | "bugs": { 54 | "url": "https://github.com/FullHuman/purgecss/issues" 55 | }, 56 | "publishConfig": { 57 | "access": "public", 58 | "registry": "https://registry.npmjs.org/" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/gulp-purgecss/__tests__/stream.test.ts: -------------------------------------------------------------------------------- 1 | import es from "event-stream"; 2 | import internal, { PassThrough } from "stream"; 3 | import File from "vinyl"; 4 | import gulpPurgecss from "../src/"; 5 | 6 | describe("gulp-purgecss with stream", () => { 7 | let myGulpPurgecss: internal.Transform; 8 | let fileTest: File.StreamFile; 9 | let fakeStream: internal.PassThrough; 10 | 11 | beforeEach(() => { 12 | fakeStream = new PassThrough(); 13 | fakeStream.write(Buffer.from(".unused, .used,")); 14 | fakeStream.write(Buffer.from(" a { color:")); 15 | fakeStream.write(Buffer.from(" blue; }")); 16 | fakeStream.end(); 17 | fileTest = new File({ 18 | contents: fakeStream, 19 | }); 20 | myGulpPurgecss = gulpPurgecss({ 21 | content: ["./__tests__/test.html"], 22 | }); 23 | }); 24 | 25 | it("returns a stream", (done) => { 26 | expect.assertions(1); 27 | myGulpPurgecss.write(fileTest); 28 | myGulpPurgecss.once("data", (file) => { 29 | expect(file.isStream()).toBe(true); 30 | done(); 31 | }); 32 | }); 33 | 34 | it("returns a purged css stream", (done) => { 35 | expect.assertions(3); 36 | myGulpPurgecss.write(fileTest); 37 | myGulpPurgecss.on("data", (file: File.StreamFile) => { 38 | file.contents.pipe( 39 | es.wait((_err: unknown, data: string) => { 40 | expect(data.includes("used")).toBe(true); 41 | expect(data.includes("unused")).toBe(false); 42 | expect(data.includes("a")).toBe(true); 43 | done(); 44 | }), 45 | ); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/purgecss-from-pug/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { purgeCSSFromPug } from "../src/index"; 2 | 3 | import { 4 | TEST_1_CONTENT, 5 | TEST_1_TAG, 6 | TEST_1_CLASS, 7 | TEST_1_ID, 8 | TEST_1_ATTRIBUTE_NAMES, 9 | TEST_1_ATTRIBUTE_VALUES, 10 | } from "./data"; 11 | 12 | describe("purgeCSSFromPug", () => { 13 | describe("from a normal html document", () => { 14 | it("finds tag selectors", () => { 15 | const received = purgeCSSFromPug(TEST_1_CONTENT); 16 | for (const item of TEST_1_TAG) { 17 | expect(received.tags.includes(item)).toBe(true); 18 | } 19 | }); 20 | 21 | it("finds classes selectors", () => { 22 | const received = purgeCSSFromPug(TEST_1_CONTENT); 23 | for (const item of TEST_1_CLASS) { 24 | expect(received.classes.includes(item)).toBe(true); 25 | } 26 | }); 27 | 28 | it("finds id selectors", () => { 29 | const received = purgeCSSFromPug(TEST_1_CONTENT); 30 | for (const item of TEST_1_ID) { 31 | expect(received.ids.includes(item)).toBe(true); 32 | } 33 | }); 34 | 35 | it("finds attributes names", () => { 36 | const received = purgeCSSFromPug(TEST_1_CONTENT); 37 | for (const item of TEST_1_ATTRIBUTE_NAMES) { 38 | expect(received.attributes.names.includes(item)).toBe(true); 39 | } 40 | }); 41 | 42 | it("finds attributes values", () => { 43 | const received = purgeCSSFromPug(TEST_1_CONTENT); 44 | for (const item of TEST_1_ATTRIBUTE_VALUES) { 45 | expect(received.attributes.values.includes(item)).toBe(true); 46 | } 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/attributes/attribute_selector.css: -------------------------------------------------------------------------------- 1 | /* CSS [attribute] Selector */ 2 | input[checked] { 3 | color: red; 4 | } 5 | 6 | a[invented] { 7 | color: red; 8 | } 9 | 10 | a[target] { 11 | color:red; 12 | } 13 | 14 | /* CSS [attribute="value"] Selector */ 15 | a[target="_blank"] { 16 | background-color: yellow; 17 | } 18 | 19 | a[target="no_blank"] { 20 | background-color: yellow; 21 | } 22 | 23 | input[value=""] { 24 | background-color: yellow; 25 | } 26 | 27 | /* CSS [attribute~="value"] Selector */ 28 | input[title~="flower"] { 29 | border: 5px solid yellow; 30 | } 31 | 32 | input[title~="grass"] { 33 | border: 5px solid yellow; 34 | } 35 | 36 | /* CSS [attribute|="value"] Selector */ 37 | html[lang|="en"] { 38 | background: yellow; 39 | } 40 | 41 | html[lang|="fr"] { 42 | background: yellow; 43 | } 44 | 45 | /* CSS [attribute^="value"] Selector */ 46 | a[href^="http"] { 47 | color: green; 48 | } 49 | 50 | a[href^="ssl"] { 51 | color: green; 52 | } 53 | 54 | /* CSS [attribute$="value"] Selector */ 55 | a[href$="pdf"] { 56 | color: blue; 57 | } 58 | 59 | a[href$="jpg"] { 60 | color: blue; 61 | } 62 | 63 | a[href$="http"] { 64 | color: blue; 65 | } 66 | 67 | /* CSS [attribute*="value"] Selector */ 68 | 69 | a[title*="thin"] { 70 | border: 5px solid yellow; 71 | } 72 | 73 | a[title*="fat"] { 74 | border: 5px solid yellow; 75 | } 76 | 77 | /* CSS [attribute*="value"] Selector with spaces */ 78 | [class*=" class2"] { 79 | color: green; 80 | } 81 | [class*="class1 class2 "] { 82 | color: blue; 83 | } 84 | 85 | [aria-selected] { 86 | font-weight: 700; 87 | } 88 | -------------------------------------------------------------------------------- /packages/vue-cli-plugin-purgecss/README.md: -------------------------------------------------------------------------------- 1 | # vue-cli-plugin-purgecss 2 | 3 | > A [Vue CLI 3 Plugin](https://github.com/vuejs/vue-cli) for installing PurgeCSS 4 | 5 | ## Install 6 | 7 | If you haven't yet installed vue-cli 3, first follow the install instructions here: https://github.com/vuejs/vue-cli 8 | 9 | Generate a project using vue-cli 3.0: 10 | 11 | ```bash 12 | vue create my-app 13 | ``` 14 | 15 | Before installing the PurgeCSS plugin, make sure to commit or stash your changes in case you need to revert the changes. 16 | 17 | To install the PurgeCSS plugin simply navigate to your application folder and add PurgeCSS. 18 | 19 | ```bash 20 | cd my-app 21 | 22 | vue add purgecss 23 | ``` 24 | 25 | ### Usage 26 | 27 | Below are the PurgeCSS options set by this plugin: 28 | 29 | ```js 30 | { 31 | content: [ `./public/**/*.html`, `./src/**/*.vue` ], 32 | defaultExtractor (content) { 33 | const contentWithoutStyleBlocks = content.replace(//gi, '') 34 | return contentWithoutStyleBlocks.match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || [] 35 | }, 36 | safelist: [ /-(leave|enter|appear)(|-(to|from|active))$/, /^(?!(|.*?:)cursor-move).+-move$/, /^router-link(|-exact)-active$/, /data-v-.*/ ], 37 | } 38 | ``` 39 | 40 | ## Contributing 41 | 42 | Please read [CONTRIBUTING.md](./../../CONTRIBUTING.md) for details on our code of 43 | conduct, and the process for submitting pull requests to us. 44 | 45 | ## Versioning 46 | 47 | PurgeCSS use [SemVer](http://semver.org/) for versioning. 48 | 49 | ## License 50 | 51 | This project is licensed under the MIT License - see the [LICENSE](./../../LICENSE) file 52 | for details. 53 | -------------------------------------------------------------------------------- /packages/purgecss-with-wordpress/README.md: -------------------------------------------------------------------------------- 1 | # PurgeCSS with Wordpress 2 | 3 | Based on the [gist](https://gist.github.com/frnwtr/5647673bb15ca8893642469d3b400cba) made by @frnwtr, `purgecss-with-wordpress` is a set of templates for 4 | Wordpress CMS. 5 | 6 | ## Getting Started 7 | 8 | #### Installation 9 | 10 | You need to install [PurgeCSS](https://github.com/FullHuman/purgecss) first. 11 | 12 | Install `purgecss-with-wordpress`: 13 | ```sh 14 | npm i --save-dev purgecss-with-wordpress 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```js 20 | 21 | import PurgeCSS from 'purgecss' 22 | import purgecssWordpress from 'purgecss-with-wordpress' 23 | 24 | const purgeCSSResults = await new PurgeCSS().purge({ 25 | content: ['**/*.html'], 26 | css: ['**/*.css'], 27 | safelist: purgecssWordpress.safelist, 28 | safelistPatterns: purgecssWordpress.safelistPatterns 29 | }) 30 | ``` 31 | 32 | If you have additional classes you want to include in either of the `safelist` or `safelistPatterns`, you can include them using the spread operator: 33 | 34 | ```js 35 | safelist: [ 36 | ...purgecssWordpress.safelist, 37 | 'red', 38 | 'blue', 39 | ], 40 | safelistPatterns: [ 41 | ...purgecssWordpress.safelistPatterns, 42 | /^red/, 43 | /blue$/, 44 | ] 45 | ``` 46 | 47 | ## Versioning 48 | 49 | Purgecss-with-wordpress use [SemVer](http://semver.org/) for versioning. 50 | 51 | ## Acknowledgment 52 | 53 | Purgecss-with-wordpress is based on the [gist](https://gist.github.com/frnwtr/5647673bb15ca8893642469d3b400cba) made by @frnwtr 54 | 55 | ## License 56 | 57 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file 58 | for details. 59 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/CarbonAds.vue: -------------------------------------------------------------------------------- 1 | 4 | 34 | 35 | 70 | -------------------------------------------------------------------------------- /packages/grunt-purgecss/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS, defaultOptions, UserDefinedOptions } from "purgecss"; 2 | 3 | function getAvailableFiles( 4 | grunt: IGrunt, 5 | files: string[] | undefined = [], 6 | ): string[] { 7 | return files.filter((filepath) => { 8 | // Warn on and remove invalid source files (if nonull was set). 9 | if (!grunt.file.exists(filepath)) { 10 | grunt.log.warn(`Source file "${filepath}" not found.`); 11 | return false; 12 | } 13 | return true; 14 | }); 15 | } 16 | 17 | function gruntPurgeCSS(grunt: IGrunt): void { 18 | grunt.registerMultiTask("purgecss", "Grunt plugin for PurgeCSS", function () { 19 | const done = this.async(); 20 | const options = this.options(defaultOptions); 21 | const promisedPurgedFiles = []; 22 | for (const file of this.files) { 23 | const source = getAvailableFiles(grunt, file.src); 24 | const purgedCss = new PurgeCSS() 25 | .purge({ 26 | ...options, 27 | css: source, 28 | }) 29 | .then((purgeCSSResults) => { 30 | if (typeof file.dest === "undefined") { 31 | throw new Error(`Destination file not found`); 32 | } 33 | 34 | grunt.file.write(file.dest, purgeCSSResults[0].css); 35 | // Print a success message 36 | grunt.log.writeln(`File "${file.dest}" created.`); 37 | }); 38 | 39 | promisedPurgedFiles.push(purgedCss); 40 | } 41 | Promise.all(promisedPurgedFiles) 42 | .then(() => { 43 | done(); 44 | }) 45 | .catch(() => done(false)); 46 | }); 47 | } 48 | 49 | export default gruntPurgeCSS; 50 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/cases/path-and-safelist-functions/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const glob = require("fast-glob"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const {PurgeCSSPlugin} = require("../../../src/"); 5 | 6 | const customExtractor = (content) => content.match(/[A-z0-9-:/]+/g) || []; 7 | 8 | const PATHS = { 9 | src: path.join(__dirname, "src"), 10 | }; 11 | 12 | module.exports = { 13 | mode: "development", 14 | devtool: false, 15 | entry: "./src/index.js", 16 | context: path.resolve(__dirname), 17 | optimization: { 18 | splitChunks: { 19 | cacheGroups: { 20 | styles: { 21 | name: "styles", 22 | type: "css/mini-extract", 23 | test: /\.css$/, 24 | chunks: "all", 25 | enforce: true, 26 | }, 27 | }, 28 | }, 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.css$/, 34 | use: [MiniCssExtractPlugin.loader, "css-loader"], 35 | }, 36 | ], 37 | }, 38 | plugins: [ 39 | new MiniCssExtractPlugin({ 40 | filename: "[name].css", 41 | }), 42 | new PurgeCSSPlugin({ 43 | paths: () => glob.sync(`${PATHS.src}/*`), 44 | safelist: () => { 45 | return { 46 | standard: ["safelisted", /^safelistedPat/], 47 | deep: [/^safelistedPatternChildren/], 48 | greedy: [/^safelistedPatternGreedy/], 49 | }; 50 | }, 51 | extractors: [ 52 | { 53 | extractor: customExtractor, 54 | extensions: ["html", "js"], 55 | }, 56 | ], 57 | }), 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /packages/purgecss/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildRollup, 3 | createRollupConfig, 4 | extractAPI, 5 | } from "../../scripts/build"; 6 | import { promises as asyncFs } from "fs"; 7 | import * as path from "path"; 8 | import typescript from "@rollup/plugin-typescript"; 9 | // import { terser } from "rollup-plugin-terser"; 10 | import json from "@rollup/plugin-json"; 11 | import { RollupOptions } from "rollup"; 12 | 13 | const external = [ 14 | "postcss", 15 | "postcss-selector-parser", 16 | "glob", 17 | "path", 18 | "fs", 19 | "util", 20 | ]; 21 | 22 | const cliBundle: RollupOptions = { 23 | external: [...external, "commander"], 24 | input: "./src/bin.ts", 25 | output: { 26 | banner: "#!/usr/bin/env node", 27 | file: "./bin/purgecss.js", 28 | footer: "main();", 29 | format: "cjs", 30 | }, 31 | plugins: [ 32 | json(), 33 | typescript({ 34 | tsconfig: "./tsconfig.json", 35 | declaration: false, 36 | declarationDir: undefined, 37 | composite: false, 38 | sourceMap: false, 39 | outDir: "./bin", 40 | }), 41 | // terser(), 42 | ], 43 | }; 44 | 45 | (async () => { 46 | await asyncFs.rm(path.resolve(__dirname, "lib"), { 47 | recursive: true, 48 | force: true, 49 | }); 50 | await asyncFs.rm(path.resolve(__dirname, "bin"), { 51 | recursive: true, 52 | force: true, 53 | }); 54 | const rollupConfig = createRollupConfig("purgecss", external); 55 | await buildRollup(rollupConfig); 56 | await buildRollup(cliBundle); 57 | await extractAPI(__dirname); 58 | await asyncFs.rm(path.resolve(__dirname, "lib", ".temp"), { 59 | recursive: true, 60 | force: true, 61 | }); 62 | })(); 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## [Code of Conduct](./.github/CODE_OF_CONDUCT.md) 2 | 3 | FullHuman has adopted the Contributor Covenant Code of Conduct for all of its 4 | project. Please read the text so that you understand how to conduct while 5 | contributing to this project. 6 | 7 | ## Semantic Versioning 8 | 9 | Purgecss use [SemVer](http://semver.org/) for versioning. 10 | 11 | ## Sending a Pull Request 12 | 13 | **Before submitting a pull request,** please make sure the following is done: 14 | 15 | 1. Fork [the repository](https://github.com/FullHuman/purgecss) 16 | and create your branch from `main`. 17 | 2. If you've added code that should be tested, add tests! 18 | 3. If you've changed APIs, update the documentation. 19 | 4. Ensure the test suite passes (`npm test`). 20 | 4. Make sure your code lints (`npm run lint`). 21 | 22 | ### Development Workflow 23 | 24 | After cloning Purgecss, run `npm i && npm run bootstrap` to fetch its dependencies. Then, you can run 25 | several commands: 26 | 27 | * `npm run build` will build cjs and es module of all PurgeCSS packages in their `lib` folder. 28 | * `npm run lint` checks the code style. 29 | * `npm test` runs the complete test suite. 30 | * `npm test -- --watch` runs an interactive test watcher. 31 | * `npm test ` runs tests with matching filenames. 32 | * `npm run build` creates the cjs and es module of all Purgecss packages in their `lib` folder. 33 | 34 | Make sure that your pull request contains unit tests for any new functionality. 35 | This way we can ensure that we don't break your code in the future. 36 | 37 | ### License 38 | 39 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file 40 | for details. 41 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/comments.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "./../src/index"; 2 | import { findInCSS, ROOT_TEST_EXAMPLES } from "./utils"; 3 | 4 | describe("ignore comment", () => { 5 | let purgedCSS: string; 6 | beforeAll(async () => { 7 | const resultsPurge = await new PurgeCSS().purge({ 8 | content: [`${ROOT_TEST_EXAMPLES}comments/ignore_comment.html`], 9 | css: [`${ROOT_TEST_EXAMPLES}comments/ignore_comment.css`], 10 | }); 11 | purgedCSS = resultsPurge[0].css; 12 | }); 13 | it("ignores h1 h2", () => { 14 | expect(purgedCSS.includes("h1")).toBe(true); 15 | expect(purgedCSS.includes("h3")).toBe(true); 16 | }); 17 | 18 | it("removes the comment", () => { 19 | expect(purgedCSS.includes("/* purgecss ignore */")).toBe(false); 20 | expect(purgedCSS.includes("/* purgecss ignore current */")).toBe(false); 21 | }); 22 | }); 23 | 24 | describe("ignore comment range", () => { 25 | let purgedCSS: string; 26 | beforeAll(async () => { 27 | const resultsPurge = await new PurgeCSS().purge({ 28 | content: [`${ROOT_TEST_EXAMPLES}comments/ignore_comment_range.html`], 29 | css: [`${ROOT_TEST_EXAMPLES}comments/ignore_comment_range.css`], 30 | }); 31 | purgedCSS = resultsPurge[0].css; 32 | }); 33 | 34 | it("ignores h1, h3, h5, h6", () => { 35 | findInCSS(expect, ["h1", "h3", "h5", "h6"], purgedCSS); 36 | }); 37 | 38 | it("removes h4", () => { 39 | expect(purgedCSS.includes("h4")).toBe(false); 40 | }); 41 | 42 | it("removes the comments", () => { 43 | expect(purgedCSS.includes("/* purgecss start ignore */")).toBe(false); 44 | expect(purgedCSS.includes("/* purgecss end ignore */")).toBe(false); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/purgecss-from-html/__tests__/data.ts: -------------------------------------------------------------------------------- 1 | export const TEST_1_CONTENT = ` 2 | 3 | 4 | It's just a test 5 | 6 | 7 |
Well
8 | 9 | 10 | 11 | 12 | 13 | 14 | `; 15 | 16 | export const TEST_1_TAG = [ 17 | "html", 18 | "head", 19 | "title", 20 | "body", 21 | "div", 22 | "a", 23 | "input", 24 | ]; 25 | 26 | export const TEST_1_CLASS = ["test-container", "test-footer", "a-link"]; 27 | 28 | export const TEST_1_ID = ["a-link", "blo"]; 29 | 30 | export const TEST_1_ATTRIBUTES = { 31 | NAMES: ["href", "id", "class", "type", "disabled"], 32 | VALUES: ["#", "a-link", "a-link", "text", ...TEST_1_CLASS, ...TEST_1_ID], 33 | }; 34 | 35 | export const TEST_2_CONTENT = ` 36 | 44 | 45 | 50 | 51 | 61 | `; 62 | 63 | export const TEST_2_TAG = ["div", "a", "input"]; 64 | 65 | export const TEST_2_CLASS = ["test-container", "test-footer", "a-link"]; 66 | 67 | export const TEST_2_ID = ["a-link", "blo"]; 68 | -------------------------------------------------------------------------------- /docs/.vuepress/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 24 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/pseudo-elements/pseudo-elements.css: -------------------------------------------------------------------------------- 1 | /* Remove inner border and padding from Firefox, but don't restore the outline like Normalize. */ 2 | 3 | ::-moz-focus-inner { 4 | padding: 0; 5 | border-style: none; 6 | } 7 | 8 | 9 | /* Fix height of inputs with a type of datetime-local, date, month, week, or time 10 | See https://github.com/twbs/bootstrap/issues/18842 */ 11 | 12 | ::-webkit-datetime-edit-fields-wrapper, 13 | ::-webkit-datetime-edit-text, 14 | ::-webkit-datetime-edit-minute, 15 | ::-webkit-datetime-edit-hour-field, 16 | ::-webkit-datetime-edit-day-field, 17 | ::-webkit-datetime-edit-month-field, 18 | ::-webkit-datetime-edit-year-field { 19 | padding: 0; 20 | } 21 | 22 | ::-webkit-inner-spin-button { 23 | height: auto; 24 | } 25 | 26 | 27 | /* Remove the inner padding in Chrome and Safari on macOS. */ 28 | 29 | ::-webkit-search-decoration { 30 | -webkit-appearance: none; 31 | } 32 | 33 | /* Remove padding around color pickers in webkit browsers */ 34 | 35 | ::-webkit-color-swatch-wrapper { 36 | padding: 0; 37 | } 38 | 39 | 40 | /* Inherit font family and line height for file input buttons */ 41 | 42 | ::file-selector-button { 43 | font: inherit; 44 | } 45 | 46 | /* 1. Change font properties to `inherit` 47 | 2. Correct the inability to style clickable types in iOS and Safari. */ 48 | 49 | ::-webkit-file-upload-button { 50 | font: inherit; 51 | -webkit-appearance: button; 52 | } 53 | 54 | ::grammar-error { 55 | text-decoration: underline green; 56 | color: green; 57 | } 58 | 59 | .used::grammar-error { 60 | text-decoration: underline blue; 61 | color: blue; 62 | } 63 | 64 | .unused::grammar-error { 65 | text-decoration: underline red; 66 | color: red; 67 | } 68 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/test_examples/css-variables/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-first: var(--wrong-order); 3 | --primary-color: blue; 4 | --secondary-color: indigo; 5 | --tertiary-color: aqua; 6 | --unused-color: violet; 7 | --used-color: rebeccapurple; 8 | --accent-color: orange; 9 | --wrong-order: yellow; 10 | --outline-color: coral; 11 | --random: var(--not-existing); 12 | } 13 | 14 | .button { 15 | --button-color: var(--tertiary-color); 16 | --border-color: linear-gradient(to top, var(--secondary-color), var(--used-color, white)); 17 | 18 | background-color: var(--primary-color); 19 | color: var(--accent-color); 20 | border-color: var(--border-color); 21 | } 22 | 23 | .button, .unused-class { 24 | outline-color: var(--outline-color); 25 | } 26 | 27 | .button:focus { 28 | background-color: var(--accent-color); 29 | color: var(--primary-color); 30 | border-color: var(--color-first); 31 | } 32 | 33 | @media (min-width: 1024px) { 34 | :root { 35 | --color-first: var(--wrong-order); 36 | --primary-color: blue; 37 | --secondary-color: indigo; 38 | --tertiary-color: aqua; 39 | --unused-color: violet; 40 | --used-color: rebeccapurple; 41 | --accent-color: orange; 42 | --wrong-order: yellow; 43 | --random: var(--not-existing); 44 | } 45 | 46 | .button { 47 | --button-color: var(--tertiary-color); 48 | --border-color: linear-gradient(to top, var(--secondary-color), var(--used-color, white)); 49 | 50 | background-color: var(--primary-color); 51 | color: var(--accent-color); 52 | border-color: var(--border-color); 53 | } 54 | 55 | .button:focus { 56 | background-color: var(--accent-color); 57 | color: var(--primary-color); 58 | border-color: var(--color-first); 59 | } 60 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "devDependencies": { 8 | "@eslint/eslintrc": "^3.1.0", 9 | "@eslint/js": "^9.11.1", 10 | "@microsoft/api-documenter": "^7.25.14", 11 | "@microsoft/api-extractor": "^7.47.9", 12 | "@rollup/plugin-json": "^6.1.0", 13 | "@rollup/plugin-typescript": "^12.1.0", 14 | "@types/jest": "^29.5.13", 15 | "@types/node": "^22.7.4", 16 | "@typescript-eslint/eslint-plugin": "^8.48.1", 17 | "@typescript-eslint/parser": "^8.48.1", 18 | "@vuepress/bundler-vite": "^2.0.0-rc.26", 19 | "@vuepress/plugin-markdown-tab": "^2.0.0-rc.120", 20 | "@vuepress/plugin-search": "^2.0.0-rc.120", 21 | "@vuepress/theme-default": "2.0.0-rc.120", 22 | "conventional-changelog": "^7.1.1", 23 | "eslint": "^9.11.1", 24 | "eslint-plugin-tsdoc": "^0.4.0", 25 | "globals": "^16.5.0", 26 | "husky": "^9.1.6", 27 | "jest": "^29.7.0", 28 | "lerna": "^9.0.3", 29 | "prettier": "^3.3.3", 30 | "rollup": "^4.22.5", 31 | "sass-embedded": "^1.81.0", 32 | "ts-jest": "^29.2.5", 33 | "ts-node": "^10.9.2", 34 | "typescript": "^5.6.2", 35 | "vuepress": "2.0.0-rc.26" 36 | }, 37 | "scripts": { 38 | "build": "lerna run build", 39 | "docs:dev": "vuepress dev docs", 40 | "docs:build": "vuepress build docs", 41 | "generate-api-reference": "api-documenter markdown -i ./docs/.vuepress/.temp/api-reference/ -o ./docs/api-reference/", 42 | "generate-changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", 43 | "lint": "eslint . --fix --ignore-pattern **/lib/** --ignore-pattern **/bin/**", 44 | "prettier": "prettier --write --parser typescript '**/*.ts'", 45 | "test": "lerna run test", 46 | "prepare": "husky || true" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/expected/font-keyframes.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Cerebri Sans'; 3 | font-weight: 400; 4 | font-style: normal; 5 | src: url('../fonts/CerebriSans-Regular.eot?') format('eot'), url('../fonts/CerebriSans-Regular.otf') format('opentype'), url('../fonts/CerebriSans-Regular.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Regular.ttf') format('truetype'), url('../fonts/CerebriSans-Regular.woff') format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Cerebri Bold'; 10 | font-weight: 400; 11 | font-style: normal; 12 | src: url('../fonts/CerebriSans-Bold.eot?') format('eot'), url('../fonts/CerebriSans-Bold.otf') format('opentype'), url('../fonts/CerebriSans-Bold.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Bold.ttf') format('truetype'), url('../fonts/CerebriSans-Bold.woff') format('woff'); 13 | } 14 | 15 | .used { 16 | color: red; 17 | font-family: 'Cerebri Sans'; 18 | } 19 | 20 | .used2 { 21 | color: blue; 22 | font-family: Cerebri Bold, serif; 23 | } 24 | 25 | 26 | @keyframes bounce { 27 | from, 20%, 53%, 80%, to { 28 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 1.000); 29 | transform: translate3d(1, 1, 0); 30 | } 31 | } 32 | 33 | .bounce { 34 | -webkit-animation-name: bounce; 35 | animation-name: bounce; 36 | -webkit-transform-origin: center bottom; 37 | transform-origin: center bottom; 38 | } 39 | 40 | @keyframes scale { 41 | from { 42 | transform: scale(1); 43 | } 44 | 45 | to { 46 | transform: scale(2); 47 | } 48 | } 49 | 50 | @keyframes spin { 51 | from { 52 | transform: rotate(0deg); 53 | } 54 | 55 | to { 56 | transform: rotate(360deg); 57 | } 58 | } 59 | 60 | .scale-spin { 61 | animation: spin 300ms linear infinite forwards,scale 300ms linear infinite alternate; 62 | } 63 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/src/config-test/expected.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Cerebri Sans'; 3 | font-weight: 400; 4 | font-style: normal; 5 | src: url('../fonts/CerebriSans-Regular.eot?') format('eot'), url('../fonts/CerebriSans-Regular.otf') format('opentype'), url('../fonts/CerebriSans-Regular.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Regular.ttf') format('truetype'), url('../fonts/CerebriSans-Regular.woff') format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Cerebri Bold'; 10 | font-weight: 400; 11 | font-style: normal; 12 | src: url('../fonts/CerebriSans-Bold.eot?') format('eot'), url('../fonts/CerebriSans-Bold.otf') format('opentype'), url('../fonts/CerebriSans-Bold.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Bold.ttf') format('truetype'), url('../fonts/CerebriSans-Bold.woff') format('woff'); 13 | } 14 | 15 | .used { 16 | color: red; 17 | font-family: 'Cerebri Sans'; 18 | } 19 | 20 | .used2 { 21 | color: blue; 22 | font-family: Cerebri Bold, serif; 23 | } 24 | 25 | 26 | @keyframes bounce { 27 | from, 20%, 53%, 80%, to { 28 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 1.000); 29 | transform: translate3d(1, 1, 0); 30 | } 31 | } 32 | 33 | .bounce { 34 | -webkit-animation-name: bounce; 35 | animation-name: bounce; 36 | -webkit-transform-origin: center bottom; 37 | transform-origin: center bottom; 38 | } 39 | 40 | @keyframes scale { 41 | from { 42 | transform: scale(1); 43 | } 44 | 45 | to { 46 | transform: scale(2); 47 | } 48 | } 49 | 50 | @keyframes spin { 51 | from { 52 | transform: rotate(0deg); 53 | } 54 | 55 | to { 56 | transform: rotate(360deg); 57 | } 58 | } 59 | 60 | .scale-spin { 61 | animation: spin 300ms linear infinite forwards,scale 300ms linear infinite alternate; 62 | } 63 | -------------------------------------------------------------------------------- /packages/purgecss/__tests__/rejectedCss.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "./../src/index"; 2 | import { ROOT_TEST_EXAMPLES } from "./utils"; 3 | 4 | describe("rejectedCss", () => { 5 | it("returns the rejected css as part of the result", async () => { 6 | expect.assertions(1); 7 | const resultsPurge = await new PurgeCSS().purge({ 8 | content: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.js`], 9 | css: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.css`], 10 | rejectedCss: true, 11 | }); 12 | const expected = `.rejected {\n color: blue;\n}`; 13 | expect(resultsPurge[0].rejectedCss?.trim()).toBe(expected.trim()); 14 | }); 15 | it("contains the rejected selectors as part of the rejected css", async () => { 16 | expect.assertions(1); 17 | const resultsPurge = await new PurgeCSS().purge({ 18 | content: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.js`], 19 | css: [`${ROOT_TEST_EXAMPLES}rejectedCss/simple.css`], 20 | rejected: true, 21 | rejectedCss: true, 22 | }); 23 | expect(resultsPurge[0].rejectedCss?.trim()).toContain( 24 | resultsPurge[0].rejected?.[0], 25 | ); 26 | }); 27 | /** 28 | * https://github.com/FullHuman/purgecss/pull/763#discussion_r754618902 29 | */ 30 | it("preserves the node correctly", async () => { 31 | expect.assertions(2); 32 | const resultsPurge = await new PurgeCSS().purge({ 33 | content: [`${ROOT_TEST_EXAMPLES}rejectedCss/empty-parent-node.js`], 34 | css: [`${ROOT_TEST_EXAMPLES}rejectedCss/empty-parent-node.css`], 35 | rejectedCss: true, 36 | }); 37 | const expectedRejectedCss = `@media (max-width: 66666px) {\n .unused-class {\n color: black;\n }\n}`; 38 | const expectedPurgedCss = `@media (max-width: 66666px) {\n .used-class {\n color: black;\n }\n}`; 39 | expect(resultsPurge[0].rejectedCss?.trim()).toEqual(expectedRejectedCss); 40 | expect(resultsPurge[0].css.trim()).toEqual(expectedPurgedCss); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /docs/plugins/gulp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Gulp 3 | lang: en-US 4 | meta: 5 | - name: description 6 | content: PurgeCSS is a tool to remove unused CSS from your project. You can use it with gulp plugin. 7 | - itemprop: description 8 | content: PurgeCSS is a tool to remove unused CSS from your project. You can use it with gulp plugin. 9 | - property: og:url 10 | content: https://purgecss.com/plugins/gulp 11 | - property: og:site_name 12 | content: purgecss.com 13 | - property: og:type 14 | content: website 15 | - property: og:image 16 | content: https://i.imgur.com/UEiUiJ0.png 17 | - property: og:locale 18 | content: en_US 19 | - property: og:title 20 | content: Remove unused CSS - PurgeCSS 21 | - property: og:description 22 | content: PurgeCSS is a tool to remove unused CSS from your project. You can use it with gulp plugin. 23 | --- 24 | 25 | # Gulp 26 | 27 | ## Installation 28 | 29 | ```sh 30 | npm i -D gulp-purgecss 31 | npm install --save-dev gulp-purgecss 32 | ``` 33 | 34 | ## Usage 35 | 36 | By default, `purgecss` outputs the source CSS _with unused selectors removed_: 37 | 38 | ```js 39 | const gulp = require('gulp') 40 | const purgecss = require('gulp-purgecss') 41 | 42 | gulp.task('purgecss', () => { 43 | return gulp.src('src/**/*.css') 44 | .pipe(purgecss({ 45 | content: ['src/**/*.html'] 46 | })) 47 | .pipe(gulp.dest('build/css')) 48 | }) 49 | ``` 50 | 51 | By setting the `rejected` option, you can 'invert' the output to list _only the removed selectors_: 52 | 53 | ```js 54 | const gulp = require('gulp') 55 | const rename = require('gulp-rename') 56 | const purgecss = require('gulp-purgecss') 57 | 58 | gulp.task('purgecss-rejected', () => { 59 | return gulp.src('src/**/*.css') 60 | .pipe(rename({ 61 | suffix: '.rejected' 62 | })) 63 | .pipe(purgecss({ 64 | content: ['src/**/*.html'], 65 | rejected: true 66 | })) 67 | .pipe(gulp.dest('build/css')) 68 | }) 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/guides/wordpress.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: WordPress 3 | lang: en-US 4 | meta: 5 | - name: description 6 | content: PurgeCSS can be used for WordPress development. A module exists to ease the process and provide common safelist items. 7 | - itemprop: description 8 | content: PurgeCSS can be used for WordPress development. A module exists to ease the process and provide common safelist items. 9 | - property: og:url 10 | content: https://purgecss.com/guides/wordpress 11 | - property: og:site_name 12 | content: purgecss.com 13 | - property: og:type 14 | content: website 15 | - property: og:image 16 | content: https://i.imgur.com/UEiUiJ0.png 17 | - property: og:locale 18 | content: en_US 19 | - property: og:title 20 | content: Remove unused CSS - PurgeCSS 21 | - property: og:description 22 | content: PurgeCSS can be used for WordPress development. A module exists to ease the process and provide common safelist items. 23 | --- 24 | 25 | # WordPress 26 | 27 | If you want to use PurgeCSS with WordPress, you might need to safelist classes generated by WordPress to avoid them being removed by PurgeCSS. `purgecss-with-wordpress` contains the classes needed to be safelisted. 28 | 29 | ## Installation 30 | 31 | You need to install [purgecss](https://github.com/FullHuman/purgecss) first. 32 | 33 | Install `purgecss-with-wordpress`: 34 | ```sh 35 | npm i --save-dev purgecss-with-wordpress 36 | ``` 37 | 38 | ## Usage 39 | 40 | ```js{2,7,8} 41 | import Purgecss from 'purgecss' 42 | import purgecssWordpress from 'purgecss-with-wordpress' 43 | 44 | const purgeCss = new Purgecss({ 45 | content: ['**/*.html'], 46 | css: ['**/*.css'], 47 | safelist: purgecssWordpress.safelist 48 | }) 49 | const result = purgecss.purge() 50 | ``` 51 | 52 | If you have additional classes you want to include, you can include them using the spread operator: 53 | 54 | ```js 55 | { 56 | safelist: [ 57 | ...purgecssWordpress.safelist, 58 | 'red', 59 | 'blue', 60 | /^red/, 61 | /blue$/, 62 | ] 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | lang: en-US 4 | meta: 5 | - name: description 6 | content: PurgeCSS is a tool to remove unused CSS from your project. It can be used as part of your development workflow. PurgeCSS comes with a JavaScript API, a CLI, and plugins for popular build tools. 7 | - itemprop: description 8 | content: PurgeCSS is a tool to remove unused CSS from your project. It can be used as part of your development workflow. PurgeCSS comes with a JavaScript API, a CLI, and plugins for popular build tools. 9 | - property: og:url 10 | content: https://purgecss.com 11 | - property: og:site_name 12 | content: purgecss.com 13 | - property: og:type 14 | content: website 15 | - property: og:image 16 | content: https://i.imgur.com/UEiUiJ0.png 17 | - property: og:locale 18 | content: en_US 19 | - property: og:title 20 | content: Remove unused CSS - PurgeCSS 21 | - property: og:description 22 | content: PurgeCSS is a tool to remove unused CSS from your project. It can be used as part of your development workflow. PurgeCSS comes with a JavaScript API, a CLI, and plugins for popular build tools. 23 | --- 24 | 25 | # About PurgeCSS 26 | 27 | PurgeCSS is a tool to remove unused CSS. It can be part of your development workflow. 28 | When you are building a website, you might decide to use a CSS framework like TailwindCSS, Bootstrap, MaterializeCSS, Foundation, etc... But you will only use a small set of the framework, and a lot of unused CSS styles will be included. 29 | 30 | This is where PurgeCSS comes into play. PurgeCSS analyzes your content and your CSS files. Then it matches the selectors used in your files with the one in your content files. It removes unused selectors from your CSS, resulting in smaller CSS files. 31 | 32 | ## Sponsors 🥰 33 | 34 | [](https://www.bairesdev.com/sponsoring-open-source-projects/) 35 | [](https://full-human.health/) 36 | -------------------------------------------------------------------------------- /packages/gulp-purgecss/README.md: -------------------------------------------------------------------------------- 1 | # gulp-purgecss 2 | 3 | [![npm](https://img.shields.io/npm/v/gulp-purgecss.svg)](https://www.npmjs.com/package/gulp-purgecss) 4 | [![license](https://img.shields.io/github/license/fullhuman/gulp-purgecss.svg)]() 5 | 6 | 7 | > [gulp](http://gulpjs.com/) plugin to removed unused CSS, using [purgecss](https://github.com/FullHuman/purgecss) 8 | 9 | ## Regarding Issues 10 | 11 | This is just a simple [gulp](https://github.com/gulpjs/gulp) plugin, which means it's nothing more than a thin wrapper around `purgecss`. If it looks like you are having CSS related issues, please go to the [purgecss](https://github.com/FullHuman/purgecss/issues) repo. Only create a new issue if it looks like you're having a problem with the plugin itself. 12 | 13 | ## Install 14 | 15 | ``` 16 | npm i -D gulp-purgecss 17 | npm install --save-dev gulp-purgecss 18 | ``` 19 | 20 | ## Usage 21 | 22 | By default, `purgecss` outputs the source CSS _with unused selectors removed_: 23 | 24 | ```js 25 | const gulp = require('gulp') 26 | const purgecss = require('gulp-purgecss') 27 | 28 | gulp.task('purgecss', () => { 29 | return gulp.src('src/**/*.css') 30 | .pipe(purgecss({ 31 | content: ['src/**/*.html'] 32 | })) 33 | .pipe(gulp.dest('build/css')) 34 | }) 35 | ``` 36 | 37 | By setting the `rejected` option, you can 'invert' the output to list _only the removed selectors_: 38 | 39 | ```js 40 | const gulp = require('gulp') 41 | const rename = require('gulp-rename') 42 | const purgecss = require('gulp-purgecss') 43 | 44 | gulp.task('purgecss-rejected', () => { 45 | return gulp.src('src/**/*.css') 46 | .pipe(rename({ 47 | suffix: '.rejected' 48 | })) 49 | .pipe(purgecss({ 50 | content: ['src/**/*.html'], 51 | rejected: true 52 | })) 53 | .pipe(gulp.dest('build/css')) 54 | }) 55 | ``` 56 | 57 | ## Versioning 58 | 59 | We use [SemVer](http://semver.org/) for versioning. 60 | 61 | ## License 62 | 63 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 64 | 65 | -------------------------------------------------------------------------------- /docs/guides/vue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vue 3 | lang: en-US 4 | meta: 5 | - name: description 6 | content: PurgeCSS can be used with Vue with the webpack plugin. 7 | - itemprop: description 8 | content: PurgeCSS can be used with Vue with the webpack plugin. 9 | - property: og:url 10 | content: https://purgecss.com/guides/vue 11 | - property: og:site_name 12 | content: purgecss.com 13 | - property: og:type 14 | content: website 15 | - property: og:image 16 | content: https://i.imgur.com/UEiUiJ0.png 17 | - property: og:locale 18 | content: en_US 19 | - property: og:title 20 | content: Remove unused CSS - PurgeCSS 21 | - property: og:description 22 | content: PurgeCSS can be used with Vue with the webpack plugin. 23 | --- 24 | 25 | # Vue 26 | 27 | ## Use the vue CLI plugin 28 | 29 | ![vue cli plugin purgecss](https://i.imgur.com/ZYnJSin.png) 30 | 31 | ### Install 32 | 33 | If you haven't yet installed vue-cli 3, first follow the install instructions here: https://github.com/vuejs/vue-cli 34 | 35 | Generate a project using vue-cli 3.0: 36 | 37 | ```sh 38 | vue create my-app 39 | ``` 40 | 41 | Before installing the PurgeCSS plugin, make sure to commit or stash your changes in case you need to revert the changes. 42 | 43 | To install the PurgeCSS plugin simply navigate to your application folder and add PurgeCSS. 44 | 45 | ```sh 46 | cd my-app 47 | 48 | vue add @fullhuman/purgecss 49 | ``` 50 | 51 | The PurgeCSS plugin will generate a `postcss.config.js` file with PurgeCSS configured in it. You can then modify the PurgeCSS options. 52 | 53 | ### Usage 54 | 55 | Below are the PurgeCSS options set by this plugin: 56 | 57 | ```js 58 | { 59 | content: [ `./public/**/*.html`, `./src/**/*.vue` ], 60 | defaultExtractor (content) { 61 | const contentWithoutStyleBlocks = content.replace(//gi, '') 62 | return contentWithoutStyleBlocks.match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || [] 63 | }, 64 | safelist: [ /-(leave|enter|appear)(|-(to|from|active))$/, /^(?!(|.*?:)cursor-move).+-move$/, /^router-link(|-exact)-active$/, /data-v-.*/ ], 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | lang: en-US 4 | meta: 5 | - name: description 6 | content: PurgeCSS is a tool to remove unused CSS from your project. It can be used as part of your development workflow. PurgeCSS comes with a JavaScript API, a CLI, and plugins for popular build tools. 7 | - itemprop: description 8 | content: PurgeCSS is a tool to remove unused CSS from your project. It can be used as part of your development workflow. PurgeCSS comes with a JavaScript API, a CLI, and plugins for popular build tools. 9 | - property: og:url 10 | content: https://purgecss.com 11 | - property: og:site_name 12 | content: purgecss.com 13 | - property: og:type 14 | content: website 15 | - property: og:image 16 | content: https://i.imgur.com/UEiUiJ0.png 17 | - property: og:locale 18 | content: en_US 19 | - property: og:title 20 | content: Remove unused CSS - PurgeCSS 21 | - property: og:description 22 | content: PurgeCSS is a tool to remove unused CSS from your project. It can be used as part of your development workflow. PurgeCSS comes with a JavaScript API, a CLI, and plugins for popular build tools. 23 | --- 24 | 25 | # Getting Started 26 | 27 | Most bundlers and frameworks to build websites are using PostCSS. The easiest way to configure PurgeCSS is with its PostCSS plugin. 28 | 29 | Install the PostCSS plugin: 30 | 31 | :::: code-tabs 32 | @tab npm 33 | ```sh 34 | npm i -D @fullhuman/postcss-purgecss 35 | ``` 36 | @tab yarn 37 | ```sh 38 | yarn add @fullhuman/postcss-purgecss --dev 39 | ``` 40 | :::: 41 | 42 | and add the PurgeCSS plugin to the PostCSS configuration: 43 | 44 | ```js{1,5-7} 45 | import { purgeCSSPlugin } from '@fullhuman/postcss-purgecss'; 46 | 47 | module.exports = { 48 | plugins: [ 49 | purgecss({ 50 | content: ['./**/*.html'] 51 | }) 52 | ] 53 | } 54 | ``` 55 | 56 | PurgeCSS will remove the CSS that is not in the files specified in the `content` option. 57 | 58 | You can find more information about PostCSS plugin and the configuration options on the following pages: 59 | - [PostCSS plugin](/plugins/postcss) 60 | - [Configuration](/configuration) 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: [bug] 5 | assignees: 6 | - octocat 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! 12 | Please, search if a similar issue has not been posted before submitting a new one. 13 | - type: textarea 14 | id: describe-the-bug 15 | attributes: 16 | label: Describe the bug 17 | description: A clear and concise description of what the bug is 18 | placeholder: Unexpected behavior... 19 | value: "A bug" 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: to-reproduce 24 | attributes: 25 | label: To Reproduce 26 | description: Steps to reproduce the behavior or a minimal repository 27 | placeholder: 1. Go to '...', 2. Click on '...' 28 | value: "steps" 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: expected-behavior 33 | attributes: 34 | label: Expected Behavior 35 | description: A clear and concise description of what you expected to happen 36 | placeholder: "Expecting..." 37 | value: "expected behavior" 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: environment 42 | attributes: 43 | label: Environment 44 | description: "Add information about OS, Package, Version" 45 | placeholder: "OS: macOS, Package: postcss-purgecss, Version: 4.0.1" 46 | value: "environment" 47 | validations: 48 | required: true 49 | - type: textarea 50 | id: context 51 | attributes: 52 | label: Add any other context about the problem here 53 | description: Additional context 54 | value: "context" 55 | - type: checkboxes 56 | id: terms 57 | attributes: 58 | label: Code of Conduct 59 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/FullHuman/purgecss/blob/main/.github/CODE_OF_CONDUCT.md) 60 | options: 61 | - label: I agree to follow this project's Code of Conduct 62 | required: true -------------------------------------------------------------------------------- /packages/purgecss/__tests__/rejected.test.ts: -------------------------------------------------------------------------------- 1 | import { PurgeCSS } from "./../src/index"; 2 | import { ROOT_TEST_EXAMPLES } from "./utils"; 3 | 4 | describe("rejected", () => { 5 | it("does not return the rejected selectors if rejected set to false", async () => { 6 | expect.assertions(1); 7 | const resultsPurge = await new PurgeCSS().purge({ 8 | content: [`${ROOT_TEST_EXAMPLES}rejected/simple.js`], 9 | css: [`${ROOT_TEST_EXAMPLES}rejected/simple.css`], 10 | }); 11 | expect(resultsPurge[0].rejected).toBe(undefined); 12 | }); 13 | 14 | it("returns an empty array if no selectors are rejected", async () => { 15 | expect.assertions(1); 16 | const purgecssResult = await new PurgeCSS().purge({ 17 | content: [`${ROOT_TEST_EXAMPLES}rejected/simple.js`], 18 | css: [`${ROOT_TEST_EXAMPLES}rejected/simple.css`], 19 | rejected: true, 20 | }); 21 | expect(purgecssResult[0].rejected).toEqual([]); 22 | }); 23 | 24 | it("returns the list of rejected selectors", async () => { 25 | expect.assertions(1); 26 | const purgecssResult = await new PurgeCSS().purge({ 27 | content: [`${ROOT_TEST_EXAMPLES}others/remove_unused.js`], 28 | css: [`${ROOT_TEST_EXAMPLES}others/remove_unused.css`], 29 | rejected: true, 30 | }); 31 | expect(purgecssResult[0].rejected).toEqual([ 32 | ".unused-class", 33 | ".another-one-not-found", 34 | ]); 35 | }); 36 | 37 | it("returns the list of rejected selectors with chaining rules", async () => { 38 | expect.assertions(1); 39 | const purgecssResult = await new PurgeCSS().purge({ 40 | content: [`${ROOT_TEST_EXAMPLES}chaining-rules/index.html`], 41 | css: [`${ROOT_TEST_EXAMPLES}chaining-rules/index.css`], 42 | rejected: true, 43 | }); 44 | expect(purgecssResult[0].rejected).toEqual([ 45 | ".parent1 p", 46 | ".parent1 h1", 47 | ".parent1.d22222ef", 48 | ".parent1.d222222222222222222ef", 49 | ".parent.def1", 50 | ".parent.def2", 51 | ".parent.de1", 52 | ".parent.d3ef1", 53 | ".parent.d33ef1", 54 | ".parent2.def", 55 | ".parent3.def1", 56 | "[href^='#']", 57 | ]); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/rollup-plugin-purgecss/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { PurgeCSS } from "purgecss"; 3 | import { Plugin } from "rollup"; 4 | import { createFilter } from "rollup-pluginutils"; 5 | import { UserDefinedOptions } from "./types"; 6 | 7 | export * from "./types"; 8 | 9 | /** 10 | * 11 | * @param options - options 12 | * @returns 13 | * 14 | * @public 15 | */ 16 | function pluginPurgeCSS(options: UserDefinedOptions): Plugin { 17 | const filter = createFilter( 18 | options.include || ["**/*.css"], 19 | options.exclude || "node_modules/**", 20 | ); 21 | 22 | const styles: string[] = []; 23 | let dest = ""; 24 | 25 | return { 26 | name: "purgecss", 27 | transform: async (code, id) => { 28 | if (!filter(id)) return null; 29 | 30 | const v = await new PurgeCSS().purge({ 31 | content: options.content, 32 | css: [ 33 | { 34 | raw: code, 35 | }, 36 | ], 37 | }); 38 | let css = v[0].css; 39 | 40 | styles.push(css); 41 | 42 | css = JSON.stringify(css); 43 | if (options.insert) { 44 | // do thing 45 | } else if (!options.output) { 46 | code = css; 47 | } else { 48 | code = `"";`; 49 | } 50 | 51 | return { 52 | code: `export default ${code}`, 53 | map: { mappings: "" }, 54 | }; 55 | }, 56 | generateBundle() { 57 | if (!options.insert && (!styles.length || options.output === false)) { 58 | return; 59 | } 60 | const css = styles.reduce((acc, value) => { 61 | return acc + value; 62 | }, ""); 63 | if (typeof options.output === "string") { 64 | return fs.writeFileSync(options.output, css); 65 | } 66 | if (typeof options.output === "function") { 67 | return options.output(css, styles); 68 | } 69 | if (!options.insert && dest) { 70 | if (dest.endsWith(".js") || dest.endsWith(".ts")) { 71 | dest = dest.slice(0, -3); 72 | } 73 | dest = `${dest}.css`; 74 | return fs.writeFileSync(dest, css); 75 | } 76 | }, 77 | }; 78 | } 79 | 80 | export default pluginPurgeCSS; 81 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/src/config-test/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Cerebri Sans'; 3 | font-weight: 400; 4 | font-style: normal; 5 | src: url('../fonts/CerebriSans-Regular.eot?') format('eot'), url('../fonts/CerebriSans-Regular.otf') format('opentype'), url('../fonts/CerebriSans-Regular.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Regular.ttf') format('truetype'), url('../fonts/CerebriSans-Regular.woff') format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Cerebri Bold'; 10 | font-weight: 400; 11 | font-style: normal; 12 | src: url('../fonts/CerebriSans-Bold.eot?') format('eot'), url('../fonts/CerebriSans-Bold.otf') format('opentype'), url('../fonts/CerebriSans-Bold.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Bold.ttf') format('truetype'), url('../fonts/CerebriSans-Bold.woff') format('woff'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'OtherFont'; 17 | font-weight: 400; 18 | font-style: normal; 19 | src: url('xxx') 20 | } 21 | 22 | .unused { 23 | color: black; 24 | } 25 | 26 | .used { 27 | color: red; 28 | font-family: 'Cerebri Sans'; 29 | } 30 | 31 | .used2 { 32 | color: blue; 33 | font-family: Cerebri Bold, serif; 34 | } 35 | 36 | 37 | @keyframes bounce { 38 | from, 20%, 53%, 80%, to { 39 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 1.000); 40 | transform: translate3d(1, 1, 0); 41 | } 42 | } 43 | 44 | .bounce { 45 | -webkit-animation-name: bounce; 46 | animation-name: bounce; 47 | -webkit-transform-origin: center bottom; 48 | transform-origin: center bottom; 49 | } 50 | 51 | @keyframes flash { 52 | from, 50%, to { 53 | opacity: 1; 54 | } 55 | 56 | 25%, 75% { 57 | opacity: 0.5; 58 | } 59 | } 60 | 61 | .flash { 62 | animation: flash 63 | } 64 | 65 | @keyframes scale { 66 | from { 67 | transform: scale(1); 68 | } 69 | 70 | to { 71 | transform: scale(2); 72 | } 73 | } 74 | 75 | @keyframes spin { 76 | from { 77 | transform: rotate(0deg); 78 | } 79 | 80 | to { 81 | transform: rotate(360deg); 82 | } 83 | } 84 | 85 | .scale-spin { 86 | animation: spin 300ms linear infinite forwards,scale 300ms linear infinite alternate; 87 | } 88 | -------------------------------------------------------------------------------- /packages/postcss-purgecss/__tests__/fixtures/src/font-keyframes/font-keyframes.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Cerebri Sans'; 3 | font-weight: 400; 4 | font-style: normal; 5 | src: url('../fonts/CerebriSans-Regular.eot?') format('eot'), url('../fonts/CerebriSans-Regular.otf') format('opentype'), url('../fonts/CerebriSans-Regular.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Regular.ttf') format('truetype'), url('../fonts/CerebriSans-Regular.woff') format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Cerebri Bold'; 10 | font-weight: 400; 11 | font-style: normal; 12 | src: url('../fonts/CerebriSans-Bold.eot?') format('eot'), url('../fonts/CerebriSans-Bold.otf') format('opentype'), url('../fonts/CerebriSans-Bold.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Bold.ttf') format('truetype'), url('../fonts/CerebriSans-Bold.woff') format('woff'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'OtherFont'; 17 | font-weight: 400; 18 | font-style: normal; 19 | src: url('xxx') 20 | } 21 | 22 | .unused { 23 | color: black; 24 | } 25 | 26 | .used { 27 | color: red; 28 | font-family: 'Cerebri Sans'; 29 | } 30 | 31 | .used2 { 32 | color: blue; 33 | font-family: Cerebri Bold, serif; 34 | } 35 | 36 | 37 | @keyframes bounce { 38 | from, 20%, 53%, 80%, to { 39 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 1.000); 40 | transform: translate3d(1, 1, 0); 41 | } 42 | } 43 | 44 | .bounce { 45 | -webkit-animation-name: bounce; 46 | animation-name: bounce; 47 | -webkit-transform-origin: center bottom; 48 | transform-origin: center bottom; 49 | } 50 | 51 | @keyframes flash { 52 | from, 50%, to { 53 | opacity: 1; 54 | } 55 | 56 | 25%, 75% { 57 | opacity: 0.5; 58 | } 59 | } 60 | 61 | .flash { 62 | animation: flash 63 | } 64 | 65 | @keyframes scale { 66 | from { 67 | transform: scale(1); 68 | } 69 | 70 | to { 71 | transform: scale(2); 72 | } 73 | } 74 | 75 | @keyframes spin { 76 | from { 77 | transform: rotate(0deg); 78 | } 79 | 80 | to { 81 | transform: rotate(360deg); 82 | } 83 | } 84 | 85 | .scale-spin { 86 | animation: spin 300ms linear infinite forwards,scale 300ms linear infinite alternate; 87 | } 88 | -------------------------------------------------------------------------------- /packages/purgecss-webpack-plugin/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { promisify } from "util"; 4 | import webpack, { Configuration } from "webpack"; 5 | 6 | const asyncFs = { 7 | readdir: promisify(fs.readdir), 8 | }; 9 | 10 | function runWebpack( 11 | options: Configuration, 12 | ): Promise { 13 | const compiler = webpack(options); 14 | return new Promise((resolve, reject) => { 15 | compiler.run((err, stats) => { 16 | if (err) reject(err); 17 | if (stats?.hasErrors()) reject(new Error(stats.toString())); 18 | resolve(stats); 19 | }); 20 | }); 21 | } 22 | 23 | async function readFileOrEmpty(path: string): Promise { 24 | try { 25 | return await fs.promises.readFile(path, "utf-8"); 26 | } catch (e) { 27 | console.error(e); 28 | return ""; 29 | } 30 | } 31 | 32 | describe("Webpack integration", () => { 33 | const cwd = process.cwd(); 34 | afterAll(() => { 35 | process.chdir(cwd); 36 | }); 37 | 38 | const cases: string[] = [ 39 | "path-and-safelist-functions", 40 | "simple", 41 | "simple-with-exclusion", 42 | ]; 43 | 44 | for (const testCase of cases) { 45 | it(`works with ${testCase} configuration`, async () => { 46 | const testDirectory = path.resolve(__dirname, "cases", testCase); 47 | const outputDirectory = path.resolve(__dirname, "js", testCase); 48 | const expectedDirectory = path.resolve(testDirectory, "expected"); 49 | 50 | process.chdir(testDirectory); 51 | 52 | const webpackConfig = await import(`${testDirectory}/webpack.config.js`); 53 | 54 | await runWebpack({ 55 | ...webpackConfig.default, 56 | output: { 57 | path: outputDirectory, 58 | }, 59 | }); 60 | 61 | const files = await asyncFs.readdir(expectedDirectory); 62 | 63 | for (const file of files) { 64 | const filePath = path.join(expectedDirectory, file); 65 | const actualPath = path.join(outputDirectory, file); 66 | 67 | const actualFile = await readFileOrEmpty(actualPath); 68 | const expectedFile = await readFileOrEmpty(filePath); 69 | 70 | expect(actualFile).toBe(expectedFile); 71 | } 72 | }); 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /packages/purgecss-from-pug/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pug extractor for PurgeCSS 3 | * 4 | * A PurgeCSS extractor for PUG files that automatically generates a list of 5 | * used CSS selectors. This extractor can be used with PurgeCSS to eliminate 6 | * unused CSS and reduce the size of your production builds. 7 | * 8 | * @packageDocumentation 9 | */ 10 | 11 | import lex from "pug-lexer"; 12 | import type { ExtractorResultDetailed } from "purgecss"; 13 | 14 | const getAttributeTokenValues = (token: lex.AttributeToken): string[] => { 15 | if (typeof token.val === "string") { 16 | return (token.mustEscape ? token.val.replace(/"/g, "") : token.val).split( 17 | " ", 18 | ); 19 | } 20 | return []; 21 | }; 22 | /** 23 | * Get the potential selectors from Pug code 24 | * 25 | * @param content - Pug code 26 | * @returns the attributes, classes, ids, and tags from the Pug code 27 | * 28 | * @public 29 | */ 30 | export const purgeCSSFromPug = (content: string): ExtractorResultDetailed => { 31 | const tokens = lex(content); 32 | const extractorResult: ExtractorResultDetailed = { 33 | attributes: { 34 | // always add to attributes, to handle things like [class*=foo] 35 | names: ["class", "id"], 36 | values: ["true", "false"], 37 | }, 38 | classes: [], 39 | ids: [], 40 | tags: [], 41 | undetermined: [], 42 | }; 43 | for (const token of tokens) { 44 | if (token.type === "tag") { 45 | extractorResult.tags.push(token.val); 46 | } else if (token.type === "class") { 47 | extractorResult.classes.push(...token.val.split(" ")); 48 | extractorResult.attributes.values.push(...token.val.split(" ")); 49 | } else if (token.type === "id") { 50 | extractorResult.ids.push(token.val); 51 | extractorResult.attributes.values.push(token.val); 52 | } else if (token.type === "attribute") { 53 | const tokenValues = getAttributeTokenValues(token); 54 | if (token.name === "class") { 55 | extractorResult.classes.push(...tokenValues); 56 | } else if (token.name === "id") { 57 | extractorResult.ids.push(...tokenValues); 58 | } else { 59 | extractorResult.attributes.names.push(token.name); 60 | } 61 | extractorResult.attributes.values.push(...tokenValues); 62 | } 63 | } 64 | return extractorResult; 65 | }; 66 | -------------------------------------------------------------------------------- /packages/purgecss-from-html/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import purgehtml from "./../src/index"; 2 | 3 | import { 4 | TEST_1_CONTENT, 5 | TEST_1_TAG, 6 | TEST_1_CLASS, 7 | TEST_1_ID, 8 | TEST_1_ATTRIBUTES, 9 | } from "./data"; 10 | import { TEST_2_CONTENT, TEST_2_TAG, TEST_2_CLASS, TEST_2_ID } from "./data"; 11 | 12 | describe("purgehtml", () => { 13 | const resultTest1 = purgehtml(TEST_1_CONTENT); 14 | 15 | describe("from a normal html document", () => { 16 | it("finds tag selectors", () => { 17 | for (const item of TEST_1_TAG) { 18 | expect(resultTest1.tags.includes(item)).toBe(true); 19 | } 20 | }); 21 | 22 | it("finds classes selectors", () => { 23 | for (const item of TEST_1_CLASS) { 24 | expect(resultTest1.classes.includes(item)).toBe(true); 25 | } 26 | }); 27 | 28 | it("finds id selectors", () => { 29 | for (const item of TEST_1_ID) { 30 | expect(resultTest1.ids.includes(item)).toBe(true); 31 | } 32 | }); 33 | 34 | it("finds attributes names", () => { 35 | for (const item of TEST_1_ATTRIBUTES.NAMES) { 36 | expect(resultTest1.attributes.names.includes(item)).toBe(true); 37 | } 38 | }); 39 | 40 | it("finds attributes values", () => { 41 | for (const item of TEST_1_ATTRIBUTES.VALUES) { 42 | expect(resultTest1.attributes.values.includes(item)).toBe(true); 43 | } 44 | }); 45 | }); 46 | 47 | const resultTest2 = purgehtml(TEST_2_CONTENT); 48 | 49 | describe("from a template tag", () => { 50 | it("finds tag selectors", () => { 51 | for (const item of TEST_2_TAG) { 52 | expect(resultTest2.tags.includes(item)).toBe(true); 53 | } 54 | }); 55 | 56 | it("finds classes selectors", () => { 57 | for (const item of TEST_2_CLASS) { 58 | expect(resultTest2.classes.includes(item)).toBe(true); 59 | } 60 | }); 61 | 62 | it("finds id selectors", () => { 63 | for (const item of TEST_2_ID) { 64 | expect(resultTest2.ids.includes(item)).toBe(true); 65 | } 66 | }); 67 | 68 | it("finds all selectors", () => { 69 | const selectors = [...TEST_2_TAG, ...TEST_2_CLASS, ...TEST_2_ID]; 70 | for (const item of selectors) { 71 | expect( 72 | [ 73 | ...resultTest2.classes, 74 | ...resultTest2.ids, 75 | ...resultTest2.tags, 76 | ].includes(item), 77 | ).toBe(true); 78 | } 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Programmatic API 3 | lang: en-US 4 | meta: 5 | - name: description 6 | content: PurgeCSS is a tool for removing CSS that you're not actually using in your project. You can use its programmatic API to use it as part of your development workflow. 7 | - itemprop: description 8 | content: PurgeCSS is a tool for removing CSS that you're not actually using in your project. You can use its programmatic API to use it as part of your development workflow. 9 | - property: og:url 10 | content: https://purgecss.com/api 11 | - property: og:site_name 12 | content: purgecss.com 13 | - property: og:image 14 | content: https://i.imgur.com/UEiUiJ0.png 15 | - property: og:locale 16 | content: en_US 17 | - property: og:title 18 | content: Remove unused CSS - PurgeCSS 19 | - property: og:description 20 | content: PurgeCSS is a tool for removing CSS that you're not actually using in your project. You can use its programmatic API to use it as part of your development workflow. 21 | --- 22 | 23 | # Programmatic API 24 | 25 | Start by installing PurgeCSS as a dev dependency. 26 | 27 | :::: code-group 28 | ::: code-group-item NPM 29 | ```sh 30 | npm install purgecss --save-dev 31 | ``` 32 | ::: 33 | ::: code-group-item YARN 34 | ```sh 35 | yarn add purgecss --dev 36 | ``` 37 | ::: 38 | :::: 39 | 40 | You can now use PurgeCSS inside a JavaScript file. 41 | 42 | In the following examples, the options passed to PurgeCSS are the same as the ones [here](configuration.md). The result `purgecssResult` is an array of an object containing the name of the files with the purged CSS. 43 | 44 | ## Usage 45 | 46 | ### ES Module Import Syntax 47 | ```js 48 | import { PurgeCSS } from 'purgecss' 49 | const purgeCSSResult = await new PurgeCSS().purge({ 50 | content: ['**/*.html'], 51 | css: ['**/*.css'] 52 | }) 53 | ``` 54 | 55 | ### CommonJS Syntax 56 | ```js 57 | const { PurgeCSS } = require('purgecss') 58 | const purgeCSSResult = await new PurgeCSS().purge({ 59 | content: ['**/*.html'], 60 | css: ['**/*.css'] 61 | }) 62 | ``` 63 | 64 | The format of purgeCSSResult is 65 | 66 | ```js 67 | [ 68 | { 69 | file: 'main.css', 70 | css: '/* purged css for main.css */' 71 | }, 72 | { 73 | file: 'animate.css', 74 | css: '/* purged css for animate.css */' 75 | } 76 | ] 77 | ``` 78 | 79 | The type of the result is 80 | 81 | ```typescript 82 | interface ResultPurge { 83 | css: string; 84 | file?: string; 85 | rejected?: string[]; 86 | rejectedCss?: string; 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | title: PurgeCSS - Remove unused CSS 4 | lang: en-US 5 | meta: 6 | - name: description 7 | content: PurgeCSS is a tool to remove unused CSS from your project. It can be used as part of your development workflow. PurgeCSS comes with a JavaScript API, a CLI, and plugins for popular build tools. 8 | - itemprop: description 9 | content: PurgeCSS is a tool to remove unused CSS from your project. It can be used as part of your development workflow. PurgeCSS comes with a JavaScript API, a CLI, and plugins for popular build tools. 10 | - property: og:url 11 | content: https://purgecss.com 12 | - property: og:site_name 13 | content: purgecss.com 14 | - property: og:type 15 | content: website 16 | - property: og:image 17 | content: https://i.imgur.com/UEiUiJ0.png 18 | - property: og:locale 19 | content: en_US 20 | - property: og:title 21 | content: Remove unused CSS - PurgeCSS 22 | - property: og:description 23 | content: PurgeCSS is a tool to remove unused CSS from your project. It can be used as part of your development workflow. PurgeCSS comes with a JavaScript API, a CLI, and plugins for popular build tools. 24 | 25 | heroImage: https://i.imgur.com/UEiUiJ0.png 26 | actions: 27 | - text: Get Started 28 | link: /getting-started 29 | type: primary 30 | - text: Introduction 31 | link: /introduction 32 | type: secondary 33 | footer: MIT Licensed | Copyright © 2018-present Full Human LTD 34 | --- 35 | 36 | ## Sponsors 🥰 37 | 38 | [](https://www.bairesdev.com/sponsoring-open-source-projects/) 39 | [](https://full-human.health/) 40 | 41 | ## Table of Contents 42 | 43 | ### PurgeCSS 44 | 45 | - [Configuration](configuration.md) 46 | - [Command Line Interface](CLI.md) 47 | - [Programmatic API](api.md) 48 | - [Safelisting](safelisting.md) 49 | - [Extractors](extractors.md) 50 | - [Comparison](comparison.md) 51 | 52 | ### Plugins 53 | 54 | - [PostCSS](plugins/postcss.md) 55 | - [Webpack](plugins/webpack.md) 56 | - [Gulp](plugins/gulp.md) 57 | - [Grunt](plugins/grunt.md) 58 | - [Gatsby](plugins/gatsby.md) 59 | 60 | ### Guides 61 | 62 | - [Vue.js](guides/vue.md) 63 | - [Nuxt.js](guides/nuxt.md) 64 | - [React.js](guides/react.md) 65 | - [Next.js](guides/next.md) 66 | - [Razzle](guides/razzle.md) 67 | - [WordPress](guides/wordpress.md) 68 | - [Hugo](guides/hugo.md) 69 | 70 | ### Common Questions 71 | 72 | - [How to use with CSS modules?](css_modules.md) 73 | - [How to use with Ant Design?](ant_design.md) 74 | -------------------------------------------------------------------------------- /docs/ant_design.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ant Design 3 | lang: en-US 4 | meta: 5 | - name: description 6 | content: PurgeCSS can be used with Ant Design but requires to create a custom CSS extractor. 7 | - itemprop: description 8 | content: PurgeCSS can be used with Ant Design but requires to create a custom CSS extractor. 9 | - property: og:url 10 | content: https://purgecss.com/ant_design 11 | - property: og:site_name 12 | content: purgecss.com 13 | - property: og:image 14 | content: https://i.imgur.com/UEiUiJ0.png 15 | - property: og:locale 16 | content: en_US 17 | - property: og:title 18 | content: Remove unused CSS - PurgeCSS 19 | - property: og:description 20 | content: PurgeCSS can be used with Ant Design but requires to create a custom CSS extractor. 21 | --- 22 | 23 | # How to use with Ant Design 24 | 25 | ::: tip 26 | The content of this page comes from [this issue](https://github.com/FullHuman/purgecss/issues/172#issuecomment-637045325). 27 | ::: 28 | 29 | PurgeCSS works by comparing the selectors in your content files with the ones on your CSS files. When using component libraries with their own CSS, it happens the CSS is removed because the content is not found. You then need to specify where the content can be found. 30 | 31 | In the case of ant-design, the list of selectors used in ant-design cannot be retrieve easily from its content. 32 | 33 | Below is a way to use PurgeCSS with Ant Design and React. 34 | The project was created with create-react-app. Then, it is using react-app-rewired to extend the configuration. 35 | 36 | 37 | ```js 38 | const glob = require("glob-all"); 39 | const paths = require("react-scripts/config/paths"); 40 | 41 | const { override, addPostcssPlugins } = require("customize-cra"); 42 | 43 | const purgecss = require("@fullhuman/postcss-purgecss")({ 44 | content: [ 45 | paths.appHtml, 46 | ...glob.sync(`${paths.appSrc}/**/*.js`, { nodir: true }), 47 | ...glob.sync(`${paths.appNodeModules}/antd/es/button/**/*.css`, { 48 | nodir: true, 49 | }), 50 | ], 51 | extractors: [ 52 | { 53 | extractor: (content) => content.match(/([a-zA-Z-]+)(?= {)/g) || [], 54 | extensions: ["css"], 55 | }, 56 | ], 57 | }); 58 | 59 | module.exports = override( 60 | addPostcssPlugins([ 61 | ...(process.env.NODE_ENV === "production" ? [purgecss] : []), 62 | ]) 63 | ); 64 | ``` 65 | 66 | I essentially added a path to the antd css file that I want to keep. in the example below, `button`. 67 | 68 | ```js 69 | ...glob.sync(`${paths.appNodeModules}/antd/es/button/**/*.css`, 70 | ``` 71 | 72 | To keep antd entirely, you could replace by 73 | ```js 74 | ...glob.sync(`${paths.appNodeModules}/antd/es/**/*.css`, 75 | ``` 76 | 77 | and wrote an extractor for css file that intend to get the selectors from the file: 78 | ```js 79 | extractors: [ 80 | { 81 | extractor: (content) => content.match(/([a-zA-Z-]+)(?= {)/g) || [], 82 | extensions: ["css"], 83 | }, 84 | ], 85 | ``` 86 | -------------------------------------------------------------------------------- /docs/plugins/postcss.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PostCSS 3 | lang: en-US 4 | meta: 5 | - name: description 6 | content: PurgeCSS is a tool for removing CSS that you're not actually using in your project. You can use it with postcss with a plugin. 7 | - itemprop: description 8 | content: PurgeCSS is a tool for removing CSS that you're not actually using in your project. You can use it with postcss with a plugin. 9 | - property: og:url 10 | content: https://purgecss.com/plugins/postcss 11 | - property: og:site_name 12 | content: purgecss.com 13 | - property: og:type 14 | content: website 15 | - property: og:image 16 | content: https://i.imgur.com/UEiUiJ0.png 17 | - property: og:locale 18 | content: en_US 19 | - property: og:title 20 | content: Remove unused CSS - PurgeCSS 21 | - property: og:description 22 | content: PurgeCSS is a tool for removing CSS that you're not actually using in your project. You can use it with postcss with a plugin. 23 | --- 24 | 25 | # PostCSS 26 | 27 | ::: warning 28 | If you are using PostCSS 7, install @fullhuman/postcss-purgecss 3.0.0: `npm i -D @fullhuman/postcss-purgecss@3.0.0`. 29 | From version 4.0, it is compatible with PostCSS >=8 only. 30 | ::: 31 | 32 | ## Installation 33 | 34 | ```sh 35 | npm i -D @fullhuman/postcss-purgecss postcss 36 | ``` 37 | 38 | ## Usage 39 | 40 | In `postcss.config.js`: 41 | 42 | ```js 43 | import { purgeCSSPlugin } from '@fullhuman/postcss-purgecss'; 44 | 45 | module.exports = { 46 | plugins: [ 47 | purgeCSSPlugin({ 48 | content: ['./**/*.html'] 49 | }) 50 | ] 51 | } 52 | ``` 53 | 54 | Using PostCSS API: 55 | 56 | ```js 57 | import { purgeCSSPlugin } from '@fullhuman/postcss-purgecss'; 58 | 59 | postcss([ 60 | purgeCSSPlugin({ 61 | content: ['./src/**/*.html'] 62 | }) 63 | ]) 64 | ``` 65 | 66 | See [PostCSS](https://github.com/postcss/postcss) documentation for examples for your environment. 67 | 68 | ## Options 69 | 70 | All of the options of PurgeCSS are available to use with the plugins. 71 | You will find below the type definition of the main options available. For the complete list, go to the [PurgeCSS documentation website](https://www.purgecss.com/configuration.html#options). 72 | 73 | ```ts 74 | export interface UserDefinedOptions { 75 | content?: Array; 76 | contentFunction?: (sourceFile: string) => Array; 77 | defaultExtractor?: ExtractorFunction; 78 | extractors?: Array; 79 | fontFace?: boolean; 80 | keyframes?: boolean; 81 | output?: string; 82 | rejected?: boolean; 83 | stdin?: boolean; 84 | stdout?: boolean; 85 | variables?: boolean; 86 | safelist?: UserDefinedSafelist; 87 | blocklist?: StringRegExpArray; 88 | } 89 | 90 | interface RawContent { 91 | extension: string 92 | raw: string 93 | } 94 | 95 | interface RawCSS { 96 | raw: string 97 | } 98 | 99 | type StringRegExpArray = Array; 100 | ``` 101 | -------------------------------------------------------------------------------- /packages/purgecss/src/ExtractorResultSets.ts: -------------------------------------------------------------------------------- 1 | import { ExtractorResult } from "./types"; 2 | 3 | function mergeSets(into: Set, from?: string[] | Set): void { 4 | if (from) { 5 | from.forEach(into.add, into); 6 | } 7 | } 8 | 9 | /** 10 | * @public 11 | */ 12 | class ExtractorResultSets { 13 | private undetermined = new Set(); 14 | private attrNames = new Set(); 15 | private attrValues = new Set(); 16 | private classes = new Set(); 17 | private ids = new Set(); 18 | private tags = new Set(); 19 | 20 | constructor(er: ExtractorResult) { 21 | this.merge(er); 22 | } 23 | 24 | merge(that: ExtractorResult | ExtractorResultSets): this { 25 | if (Array.isArray(that)) { 26 | mergeSets(this.undetermined, that); 27 | } else if (that instanceof ExtractorResultSets) { 28 | mergeSets(this.undetermined, that.undetermined); 29 | mergeSets(this.attrNames, that.attrNames); 30 | mergeSets(this.attrValues, that.attrValues); 31 | mergeSets(this.classes, that.classes); 32 | mergeSets(this.ids, that.ids); 33 | mergeSets(this.tags, that.tags); 34 | } else { 35 | // ExtractorResultDetailed: 36 | mergeSets(this.undetermined, that.undetermined); 37 | if (that.attributes) { 38 | mergeSets(this.attrNames, that.attributes.names); 39 | mergeSets(this.attrValues, that.attributes.values); 40 | } 41 | mergeSets(this.classes, that.classes); 42 | mergeSets(this.ids, that.ids); 43 | mergeSets(this.tags, that.tags); 44 | } 45 | return this; 46 | } 47 | 48 | hasAttrName(name: string): boolean { 49 | return this.attrNames.has(name) || this.undetermined.has(name); 50 | } 51 | 52 | private someAttrValue(predicate: (value: string) => boolean): boolean { 53 | for (const val of this.attrValues) { 54 | if (predicate(val)) return true; 55 | } 56 | for (const val of this.undetermined) { 57 | if (predicate(val)) return true; 58 | } 59 | return false; 60 | } 61 | 62 | hasAttrPrefix(prefix: string): boolean { 63 | return this.someAttrValue((value) => value.startsWith(prefix)); 64 | } 65 | 66 | hasAttrSuffix(suffix: string): boolean { 67 | return this.someAttrValue((value) => value.endsWith(suffix)); 68 | } 69 | 70 | hasAttrSubstr(substr: string): boolean { 71 | const wordSubstr = substr.trim().split(" "); 72 | return wordSubstr.every((word) => 73 | this.someAttrValue((value) => value.includes(word)), 74 | ); 75 | } 76 | 77 | hasAttrValue(value: string): boolean { 78 | return this.attrValues.has(value) || this.undetermined.has(value); 79 | } 80 | 81 | hasClass(name: string): boolean { 82 | return this.classes.has(name) || this.undetermined.has(name); 83 | } 84 | 85 | hasId(id: string): boolean { 86 | return this.ids.has(id) || this.undetermined.has(id); 87 | } 88 | 89 | hasTag(tag: string): boolean { 90 | return this.tags.has(tag) || this.undetermined.has(tag); 91 | } 92 | } 93 | 94 | export default ExtractorResultSets; 95 | -------------------------------------------------------------------------------- /packages/purgecss/README.md: -------------------------------------------------------------------------------- 1 | # PurgeCSS 2 | 3 | [![npm](https://img.shields.io/npm/v/purgecss?style=for-the-badge)](https://www.npmjs.com/package/purgecss) 4 | ![npm](https://img.shields.io/npm/dm/purgecss?style=for-the-badge) 5 | ![GitHub](https://img.shields.io/github/license/FullHuman/purgecss?style=for-the-badge) 6 | ![Dependabot](https://img.shields.io/badge/dependabot-enabled-%23024ea4?style=for-the-badge) 7 | [![Coverage Status](https://img.shields.io/coveralls/github/FullHuman/purgecss/main?style=for-the-badge)](https://coveralls.io/github/FullHuman/purgecss?branch=main) 8 | 9 |

10 | PurgeCSS logo 11 |

12 | 13 | ## What is PurgeCSS? 14 | 15 | When you are building a website, chances are that you are using a css framework like Bootstrap, Materializecss, Foundation, etc... But you will only use a small set of the framework and a lot of unused css styles will be included. 16 | 17 | This is where PurgeCSS comes into play. PurgeCSS analyzes your content and your css files. Then it matches the selectors used in your files with the one in your content files. It removes unused selectors from your css, resulting in smaller css files. 18 | 19 | ## Sponsors 🥰 20 | 21 | [](https://www.bairesdev.com/sponsoring-open-source-projects/) 22 | 23 | ## Documentation 24 | 25 | You can find the PurgeCSS documentation on [this website](https://purgecss.com). 26 | 27 | ### Table of Contents 28 | 29 | #### PurgeCSS 30 | 31 | - [Configuration](https://purgecss.com/configuration.html) 32 | - [Command Line Interface](https://purgecss.com/CLI.html) 33 | - [Programmatic API](https://purgecss.com/api.html) 34 | - [Safelisting](https://purgecss.com/safelisting.html) 35 | - [Extractors](https://purgecss.com/extractors.html) 36 | - [Comparison](https://purgecss.com/comparison.html) 37 | 38 | #### Plugins 39 | 40 | - [PostCSS](https://purgecss.com/plugins/postcss.html) 41 | - [Webpack](https://purgecss.com/plugins/webpack.html) 42 | - [Gulp](https://purgecss.com/plugins/gulp.html) 43 | - [Grunt](https://purgecss.com/plugins/grunt.html) 44 | - [Gatsby](https://purgecss.com/plugins/gatsby.html) 45 | 46 | #### Guides 47 | 48 | - [Vue.js](https://purgecss.com/guides/vue.html) 49 | - [Nuxt.js](https://purgecss.com/guides/nuxt.html) 50 | - [React.js](https://purgecss.com/guides/react.html) 51 | - [Next.js](https://purgecss.com/guides/next.html) 52 | - [Razzle](https://purgecss.com/guides/razzle.html) 53 | 54 | ## Getting Started 55 | 56 | #### Installation 57 | 58 | ``` 59 | npm i --save-dev purgecss 60 | ``` 61 | 62 | ## Usage 63 | 64 | ```js 65 | import PurgeCSS from "purgecss"; 66 | const purgeCSSResults = await new PurgeCSS().purge({ 67 | content: ["**/*.html"], 68 | css: ["**/*.css"], 69 | }); 70 | ``` 71 | 72 | ## Contributing 73 | 74 | Please read [CONTRIBUTING.md](./../../CONTRIBUTING.md) for details on our code of 75 | conduct, and the process for submitting pull requests to us. 76 | 77 | ## Versioning 78 | 79 | PurgeCSS use [SemVer](http://semver.org/) for versioning. 80 | 81 | ## License 82 | 83 | This project is licensed under the MIT License - see the [LICENSE](./../../LICENSE) file 84 | for details. 85 | --------------------------------------------------------------------------------