├── .github └── workflows │ ├── main.yml │ ├── release.yml │ └── size.yml ├── .gitignore ├── .releaserc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── renovate.json ├── src └── index.ts ├── test └── index.test.ts ├── tsconfig.json └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['14.x', '16.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - next 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '16' 20 | - name: Install dependencies 21 | run: yarn 22 | - name: Build 23 | run: yarn build 24 | - name: Release 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | run: yarn semantic-release 29 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | coverage 6 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | - main 3 | - next 4 | plugins: 5 | - '@semantic-release/commit-analyzer' 6 | - '@semantic-release/release-notes-generator' 7 | - '@semantic-release/npm' 8 | - '@semantic-release/github' 9 | - '@semantic-release/changelog' 10 | - '@semantic-release/git' 11 | preset: 'conventionalcommits' 12 | presetConfig: 13 | types: 14 | - 'type': 'feat' 15 | 'section': 'Features' 16 | - 'type': 'feature' 17 | 'section': 'Features' 18 | - 'type': 'fix' 19 | 'section': 'Bug Fixes' 20 | - 'type': 'perf' 21 | 'section': 'Performance Improvements' 22 | - 'type': 'revert' 23 | 'section': 'Reverts' 24 | - 'type': 'docs' 25 | 'section': 'Documentation' 26 | - 'type': 'style' 27 | 'section': 'Styles' 28 | - 'type': 'chore' 29 | 'section': 'Miscellaneous Chores' 30 | - 'type': 'refactor' 31 | 'section': 'Code Refactoring' 32 | - 'type': 'test' 33 | 'section': 'Tests' 34 | - 'type': 'build' 35 | 'section': 'Build System' 36 | - 'type': 'ci' 37 | 'section': 'Continuous Integration' 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.0](https://github.com/mbrandau/postcss-class-name-shortener/compare/v1.1.0...v2.0.0) (2022-03-26) 2 | 3 | 4 | ### ⚠ BREAKING CHANGES 5 | 6 | * now uses css-shortener v3 7 | 8 | ### Features 9 | 10 | * update to css-shortener v3 ([65d6c33](https://github.com/mbrandau/postcss-class-name-shortener/commit/65d6c3341d92f14c1ba234426e2180b97cf29dee)) 11 | 12 | 13 | ### Miscellaneous Chores 14 | 15 | * add missing dependencies ([1e5182c](https://github.com/mbrandau/postcss-class-name-shortener/commit/1e5182c594f38e15db9d7b68559f89061f064d80)) 16 | * **release:** 1.0.0 [skip ci] ([99d2131](https://github.com/mbrandau/postcss-class-name-shortener/commit/99d2131cb669b6902924c3fdce4c5f9a2244de9c)) 17 | * update lockfile ([03b18a8](https://github.com/mbrandau/postcss-class-name-shortener/commit/03b18a8d2c8b3c84d29fef499b0f807564c4add2)) 18 | 19 | ## 1.0.0 (2022-03-26) 20 | 21 | 22 | ### ⚠ BREAKING CHANGES 23 | 24 | * now uses css-shortener v3 25 | 26 | ### Features 27 | 28 | * add disable option ([3634939](https://github.com/mbrandau/postcss-class-name-shortener/commit/3634939da8e1bed8ece4346b5f72d7c3e0b17e69)) 29 | * update to css-shortener v3 ([65d6c33](https://github.com/mbrandau/postcss-class-name-shortener/commit/65d6c3341d92f14c1ba234426e2180b97cf29dee)) 30 | 31 | 32 | ### Miscellaneous Chores 33 | 34 | * add missing dependencies ([1e5182c](https://github.com/mbrandau/postcss-class-name-shortener/commit/1e5182c594f38e15db9d7b68559f89061f064d80)) 35 | * **npm:** update lockfile version ([f1ad9c6](https://github.com/mbrandau/postcss-class-name-shortener/commit/f1ad9c6811663ac63ddbd2ef653cd182c3d2b854)) 36 | * update lockfile ([03b18a8](https://github.com/mbrandau/postcss-class-name-shortener/commit/03b18a8d2c8b3c84d29fef499b0f807564c4add2)) 37 | 38 | # Change Log 39 | 40 | ## 1.1.0 41 | 42 | - Add ability to pass an existing [CssShortener](https://github.com/mbrandau/css-shortener) in options to use for generating and mapping class names 43 | 44 | ## 1.0.0 45 | 46 | - Initial release 47 | 48 | This project adheres to [Semantic Versioning](http://semver.org/). 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Maximilian Brandau 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Class Name Shortener 2 | 3 | [![CI](https://github.com/mbrandau/postcss-class-name-shortener/actions/workflows/main.yml/badge.svg)](https://github.com/mbrandau/postcss-class-name-shortener/actions/workflows/main.yml) [![npm](https://img.shields.io/npm/v/postcss-class-name-shortener.svg)](https://www.npmjs.com/package/postcss-class-name-shortener) [![npm](https://img.shields.io/npm/dt/postcss-class-name-shortener.svg)](https://www.npmjs.com/package/postcss-class-name-shortener) [![GitHub issues](https://img.shields.io/github/issues/mbrandau/postcss-class-name-shortener.svg)](https://github.com/mbrandau/postcss-class-name-shortener/issues) 4 | 5 | [PostCSS] plugin that shortens CSS class names to optimize website performance. 6 | 7 | The plugin will produce an object with all mapped class names and return it via the callback. 8 | 9 | [postcss]: https://github.com/postcss/postcss 10 | 11 | ```css 12 | // INPUT 13 | .long-class-name-that-just-sets-the-text-color-to-black { 14 | color: black; 15 | } 16 | ``` 17 | 18 | ```css 19 | // OUTPUT 20 | .a { 21 | color: black; 22 | } 23 | ``` 24 | 25 | This plugin uses [css-shortener](https://github.com/mbrandau/css-shortener) under the hood. 26 | 27 | ## Usage 28 | 29 | ```bash 30 | npm install --save postcss-class-name-shortener 31 | ``` 32 | 33 | ```js 34 | const classNameShortener = require('postcss-class-name-shortener'); 35 | const fs = require('fs'); 36 | 37 | postcss([ 38 | [ 39 | 'postcss-class-name-shortener', 40 | { 41 | outputMapCallback: map => { 42 | console.log(JSON.stringify(map)); 43 | // You can return a promise 44 | return new Promise((resolve, reject) => { 45 | fs.writeFile('cssMap.json', JSON.stringify(map), err => { 46 | if(err) reject(err); 47 | else resolve(); 48 | }); 49 | }) 50 | } 51 | // Optionally disable shorting of class names in dev environment 52 | disable: process.env.NODE_ENV === 'development' 53 | } 54 | ] 55 | ]) 56 | ``` 57 | 58 | The `map` object will look like this: 59 | 60 | ```json 61 | { 62 | "long-class-name-that-just-sets-the-text-color-to-black": "a" 63 | } 64 | ``` 65 | 66 | ## Options 67 | 68 | See [PostCSS] docs for examples for your environment. 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-class-name-shortener", 3 | "version": "2.0.0", 4 | "description": "PostCSS plugin that shortens CSS class names to optimize website performance.", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "css-shortener", 10 | "shortener", 11 | "class", 12 | "name", 13 | "shorten" 14 | ], 15 | "author": "Maximilian Brandau ", 16 | "license": "MIT", 17 | "repository": "mbrandau/postcss-class-name-shortener", 18 | "bugs": { 19 | "url": "https://github.com/mbrandau/postcss-class-name-shortener/issues" 20 | }, 21 | "homepage": "https://github.com/mbrandau/postcss-class-name-shortener", 22 | "main": "dist/index.js", 23 | "typings": "dist/index.d.ts", 24 | "files": [ 25 | "dist", 26 | "src" 27 | ], 28 | "engines": { 29 | "node": ">=10" 30 | }, 31 | "scripts": { 32 | "start": "tsdx watch", 33 | "build": "tsdx build", 34 | "test": "tsdx test", 35 | "lint": "tsdx lint", 36 | "prepare": "tsdx build", 37 | "size": "size-limit", 38 | "analyze": "size-limit --why", 39 | "semantic-release": "semantic-release" 40 | }, 41 | "peerDependencies": {}, 42 | "husky": { 43 | "hooks": { 44 | "pre-commit": "tsdx lint" 45 | } 46 | }, 47 | "prettier": { 48 | "printWidth": 80, 49 | "semi": true, 50 | "singleQuote": true, 51 | "trailingComma": "es5" 52 | }, 53 | "module": "dist/postcss-class-name-shortener.esm.js", 54 | "size-limit": [ 55 | { 56 | "path": "dist/postcss-class-name-shortener.cjs.production.min.js", 57 | "limit": "17 KB" 58 | }, 59 | { 60 | "path": "dist/postcss-class-name-shortener.esm.js", 61 | "limit": "17 KB" 62 | } 63 | ], 64 | "devDependencies": { 65 | "@semantic-release/changelog": "6.0.1", 66 | "@semantic-release/git": "10.0.1", 67 | "@size-limit/preset-small-lib": "7.0.8", 68 | "conventional-changelog-conventionalcommits": "5.0.0", 69 | "husky": "8.0.1", 70 | "semantic-release": "19.0.3", 71 | "size-limit": "7.0.8", 72 | "tsdx": "0.14.1", 73 | "tslib": "2.4.0", 74 | "typescript": "4.6.4" 75 | }, 76 | "dependencies": { 77 | "css-shortener": "^3.0.0", 78 | "postcss": "^8.4.12", 79 | "postcss-selector-parser": "^6.0.9" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:js-lib", ":semanticCommits"], 3 | "baseBranches": ["main"] 4 | } 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ClassNameMap, CssShortener } from 'css-shortener'; 2 | import { Rule } from 'postcss'; 3 | import selectorParser from 'postcss-selector-parser'; 4 | 5 | type OutputMapCallback = (map: ClassNameMap) => {}; 6 | 7 | export default function( 8 | opts: { 9 | cssShortener?: CssShortener; 10 | outputMapCallback?: OutputMapCallback; 11 | except?: Set; 12 | disable?: boolean; 13 | } = {} 14 | ) { 15 | return { 16 | postcssPlugin: 'postcss-class-name-shortener', 17 | prepare() { 18 | if ((!opts.outputMapCallback && !opts.cssShortener) || opts.disable) 19 | return {}; 20 | 21 | const exceptSet = opts.except || new Set(); 22 | const cssShortener = opts.cssShortener ?? new CssShortener(); 23 | 24 | function shouldSkip(nodeValue: string): boolean { 25 | if (exceptSet.has(nodeValue)) return true; 26 | for (const val of Array.from(exceptSet)) 27 | if (val instanceof RegExp && val.test(nodeValue)) return true; 28 | return false; 29 | } 30 | 31 | const selectorProcessor = selectorParser(selectors => { 32 | selectors.walkClasses( 33 | (node: selectorParser.ClassName | selectorParser.Identifier) => { 34 | if (shouldSkip(node.value)) return; 35 | 36 | node.value = cssShortener.shortenClassName(node.value); 37 | } 38 | ); 39 | }); 40 | 41 | return { 42 | Rule(ruleNode: Rule) { 43 | selectorProcessor.process(ruleNode); 44 | }, 45 | OnceExit() { 46 | if (opts.outputMapCallback) opts.outputMapCallback(cssShortener.map); 47 | }, 48 | }; 49 | }, 50 | }; 51 | } 52 | 53 | export const postcss = true; 54 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import { CssShortener } from 'css-shortener'; 3 | import plugin from '../src'; 4 | import { ClassNameMap } from 'css-shortener'; 5 | 6 | function buildExpectMapCallback(expectedMap: ClassNameMap) { 7 | return function(map: ClassNameMap) { 8 | expect(map).toEqual(expectedMap); 9 | }; 10 | } 11 | 12 | function run(input: string, expectedOutput: string, options: any) { 13 | return postcss([plugin(options)]) 14 | .process(input, { from: undefined }) 15 | .then(result => { 16 | expect(result.css).toEqual(expectedOutput); 17 | expect(result.warnings().length).toBe(0); 18 | return result; 19 | }); 20 | } 21 | 22 | describe('tests', () => { 23 | it('replaces class name', () => { 24 | return run('.test{ }', '.a{ }', { 25 | outputMapCallback: buildExpectMapCallback({ test: 'a' }), 26 | }); 27 | }); 28 | it('does nothing when disabled', () => { 29 | return run('.test{ }', '.test{ }', { 30 | disable: true, 31 | outputMapCallback: buildExpectMapCallback({}), 32 | }); 33 | }); 34 | it('uses custom CssShortener', () => { 35 | const shortener = new CssShortener(); 36 | shortener.importMap({ test: 'b' }); 37 | return run('.test{ } .test2{ } .test3{ }', '.b{ } .a{ } .c{ }', { 38 | cssShortener: shortener, 39 | }).then(() => { 40 | expect(shortener.map).toEqual({ test: 'b', test2: 'a', test3: 'c' }); 41 | }); 42 | }); 43 | it('does not replace class names in except option via string', () => { 44 | return run('.test{ }', '.test{ }', { expect: ['test'] }); 45 | }); 46 | it('does not replace class names in except option via RegExp', () => { 47 | return run('.test{ }', '.test{ }', { expect: [/test/] }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | --------------------------------------------------------------------------------