├── .nvmrc ├── .gitignore ├── tests ├── integrations │ ├── flat-config │ │ ├── .npmrc │ │ ├── a.vue │ │ ├── eslint.config.js │ │ └── package.json │ ├── legacy-config │ │ ├── .npmrc │ │ ├── a.vue │ │ ├── .eslintrc │ │ └── package.json │ ├── flat-config.js │ └── legacy-config.js ├── .prettierrc.json └── lib │ ├── rules │ ├── dummy.css │ ├── another.css │ ├── no-arbitrary-value.js │ ├── enforces-negative-arbitrary-values.js │ ├── no-unnecessary-arbitrary-value.js │ └── migration-from-tailwind-2.js │ ├── index.js │ └── util │ └── groupMethods.js ├── .github ├── logo.png ├── output.png ├── youtube-eslint-plugin-tailwindcss-round.png ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── build.yml └── pull_request_template.md ├── sponsors └── daily.dev.jpg ├── .vscode └── settings.json ├── lib ├── .prettierrc.json ├── util │ ├── removeDuplicatesFromArray.js │ ├── types │ │ ├── angle.js │ │ ├── length.js │ │ └── color.js │ ├── docsUrl.js │ ├── regex.js │ ├── generated.js │ ├── removeDuplicatesFromClassnamesAndWhitespaces.js │ ├── parser.js │ ├── settings.js │ ├── customConfig.js │ ├── cssFiles.js │ ├── prettier │ │ └── order.js │ └── ast.js ├── config │ ├── recommended.js │ ├── rules.js │ └── flat-recommended.js ├── index.js └── rules │ ├── no-arbitrary-value.js │ ├── enforces-negative-arbitrary-values.js │ ├── no-custom-classname.js │ ├── no-contradicting-classname.js │ ├── classnames-order.js │ ├── migration-from-tailwind-2.js │ └── no-unnecessary-arbitrary-value.js ├── .editorconfig ├── LICENSE ├── CONTRIBUTING.md ├── package.json ├── docs └── rules │ ├── no-arbitrary-value.md │ ├── no-contradicting-classname.md │ ├── classnames-order.md │ ├── enforces-shorthand.md │ ├── enforces-negative-arbitrary-values.md │ ├── no-unnecessary-arbitrary-value.md │ ├── migration-from-tailwind-2.md │ └── no-custom-classname.md └── CODE_OF_CONDUCT.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.12.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .idea 4 | -------------------------------------------------------------------------------- /tests/integrations/flat-config/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /tests/integrations/legacy-config/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyoban/eslint-plugin-tailwindcss/HEAD/.github/logo.png -------------------------------------------------------------------------------- /.github/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyoban/eslint-plugin-tailwindcss/HEAD/.github/output.png -------------------------------------------------------------------------------- /sponsors/daily.dev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyoban/eslint-plugin-tailwindcss/HEAD/sponsors/daily.dev.jpg -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true 4 | } -------------------------------------------------------------------------------- /lib/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /tests/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": true, 4 | "singleQuote": false, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /.github/youtube-eslint-plugin-tailwindcss-round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyoban/eslint-plugin-tailwindcss/HEAD/.github/youtube-eslint-plugin-tailwindcss-round.png -------------------------------------------------------------------------------- /tests/integrations/flat-config/a.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /tests/integrations/legacy-config/a.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /lib/util/removeDuplicatesFromArray.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function removeDuplicatesFromArray(arr) { 4 | return [...new Set(arr)]; 5 | } 6 | 7 | module.exports = removeDuplicatesFromArray; 8 | -------------------------------------------------------------------------------- /lib/util/types/angle.js: -------------------------------------------------------------------------------- 1 | const units = ['deg', 'grad', 'rad', 'turn']; 2 | 3 | const mergedAngleValues = [ 4 | `\\-?(\\d{1,}(\\.\\d{1,})?|\\.\\d{1,})(${units.join('|')})`, 5 | `calc\\(.{1,}\\)`, 6 | `var\\(\\-\\-[A-Za-z\\-]{1,}\\)`, 7 | ]; 8 | 9 | module.exports = { 10 | mergedAngleValues, 11 | }; 12 | -------------------------------------------------------------------------------- /lib/util/docsUrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Copied from https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/util/docsUrl.js 4 | function docsUrl(ruleName) { 5 | return `https://github.com/francoismassart/eslint-plugin-tailwindcss/tree/master/docs/rules/${ruleName}.md`; 6 | } 7 | 8 | module.exports = docsUrl; 9 | -------------------------------------------------------------------------------- /tests/integrations/legacy-config/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "vue-eslint-parser", 4 | "parserOptions": { 5 | "sourceType": "module" 6 | }, 7 | "extends": ["plugin:vue/vue3-recommended", "plugin:tailwindcss/recommended"], 8 | "rules": { 9 | "vue/multi-word-component-names": "off", 10 | "tailwindcss/classnames-order": "warn" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/integrations/flat-config/eslint.config.js: -------------------------------------------------------------------------------- 1 | import vue from "eslint-plugin-vue"; 2 | import tailwind from "eslint-plugin-tailwindcss"; 3 | 4 | export default [ 5 | ...vue.configs["flat/recommended"], 6 | ...tailwind.configs["flat/recommended"], 7 | { 8 | rules: { 9 | "vue/multi-word-component-names": "off", 10 | "tailwindcss/classnames-order": "warn", 11 | }, 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /tests/integrations/flat-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "integration-test-for-flat-config", 4 | "version": "1.0.0", 5 | "type": "module", 6 | "description": "Integration test for flat config", 7 | "dependencies": { 8 | "eslint": "^8.57.0", 9 | "eslint-plugin-vue": "^9.24.0", 10 | "eslint-plugin-tailwindcss": "file:../../..", 11 | "vue-eslint-parser": "^9.4.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/util/regex.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Escapes a string to be used in a regular expression 3 | * Copied from https://stackoverflow.com/a/3561711. 4 | * @param {string} input 5 | * @returns {string} 6 | */ 7 | function escapeRegex(input) { 8 | return input.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); 9 | } 10 | 11 | const separatorRegEx = /([\t\n\f\r ]+)/; 12 | 13 | module.exports = { 14 | escapeRegex, 15 | separatorRegEx, 16 | }; 17 | -------------------------------------------------------------------------------- /tests/integrations/legacy-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "integration-test-for-flat-config", 4 | "version": "1.0.0", 5 | "type": "module", 6 | "description": "Integration test for flat config", 7 | "dependencies": { 8 | "eslint": "^8.57.0", 9 | "eslint-plugin-vue": "^9.24.0", 10 | "eslint-plugin-tailwindcss": "file:../../..", 11 | "vue-eslint-parser": "^9.4.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/config/recommended.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Recommended coniguration for legacy style 3 | * @see https://eslint.org/docs/latest/use/configure/configuration-files 4 | * @author François Massart 5 | */ 6 | 'use strict'; 7 | 8 | const rules = require('./rules'); 9 | 10 | module.exports = { 11 | plugins: ['tailwindcss'], 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | rules, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/util/generated.js: -------------------------------------------------------------------------------- 1 | var generateRulesFallback = require('tailwindcss/lib/lib/generateRules').generateRules; 2 | 3 | function generate(className, context) { 4 | // const order = generateRulesFallback(new Set([className]), context).sort(([a], [z]) => bigSign(z - a))[0]?.[0] ?? null; 5 | const gen = generateRulesFallback(new Set([className]), context); 6 | // console.debug(gen); 7 | return gen; 8 | } 9 | 10 | module.exports = generate; 11 | -------------------------------------------------------------------------------- /lib/config/rules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Default rules configuration 3 | * @author François Massart 4 | */ 5 | 6 | module.exports = { 7 | 'tailwindcss/classnames-order': 'warn', 8 | 'tailwindcss/enforces-negative-arbitrary-values': 'warn', 9 | 'tailwindcss/enforces-shorthand': 'warn', 10 | 'tailwindcss/migration-from-tailwind-2': 'warn', 11 | 'tailwindcss/no-arbitrary-value': 'off', 12 | 'tailwindcss/no-custom-classname': 'warn', 13 | 'tailwindcss/no-contradicting-classname': 'error', 14 | 'tailwindcss/no-unnecessary-arbitrary-value': 'warn', 15 | }; 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [francoismassart] 2 | patreon: # Replace with a single Patreon username 3 | open_collective: # Replace with a single Open Collective username 4 | ko_fi: # Replace with a single Ko-fi username 5 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 6 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 7 | liberapay: # Replace with a single Liberapay username 8 | issuehunt: # Replace with a single IssueHunt username 9 | otechie: # Replace with a single Otechie username 10 | custom: [https://thanks.dev/r/gh/francoismassart] 11 | -------------------------------------------------------------------------------- /lib/config/flat-recommended.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Recommended coniguration for flat style 3 | * @see https://eslint.org/docs/latest/use/configure/configuration-files-new 4 | * @author François Massart 5 | */ 6 | 'use strict'; 7 | 8 | const rules = require('./rules'); 9 | 10 | module.exports = [ 11 | { 12 | name: 'tailwindcss:base', 13 | plugins: { 14 | get tailwindcss() { 15 | return require('../index'); 16 | }, 17 | }, 18 | languageOptions: { 19 | parserOptions: { 20 | ecmaFeatures: { 21 | jsx: true, 22 | }, 23 | }, 24 | }, 25 | }, 26 | { 27 | name: 'tailwindcss:rules', 28 | rules, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /tests/lib/rules/dummy.css: -------------------------------------------------------------------------------- 1 | /* Only used for running tests */ 2 | .some { 3 | color: black; 4 | } 5 | .white-listed { 6 | color: yellow; 7 | } 8 | .classnames { 9 | color: red; 10 | } 11 | .one, 12 | .two { 13 | color: red; 14 | } 15 | 16 | @media screen and (min-width: 480px) { 17 | body { 18 | background-color: lightgreen; 19 | } 20 | } 21 | 22 | #main { 23 | border: 1px solid black; 24 | } 25 | @layer base { 26 | ul li { 27 | padding: 5px; 28 | } 29 | .base { 30 | display: block; 31 | } 32 | } 33 | 34 | @tailwind utilities; 35 | 36 | .btn { 37 | @apply border-red; 38 | } 39 | 40 | .parent { 41 | .child, 42 | .btn { 43 | background: none; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature request] " 5 | labels: 'enhancement' 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 | -------------------------------------------------------------------------------- /lib/util/removeDuplicatesFromClassnamesAndWhitespaces.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function removeDuplicatesFromClassnamesAndWhitespaces(orderedClassNames, whitespaces, headSpace, tailSpace) { 4 | let previous = orderedClassNames[0]; 5 | const offset = (!headSpace && !tailSpace) || tailSpace ? -1 : 0; 6 | for (let i = 1; i < orderedClassNames.length; i++) { 7 | const cls = orderedClassNames[i]; 8 | // This function assumes that the list of classNames is ordered, so just comparing to the previous className is enough 9 | if (cls === previous) { 10 | orderedClassNames.splice(i, 1); 11 | whitespaces.splice(i + offset, 1); 12 | i--; 13 | } 14 | previous = cls; 15 | } 16 | } 17 | 18 | module.exports = removeDuplicatesFromClassnamesAndWhitespaces; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. macOS, windows 10] 28 | - Softwares + version used: 29 | - [e.g. VSCode 1.54.3] 30 | - [... Terminal 2.9.5, npm 6.14.5, node v14.5.0] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | 35 | **eslint config file or live demo** 36 | By providing a link to a live demo, a demo video or a github repo fixing the issue will be easier. 37 | -------------------------------------------------------------------------------- /tests/lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Use a consistent orders for the Tailwind CSS classnames, based on property then on variants 3 | * @author François Massart 4 | */ 5 | "use strict"; 6 | 7 | var plugin = require("../../lib/index"); 8 | 9 | var assert = require("assert"); 10 | var fs = require("fs"); 11 | var path = require("path"); 12 | 13 | var rules = fs.readdirSync(path.resolve(__dirname, "../../lib/rules/")).map(function (f) { 14 | return path.basename(f, ".js"); 15 | }); 16 | 17 | describe("all rule files should be exported by the plugin", function () { 18 | rules.forEach(function (ruleName) { 19 | it(`should export ${ruleName}`, function () { 20 | assert.equal(plugin.rules[ruleName], require(path.join("../../lib/rules", ruleName))); 21 | }); 22 | }); 23 | }); 24 | 25 | describe("configurations", function () { 26 | it(`should export a "recommended" configuration`, function () { 27 | assert(plugin.configs.recommended); 28 | }); 29 | 30 | it(`should export a "flat/recommended" configuration`, function () { 31 | assert(plugin.configs["flat/recommended"]); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Francois Massart 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 | -------------------------------------------------------------------------------- /tests/lib/rules/another.css: -------------------------------------------------------------------------------- 1 | /* override input always getting focus styling from package "focus-visible" */ 2 | .focus-outline[data-focus-visible-added].focus-visible.focus, 3 | .focus-outline[data-focus-visible-added].focus-visible.focus:focus, 4 | input[type="text"].focus\:outline-none[data-focus-visible-added]:focus, 5 | input[type="email"].focus\:outline-none[data-focus-visible-added]:focus { 6 | box-shadow: none; 7 | outline-width: 0; 8 | } 9 | /* .custom-carousel li:first-child { 10 | padding-left: 20vw; 11 | } */ 12 | 13 | :global(.slide:not(.selected)) { 14 | opacity: 0.2; 15 | } 16 | 17 | .gallery-snackbar { 18 | @apply w-full; 19 | } 20 | 21 | .custom-slide { 22 | height: 70vh; 23 | } 24 | 25 | @media (min-width: 768px) { 26 | .custom-carousel li:not(:first-child) { 27 | @apply pl-16; 28 | } 29 | 30 | .custom-carousel li:not(:last-child) { 31 | @apply pr-16; 32 | } 33 | 34 | .custom-carousel :global(.carousel-root) { 35 | padding: 0 calc(10vw + 16px); 36 | } 37 | 38 | .gallery-snackbar { 39 | width: calc(80vw - 32px); 40 | } 41 | 42 | .indicator { 43 | margin-left: calc(10vw + 16px); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | # waiting on: https://github.com/actions/setup-node/issues/531 9 | - run: corepack enable 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: 21 13 | cache: npm 14 | - run: npm ci 15 | 16 | - name: test build package on node@21 (current) 17 | run: | 18 | node --version 19 | npm --version 20 | npm run test 21 | 22 | # Not using a matrix here since it's simpler 23 | # to just duplicate it and not spawn new instances 24 | 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | - name: test build package on node@20 (LTS) 29 | run: | 30 | node --version 31 | npm --version 32 | npm run test 33 | 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version: 18 37 | - name: test build package on node@18 (LTS) 38 | run: | 39 | node --version 40 | npm --version 41 | npm run test 42 | -------------------------------------------------------------------------------- /lib/util/parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see parserServices https://eslint.org/docs/developer-guide/working-with-rules#the-context-object 3 | * @param {Object} context 4 | * @param {Function} templateBodyVisitor 5 | * @param {Function} scriptVisitor 6 | * @returns 7 | */ 8 | function defineTemplateBodyVisitor(context, templateBodyVisitor, scriptVisitor) { 9 | const parserServices = getParserServices(context); 10 | if (parserServices == null || parserServices.defineTemplateBodyVisitor == null) { 11 | // Default parser 12 | return scriptVisitor; 13 | } 14 | 15 | // Using "vue-eslint-parser" requires this setup 16 | // @see https://eslint.org/docs/developer-guide/working-with-rules#the-context-object 17 | return parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor); 18 | } 19 | 20 | /** 21 | * This function is API compatible with eslint v8.x and eslint v9 or later. 22 | * @see https://eslint.org/blog/2023/09/preparing-custom-rules-eslint-v9/#from-context-to-sourcecode 23 | */ 24 | function getParserServices(context) { 25 | return context.sourceCode ? context.sourceCode.parserServices : context.parserServices; 26 | } 27 | 28 | module.exports = { 29 | defineTemplateBodyVisitor, 30 | }; 31 | -------------------------------------------------------------------------------- /tests/integrations/flat-config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { strict: assert } = require("assert"); 4 | const cp = require("child_process"); 5 | const path = require("path"); 6 | const semver = require("semver"); 7 | 8 | const ESLINT_BIN_PATH = [".", "node_modules", ".bin", "eslint"].join(path.sep); 9 | 10 | describe("Integration with flat config", () => { 11 | let originalCwd; 12 | 13 | before(() => { 14 | originalCwd = process.cwd(); 15 | process.chdir(path.join(__dirname, "flat-config")); 16 | cp.execSync("npm i -f", { stdio: "inherit" }); 17 | }); 18 | after(() => { 19 | process.chdir(originalCwd); 20 | }); 21 | 22 | it("should work with flat config", () => { 23 | if ( 24 | !semver.satisfies( 25 | process.version, 26 | require(path.join(__dirname, "flat-config/node_modules/eslint/package.json")).engines.node 27 | ) 28 | ) { 29 | return; 30 | } 31 | 32 | const lintResult = cp.execSync(`${ESLINT_BIN_PATH} a.vue --format=json`, { 33 | encoding: "utf8", 34 | }); 35 | const result = JSON.parse(lintResult); 36 | assert.strictEqual(result.length, 1); 37 | assert.deepStrictEqual(result[0].messages[0].messageId, "invalidOrder"); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Rules enforcing best practices while using Tailwind CSS 3 | * @author François Massart 4 | */ 5 | 'use strict'; 6 | 7 | //------------------------------------------------------------------------------ 8 | // Plugin Definition 9 | //------------------------------------------------------------------------------ 10 | 11 | // import all rules in lib/rules 12 | var base = __dirname + '/rules/'; 13 | module.exports = { 14 | rules: { 15 | 'classnames-order': require(base + 'classnames-order'), 16 | 'enforces-negative-arbitrary-values': require(base + 'enforces-negative-arbitrary-values'), 17 | 'enforces-shorthand': require(base + 'enforces-shorthand'), 18 | 'migration-from-tailwind-2': require(base + 'migration-from-tailwind-2'), 19 | 'no-arbitrary-value': require(base + 'no-arbitrary-value'), 20 | 'no-contradicting-classname': require(base + 'no-contradicting-classname'), 21 | 'no-custom-classname': require(base + 'no-custom-classname'), 22 | 'no-unnecessary-arbitrary-value': require(base + 'no-unnecessary-arbitrary-value'), 23 | }, 24 | configs: { 25 | recommended: require('./config/recommended'), 26 | 'flat/recommended': require('./config/flat-recommended'), 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /tests/integrations/legacy-config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { strict: assert } = require("assert"); 4 | const cp = require("child_process"); 5 | const path = require("path"); 6 | const semver = require("semver"); 7 | 8 | const ESLINT_BIN_PATH = [".", "node_modules", ".bin", "eslint"].join(path.sep); 9 | 10 | describe("Integration with legacy config", () => { 11 | let originalCwd; 12 | 13 | before(() => { 14 | originalCwd = process.cwd(); 15 | process.chdir(path.join(__dirname, "legacy-config")); 16 | cp.execSync("npm i -f", { stdio: "inherit" }); 17 | }); 18 | after(() => { 19 | process.chdir(originalCwd); 20 | }); 21 | 22 | it("should work with legacy config", () => { 23 | if ( 24 | !semver.satisfies( 25 | process.version, 26 | require(path.join(__dirname, "legacy-config/node_modules/eslint/package.json")).engines.node 27 | ) 28 | ) { 29 | return; 30 | } 31 | 32 | const lintResult = cp.execSync(`${ESLINT_BIN_PATH} a.vue --format=json`, { 33 | encoding: "utf8", 34 | }); 35 | const result = JSON.parse(lintResult); 36 | assert.strictEqual(result.length, 1); 37 | assert.deepStrictEqual(result[0].messages[0].messageId, "invalidOrder"); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `esLint-plugin-tailwindcss` 2 | 3 | ## Contributing 4 | 5 | When contributing to this repository, please first discuss the change you wish to make via issue, 6 | email, or any other method with the owners of this repository before making a change. 7 | 8 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 9 | 10 | ## Development 11 | 12 | You can use [Corepack](https://nodejs.org/api/corepack.html) to ensure you're using the same package 13 | manager. Run `corepack enabled` before running `npm install`. 14 | 15 | ## Pull Request Process 16 | 17 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 18 | build. 19 | 2. Update the README.md with details of changes to the interface, this includes new environment 20 | variables, exposed ports, useful file locations and container parameters. 21 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 22 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 23 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 24 | do not have permission to do that, you may request the second reviewer to merge it for you. 25 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request Name 2 | 3 | ## Description 4 | 5 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 6 | List any dependencies that are required for this change. 7 | 8 | Fixes # (issue) 9 | 10 | ## Type of change 11 | 12 | Please delete options that are not relevant. 13 | 14 | - [ ] Bug fix (non-breaking change which fixes an issue) 15 | - [ ] New feature (non-breaking change which adds functionality) 16 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 17 | - [ ] This change requires a documentation update 18 | 19 | ## How Has This Been Tested? 20 | 21 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. 22 | Please also list any relevant details for your test configuration 23 | 24 | - [ ] Test A 25 | - [ ] Test B 26 | 27 | **Test Configuration**: 28 | 29 | - OS + version: e.g. macOS Mojave 30 | - NPM version: ... 31 | - Node version: ... 32 | 33 | ## Checklist: 34 | 35 | - [ ] My code follows the style guidelines of this project 36 | - [ ] I have performed a self-review of my own code 37 | - [ ] I have commented my code, particularly in hard-to-understand areas 38 | - [ ] I have made corresponding changes to the documentation 39 | - [ ] My changes generate no new warnings 40 | - [ ] I have added tests that prove my fix is effective or that my feature works 41 | - [ ] Any dependent changes have been merged and published in downstream modules 42 | - [ ] I have checked my code and corrected any misspellings 43 | -------------------------------------------------------------------------------- /lib/util/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let resolveDefaultConfigPathAlias; 3 | 4 | try { 5 | const { resolveDefaultConfigPath } = require('tailwindcss/lib/util/resolveConfigPath'); 6 | resolveDefaultConfigPathAlias = resolveDefaultConfigPath; 7 | } catch (err) { 8 | resolveDefaultConfigPathAlias = null; 9 | } 10 | 11 | function getOption(context, name) { 12 | // Options (defined at rule level) 13 | const options = context.options[0] || {}; 14 | if (options[name] != undefined) { 15 | return options[name]; 16 | } 17 | // Settings (defined at plugin level, shared accross rules) 18 | if (context.settings && context.settings.tailwindcss && context.settings.tailwindcss[name] != undefined) { 19 | return context.settings.tailwindcss[name]; 20 | } 21 | // Fallback to defaults 22 | switch (name) { 23 | case 'callees': 24 | return ['classnames', 'clsx', 'ctl', 'cva', 'tv']; 25 | case 'ignoredKeys': 26 | return ['compoundVariants', 'defaultVariants']; 27 | case 'classRegex': 28 | return '^class(Name)?$'; 29 | case 'config': 30 | if (resolveDefaultConfigPathAlias === null) { 31 | return 'tailwind.config.js'; 32 | } else { 33 | return resolveDefaultConfigPathAlias(); 34 | } 35 | case 'cssFiles': 36 | return ['**/*.css', '!**/node_modules', '!**/.*', '!**/dist', '!**/build']; 37 | case 'cssFilesRefreshRate': 38 | return 5_000; 39 | case 'removeDuplicates': 40 | return true; 41 | case 'skipClassAttribute': 42 | return false; 43 | case 'tags': 44 | return []; 45 | case 'whitelist': 46 | return []; 47 | } 48 | } 49 | 50 | module.exports = getOption; 51 | -------------------------------------------------------------------------------- /lib/util/types/length.js: -------------------------------------------------------------------------------- 1 | const removeDuplicatesFromArray = require('../removeDuplicatesFromArray'); 2 | 3 | // Units 4 | const fontUnits = ['cap', 'ch', 'em', 'ex', 'ic', 'lh', 'rem', 'rlh']; 5 | const viewportUnits = ['vb', 'vh', 'vi', 'vw', 'vmin', 'vmax']; 6 | const absoluteUnits = ['px', 'mm', 'cm', 'in', 'pt', 'pc']; 7 | const perInchUnits = ['lin', 'pt', 'mm']; 8 | const otherUnits = ['%']; 9 | const mergedUnits = removeDuplicatesFromArray([ 10 | ...fontUnits, 11 | ...viewportUnits, 12 | ...absoluteUnits, 13 | ...perInchUnits, 14 | ...otherUnits, 15 | ]); 16 | const selectedUnits = mergedUnits.filter((el) => { 17 | // All units minus this blacklist 18 | return !['cap', 'ic', 'vb', 'vi'].includes(el); 19 | }); 20 | 21 | const absoluteValues = ['0', 'xx\\-small', 'x\\-small', 'small', 'medium', 'large', 'x\\-large', 'xx\\-large']; 22 | const relativeValues = ['larger', 'smaller']; 23 | const globalValues = ['inherit', 'initial', 'unset']; 24 | const mergedValues = [...absoluteValues, ...relativeValues, ...globalValues]; 25 | 26 | const mergedLengthValues = [`\\-?\\d*\\.?\\d*(${mergedUnits.join('|')})`, ...mergedValues]; 27 | mergedLengthValues.push('length\\:var\\(\\-\\-[a-z\\-]{1,}\\)'); 28 | 29 | const mergedUnitsRegEx = `\\[(\\d{1,}(\\.\\d{1,})?|(\\.\\d{1,})?)(${mergedUnits.join('|')})\\]`; 30 | 31 | const selectedUnitsRegEx = `\\[(\\d{1,}(\\.\\d{1,})?|(\\.\\d{1,})?)(${selectedUnits.join('|')})\\]`; 32 | 33 | const anyCalcRegEx = `\\[calc\\(.{1,}\\)\\]`; 34 | 35 | const validZeroRegEx = `^(0(\\.0{1,})?|\\.0{1,})(${mergedUnits.join('|')})?$`; 36 | 37 | module.exports = { 38 | mergedUnits, 39 | selectedUnits, 40 | mergedUnitsRegEx, 41 | selectedUnitsRegEx, 42 | anyCalcRegEx, 43 | mergedValues, 44 | mergedLengthValues, 45 | validZeroRegEx, 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-tailwindcss", 3 | "version": "3.18.0", 4 | "description": "Rules enforcing best practices while using Tailwind CSS", 5 | "keywords": [ 6 | "eslint", 7 | "eslintplugin", 8 | "eslint-plugin", 9 | "tailwind", 10 | "tailwindcss" 11 | ], 12 | "author": "François Massart", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/francoismassart/eslint-plugin-tailwindcss" 16 | }, 17 | "homepage": "https://github.com/francoismassart/eslint-plugin-tailwindcss", 18 | "bugs": "https://github.com/francoismassart/eslint-plugin-tailwindcss/issues", 19 | "main": "lib/index.js", 20 | "scripts": { 21 | "test": "npm run test:base && npm run test:integration", 22 | "test:base": "mocha \"tests/lib/**/*.js\"", 23 | "test:integration": "mocha \"tests/integrations/*.js\" --timeout 60000" 24 | }, 25 | "files": [ 26 | "lib" 27 | ], 28 | "peerDependencies": { 29 | "tailwindcss": "^3.4.0" 30 | }, 31 | "dependencies": { 32 | "fast-glob": "^3.2.5", 33 | "postcss": "^8.4.4" 34 | }, 35 | "devDependencies": { 36 | "@angular-eslint/template-parser": "^15.2.0", 37 | "@tailwindcss/aspect-ratio": "^0.4.2", 38 | "@tailwindcss/forms": "^0.5.3", 39 | "@tailwindcss/line-clamp": "^0.4.2", 40 | "@tailwindcss/typography": "^0.5.8", 41 | "@typescript-eslint/parser": "^5.50.0", 42 | "autoprefixer": "^10.4.0", 43 | "daisyui": "^2.6.4", 44 | "eslint": "^8.57.0", 45 | "mocha": "^10.2.0", 46 | "semver": "^7.6.0", 47 | "tailwindcss": "^3.4.0", 48 | "typescript": "4.3.5", 49 | "vue-eslint-parser": "^9.4.2" 50 | }, 51 | "packageManager": "npm@10.2.5+sha256.8002e3e7305d2abd4016e1368af48d49b066c269079eeb10a56e7d6598acfdaa", 52 | "engines": { 53 | "node": ">=18.12.0" 54 | }, 55 | "license": "MIT" 56 | } 57 | -------------------------------------------------------------------------------- /lib/util/customConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const resolveConfig = require('tailwindcss/resolveConfig'); 6 | let twLoadConfig; 7 | 8 | try { 9 | twLoadConfig = require('tailwindcss/lib/lib/load-config'); 10 | } catch (err) { 11 | twLoadConfig = null; 12 | } 13 | 14 | // for nativewind preset 15 | process.env.TAILWIND_MODE = 'build'; 16 | 17 | const CHECK_REFRESH_RATE = 1_000; 18 | let lastCheck = null; 19 | let mergedConfig = new Map(); 20 | let lastModifiedDate = new Map(); 21 | 22 | /** 23 | * @see https://stackoverflow.com/questions/9210542/node-js-require-cache-possible-to-invalidate 24 | * @param {string} module The path to the module 25 | * @returns the module's export 26 | */ 27 | function requireUncached(module) { 28 | delete require.cache[require.resolve(module)]; 29 | if (twLoadConfig === null) { 30 | // Using native loading 31 | return require(module); 32 | } else { 33 | // Using Tailwind CSS's loadConfig utility 34 | return twLoadConfig.loadConfig(module); 35 | } 36 | } 37 | 38 | /** 39 | * Load the config from a path string or parsed from an object 40 | * @param {string|Object} config 41 | * @returns `null` when unchanged, `{}` when not found 42 | */ 43 | function loadConfig(config) { 44 | let loadedConfig = null; 45 | if (typeof config === 'string') { 46 | const resolvedPath = path.isAbsolute(config) ? config : path.join(path.resolve(), config); 47 | try { 48 | const stats = fs.statSync(resolvedPath); 49 | const mtime = `${stats.mtime || ''}`; 50 | if (stats === null) { 51 | // Default to no config 52 | loadedConfig = {}; 53 | } else if (lastModifiedDate.get(resolvedPath) !== mtime) { 54 | // Load the config based on path 55 | lastModifiedDate.set(resolvedPath, mtime); 56 | loadedConfig = requireUncached(resolvedPath); 57 | } else { 58 | // Unchanged config 59 | loadedConfig = null; 60 | } 61 | } catch (err) { 62 | // Default to no config 63 | loadedConfig = {}; 64 | } finally { 65 | return loadedConfig; 66 | } 67 | } else { 68 | if (typeof config === 'object' && config !== null) { 69 | return config; 70 | } 71 | return {}; 72 | } 73 | } 74 | 75 | function resolve(twConfig) { 76 | const newConfig = mergedConfig.get(twConfig) === undefined; 77 | const now = Date.now(); 78 | const expired = now - lastCheck > CHECK_REFRESH_RATE; 79 | if (newConfig || expired) { 80 | lastCheck = now; 81 | const userConfig = loadConfig(twConfig); 82 | // userConfig is null when config file was not modified 83 | if (userConfig !== null) { 84 | mergedConfig.set(twConfig, resolveConfig(userConfig)); 85 | } 86 | } 87 | return mergedConfig.get(twConfig); 88 | } 89 | 90 | module.exports = { 91 | resolve, 92 | }; 93 | -------------------------------------------------------------------------------- /lib/util/cssFiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fg = require('fast-glob'); 4 | const fs = require('fs'); 5 | const postcss = require('postcss'); 6 | const lastClassFromSelectorRegexp = /\.([^\.\,\s\n\:\(\)\[\]\'~\+\>\*\\]*)/gim; 7 | const removeDuplicatesFromArray = require('./removeDuplicatesFromArray'); 8 | 9 | const cssFilesInfos = new Map(); 10 | let lastUpdate = null; 11 | let classnamesFromFiles = []; 12 | 13 | /** 14 | * Read CSS files and extract classnames 15 | * @param {Array} patterns Glob patterns to locate files 16 | * @param {Number} refreshRate Interval 17 | * @returns {Array} List of classnames 18 | */ 19 | const generateClassnamesListSync = (patterns, refreshRate = 5_000) => { 20 | const now = Date.now(); 21 | const isExpired = lastUpdate === null || now - lastUpdate > refreshRate; 22 | 23 | if (!isExpired) { 24 | // console.log(`generateClassnamesListSync from cache (${classnamesFromFiles.length} classes)`); 25 | return classnamesFromFiles; 26 | } 27 | 28 | // console.log('generateClassnamesListSync EXPIRED'); 29 | // Update classnames from CSS files 30 | lastUpdate = now; 31 | const filesToBeRemoved = new Set([...cssFilesInfos.keys()]); 32 | const files = fg.sync(patterns, { suppressErrors: true, stats: true }); 33 | for (const file of files) { 34 | let mtime = ''; 35 | let canBeSkipped = cssFilesInfos.has(file.path); 36 | if (canBeSkipped) { 37 | // This file is still used 38 | filesToBeRemoved.delete(file.path); 39 | // Check modification date 40 | const stats = fs.statSync(file.path); 41 | mtime = `${stats.mtime || ''}`; 42 | canBeSkipped = cssFilesInfos.get(file.path).mtime === mtime; 43 | } 44 | if (canBeSkipped) { 45 | // File did not change since last run 46 | continue; 47 | } 48 | // Parse CSS file 49 | const data = fs.readFileSync(file.path, 'utf-8'); 50 | const root = postcss.parse(data); 51 | let detectedClassnames = new Set(); 52 | root.walkRules((rule) => { 53 | const matches = [...rule.selector.matchAll(lastClassFromSelectorRegexp)]; 54 | const classnames = matches.map((arr) => arr[1]); 55 | detectedClassnames = new Set([...detectedClassnames, ...classnames]); 56 | }); 57 | // Save the detected classnames 58 | cssFilesInfos.set(file.path, { 59 | mtime: mtime, 60 | classNames: [...detectedClassnames], 61 | }); 62 | } 63 | // Remove erased CSS from the Map 64 | const deletedFiles = [...filesToBeRemoved]; 65 | for (let i = 0; i < deletedFiles.length; i++) { 66 | cssFilesInfos.delete(deletedFiles[i]); 67 | } 68 | // Build the final list 69 | classnamesFromFiles = []; 70 | cssFilesInfos.forEach((css) => { 71 | classnamesFromFiles = [...classnamesFromFiles, ...css.classNames]; 72 | }); 73 | // Unique classnames 74 | return removeDuplicatesFromArray(classnamesFromFiles); 75 | }; 76 | 77 | module.exports = generateClassnamesListSync; 78 | -------------------------------------------------------------------------------- /docs/rules/no-arbitrary-value.md: -------------------------------------------------------------------------------- 1 | # Forbid using arbitrary values in classnames (no-arbitrary-value) 2 | 3 | Tailwind CSS 3 is Just In Time, all the time. It brings flexibility, great compilation perfs and arbitrary values. 4 | Arbitrary values are great but can be problematic too if you wish to restrict developer to stick with the values defined in the Tailwind CSS config file. 5 | 6 | **By default this rule is turned `off`, if you want to use it set it to `warn` or `error`.** 7 | 8 | ## Rule Details 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```html 13 |
border width
14 | ``` 15 | 16 | Examples of **correct** code for this rule: 17 | 18 | ```html 19 |
border width
20 | ``` 21 | 22 | ### Options 23 | 24 | ```js 25 | ... 26 | "tailwindcss/no-arbitrary-value": [, { 27 | "callees": Array, 28 | "config": |, 29 | "skipClassAttribute": , 30 | "tags": Array, 31 | }] 32 | ... 33 | ``` 34 | 35 | ### `callees` (default: `["classnames", "clsx", "ctl", "cva", "tv"]`) 36 | 37 | If you use some utility library like [@netlify/classnames-template-literals](https://github.com/netlify/classnames-template-literals), you can add its name to the list to make sure it gets parsed by this rule. 38 | 39 | For best results, gather the declarative classnames together, avoid mixing conditional classnames in between, move them at the end. 40 | 41 | ### `ignoredKeys` (default: `["compoundVariants", "defaultVariants"]`) 42 | 43 | Using libraries like `cva`, some of its object keys are not meant to contain classnames in its value(s). 44 | You can specify which key(s) won't be parsed by the plugin using this setting. 45 | For example, `cva` has `compoundVariants` and `defaultVariants`. 46 | NB: As `compoundVariants` can have classnames inside its `class` property, you can also use a callee to make sure this inner part gets parsed while its parent is ignored. 47 | 48 | ### `config` (default: generated by `tailwindcss/lib/lib/load-config`) 49 | 50 | By default the plugin will try to load the file returned by the official `loadConfig()` utility. 51 | 52 | This allows the plugin to use your customized `colors`, `spacing`, `screens`... 53 | 54 | You can provide another path or filename for your Tailwind CSS config file like `"config/tailwind.js"`. 55 | 56 | If the external file cannot be loaded (e.g. incorrect path or deleted file), an empty object `{}` will be used instead. 57 | 58 | It is also possible to directly inject a configuration as plain `object` like `{ prefix: "tw-", theme: { ... } }`. 59 | 60 | Finally, the plugin will [merge the provided configuration](https://tailwindcss.com/docs/configuration#referencing-in-java-script) with [Tailwind CSS's default configuration](https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js). 61 | 62 | ### `skipClassAttribute` (default: `false`) 63 | 64 | Set `skipClassAttribute` to `true` if you only want to lint the classnames inside one of the `callees`. 65 | While, this will avoid linting the `class` and `className` attributes, it will still lint matching `callees` inside of these attributes. 66 | 67 | ### `tags` (default: `[]`) 68 | 69 | Optional, if you are using tagged templates, you should provide the tags in this array. 70 | 71 | ### `classRegex` (default: `"^class(Name)?$"`) 72 | 73 | Optional, can be used to support custom attributes 74 | 75 | ## Further Reading 76 | 77 | This rule will not fix the issue for you because it cannot guess the correct class candidate. 78 | -------------------------------------------------------------------------------- /docs/rules/no-contradicting-classname.md: -------------------------------------------------------------------------------- 1 | # Avoid contradicting Tailwind CSS classnames (e.g. "w-3 w-5") (no-contradicting-classname) 2 | 3 | The majority of the Tailwind CSS classes only affect a single CSS property. 4 | Using two or more classnames which affect the same property for the same variant means trouble and confusion. 5 | 6 | ## Rule Details 7 | 8 | The rule aims to warn you about contradictions in the classnames you are attaching to an element. 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```html 13 |
which is the correct width ?
14 | ``` 15 | 16 | Examples of **correct** code for this rule: 17 | 18 | ```html 19 |
padding is defined once per variant max.
20 | ``` 21 | 22 | ### Options 23 | 24 | ```js 25 | ... 26 | "tailwindcss/no-contradicting-classname": [, { 27 | "callees": Array, 28 | "config": |, 29 | "skipClassAttribute": , 30 | "tags": Array, 31 | }] 32 | ... 33 | ``` 34 | 35 | ### `callees` (default: `["classnames", "clsx", "ctl", "cva", "tv"]`) 36 | 37 | If you use some utility library like [@netlify/classnames-template-literals](https://github.com/netlify/classnames-template-literals), you can add its name to the list to make sure it gets parsed by this rule. 38 | 39 | For best results, gather the declarative classnames together, avoid mixing conditional classnames in between, move them at the end. 40 | 41 | ### `ignoredKeys` (default: `["compoundVariants", "defaultVariants"]`) 42 | 43 | Using libraries like `cva`, some of its object keys are not meant to contain classnames in its value(s). 44 | You can specify which key(s) won't be parsed by the plugin using this setting. 45 | For example, `cva` has `compoundVariants` and `defaultVariants`. 46 | NB: As `compoundVariants` can have classnames inside its `class` property, you can also use a callee to make sure this inner part gets parsed while its parent is ignored. 47 | 48 | ### `config` (default: generated by `tailwindcss/lib/lib/load-config`) 49 | 50 | By default the plugin will try to load the file returned by the official `loadConfig()` utility. 51 | 52 | This allows the plugin to use your customized `colors`, `spacing`, `screens`... 53 | 54 | You can provide another path or filename for your Tailwind CSS config file like `"config/tailwind.js"`. 55 | 56 | If the external file cannot be loaded (e.g. incorrect path or deleted file), an empty object `{}` will be used instead. 57 | 58 | It is also possible to directly inject a configuration as plain `object` like `{ prefix: "tw-", theme: { ... } }`. 59 | 60 | Finally, the plugin will [merge the provided configuration](https://tailwindcss.com/docs/configuration#referencing-in-java-script) with [Tailwind CSS's default configuration](https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js). 61 | 62 | ### `skipClassAttribute` (default: `false`) 63 | 64 | Set `skipClassAttribute` to `true` if you only want to lint the classnames inside one of the `callees`. 65 | While, this will avoid linting the `class` and `className` attributes, it will still lint matching `callees` inside of these attributes. 66 | 67 | ### `tags` (default: `[]`) 68 | 69 | Optional, if you are using tagged templates, you should provide the tags in this array. 70 | 71 | ### `classRegex` (default: `"^class(Name)?$"`) 72 | 73 | Optional, can be used to support custom attributes 74 | 75 | ## Further Reading 76 | 77 | This rule will not fix the issue but will let you know about the issue. 78 | -------------------------------------------------------------------------------- /docs/rules/classnames-order.md: -------------------------------------------------------------------------------- 1 | # Use a consistent orders for the Tailwind CSS classnames, based on the official order (tailwindcss/classnames-order) 2 | 3 | Enforces a consistent order of the Tailwind CSS classnames and its variants. 4 | 5 | > **Note**: Since version `3.6.0`, the ordering is solely done using the [order process from the official `prettier-plugin-tailwindcss`](https://tailwindcss.com/blog/automatic-class-sorting-with-prettier#how-classes-are-sorted) 6 | 7 | ## Rule Details 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```html 12 |
13 | ``` 14 | 15 | Examples of **correct** code for this rule: 16 | 17 | ```html 18 |
19 | ``` 20 | 21 | ### Options 22 | 23 | ```js 24 | ... 25 | "tailwindcss/classnames-order": [, { 26 | "callees": Array, 27 | "config": |, 28 | "removeDuplicates": , 29 | "skipClassAttribute": , 30 | "tags": Array, 31 | }] 32 | ... 33 | ``` 34 | 35 | ### `callees` (default: `["classnames", "clsx", "ctl", "cva", "tv"]`) 36 | 37 | If you use some utility library like [@netlify/classnames-template-literals](https://github.com/netlify/classnames-template-literals), you can add its name to the list to make sure it gets parsed by this rule. 38 | 39 | For best results, gather the declarative classnames together, avoid mixing conditional classnames in between, move them at the end. 40 | 41 | ### `ignoredKeys` (default: `["compoundVariants", "defaultVariants"]`) 42 | 43 | Using libraries like `cva`, some of its object keys are not meant to contain classnames in its value(s). 44 | You can specify which key(s) won't be parsed by the plugin using this setting. 45 | For example, `cva` has `compoundVariants` and `defaultVariants`. 46 | NB: As `compoundVariants` can have classnames inside its `class` property, you can also use a callee to make sure this inner part gets parsed while its parent is ignored. 47 | 48 | ### `config` (default: generated by `tailwindcss/lib/lib/load-config`) 49 | 50 | By default the plugin will try to load the file returned by the official `loadConfig()` utility. 51 | 52 | This allows the plugin to use your customized `colors`, `spacing`, `screens`... 53 | 54 | You can provide another path or filename for your Tailwind CSS config file like `"config/tailwind.js"`. 55 | 56 | If the external file cannot be loaded (e.g. incorrect path or deleted file), an empty object `{}` will be used instead. 57 | 58 | It is also possible to directly inject a configuration as plain `object` like `{ prefix: "tw-", theme: { ... } }`. 59 | 60 | Finally, the plugin will [merge the provided configuration](https://tailwindcss.com/docs/configuration#referencing-in-java-script) with [Tailwind CSS's default configuration](https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js). 61 | 62 | ### `removeDuplicates` (default: `true`) 63 | 64 | Duplicate classnames are automatically removed but you can always disable this behavior by setting `removeDuplicates` to `false`. 65 | 66 | ### `skipClassAttribute` (default: `false`) 67 | 68 | Set `skipClassAttribute` to `true` if you only want to lint the classnames inside one of the `callees`. 69 | While, this will avoid linting the `class` and `className` attributes, it will still lint matching `callees` inside of these attributes. 70 | 71 | ### `tags` (default: `[]`) 72 | 73 | Optional, if you are using tagged templates, you should provide the tags in this array. 74 | 75 | ### `classRegex` (default: `"^class(Name)?$"`) 76 | 77 | Optional, can be used to support custom attributes 78 | -------------------------------------------------------------------------------- /lib/util/prettier/order.js: -------------------------------------------------------------------------------- 1 | const { separatorRegEx } = require('../regex'); 2 | 3 | function bigSign(bigIntValue) { 4 | return (bigIntValue > 0n) - (bigIntValue < 0n); 5 | } 6 | 7 | function prefixCandidate(context, selector) { 8 | let prefix = context.tailwindConfig.prefix; 9 | return typeof prefix === 'function' ? prefix(selector) : prefix + selector; 10 | } 11 | 12 | // Polyfill for older Tailwind CSS versions 13 | function getClassOrderPolyfill(classes, { env }) { 14 | // A list of utilities that are used by certain Tailwind CSS utilities but 15 | // that don't exist on their own. This will result in them "not existing" and 16 | // sorting could be weird since you still require them in order to make the 17 | // host utitlies work properly. (Thanks Biology) 18 | let parasiteUtilities = new Set([prefixCandidate(env.context, 'group'), prefixCandidate(env.context, 'peer')]); 19 | 20 | let classNamesWithOrder = []; 21 | 22 | for (let className of classes) { 23 | let order = env.generateRules(new Set([className]), env.context).sort(([a], [z]) => bigSign(z - a))[0]?.[0] ?? null; 24 | 25 | if (order === null && parasiteUtilities.has(className)) { 26 | // This will make sure that it is at the very beginning of the 27 | // `components` layer which technically means 'before any 28 | // components'. 29 | order = env.context.layerOrder.components; 30 | } 31 | 32 | classNamesWithOrder.push([className, order]); 33 | } 34 | 35 | return classNamesWithOrder; 36 | } 37 | 38 | function sortClasses(classStr, { env, ignoreFirst = false, ignoreLast = false }) { 39 | if (typeof classStr !== 'string' || classStr === '') { 40 | return classStr; 41 | } 42 | 43 | // Ignore class attributes containing `{{`, to match Prettier behaviour: 44 | // https://github.com/prettier/prettier/blob/main/src/language-html/embed.js#L83-L88 45 | if (classStr.includes('{{')) { 46 | return classStr; 47 | } 48 | 49 | let result = ''; 50 | let parts = classStr.split(separatorRegEx); 51 | let classes = parts.filter((_, i) => i % 2 === 0); 52 | let whitespace = parts.filter((_, i) => i % 2 !== 0); 53 | 54 | if (classes[classes.length - 1] === '') { 55 | classes.pop(); 56 | } 57 | 58 | let prefix = ''; 59 | if (ignoreFirst) { 60 | prefix = `${classes.shift() ?? ''}${whitespace.shift() ?? ''}`; 61 | } 62 | 63 | let suffix = ''; 64 | if (ignoreLast) { 65 | suffix = `${whitespace.pop() ?? ''}${classes.pop() ?? ''}`; 66 | } 67 | 68 | let classNamesWithOrder = env.context.getClassOrder 69 | ? env.context.getClassOrder(classes) 70 | : getClassOrderPolyfill(classes, { env }); 71 | 72 | classes = classNamesWithOrder 73 | .sort(([, a], [, z]) => { 74 | if (a === z) return 0; 75 | // if (a === null) return options.unknownClassPosition === 'start' ? -1 : 1 76 | // if (z === null) return options.unknownClassPosition === 'start' ? 1 : -1 77 | if (a === null) return -1; 78 | if (z === null) return 1; 79 | return bigSign(a - z); 80 | }) 81 | .map(([className]) => className); 82 | 83 | for (let i = 0; i < classes.length; i++) { 84 | result += `${classes[i]}${whitespace[i] ?? ''}`; 85 | } 86 | 87 | return prefix + result + suffix; 88 | } 89 | 90 | /** 91 | * 92 | * @param {Array} unordered the unordered classnames 93 | * @param context 94 | * @returns {Array} the ordered classnames 95 | */ 96 | function order(unordered, context) { 97 | const sorted = sortClasses(unordered.join(' '), { env: { context: context } }); 98 | return sorted; 99 | } 100 | 101 | module.exports = order; 102 | -------------------------------------------------------------------------------- /docs/rules/enforces-shorthand.md: -------------------------------------------------------------------------------- 1 | # Replaces multiple Tailwind CSS classnames by their shorthand (enforces-shorthand) 2 | 3 | This rule will help you reduce the number of [Tailwind CSS](https://tailwindcss.com/) classnames by using shorthands. 4 | 5 | ## Rule Details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```html 10 |
border shorthand
11 | ``` 12 | 13 | Examples of **correct** code for this rule: 14 | 15 | ```html 16 |
border shorthand
17 | ``` 18 | 19 | #### Limitations 20 | 21 | At the moment, the rule will not merge mixed classnames (e.g. using regular values AND arbitrary values). 22 | 23 | ```html 24 |
25 | won't be converted to border-0 shorthand 26 |
27 | ``` 28 | 29 | Also, unless you are using the `classnames-order` rule, the order of your classnames may be affected by the fix. 30 | If indeed, you are using the `classnames-order` rule, then it'll be automatically re-ordered during the next lint process (depending on your autofix preferences) and you won't notice any order issue. 31 | 32 | ### Options 33 | 34 | ```js 35 | ... 36 | "tailwindcss/enforces-shorthand": [, { 37 | "callees": Array, 38 | "config": |, 39 | "skipClassAttribute": , 40 | "tags": Array, 41 | }] 42 | ... 43 | ``` 44 | 45 | ### `callees` (default: `["classnames", "clsx", "ctl", "cva", "tv"]`) 46 | 47 | If you use some utility library like [@netlify/classnames-template-literals](https://github.com/netlify/classnames-template-literals), you can add its name to the list to make sure it gets parsed by this rule. 48 | 49 | For best results, gather the declarative classnames together, avoid mixing conditional classnames in between, move them at the end. 50 | 51 | ### `ignoredKeys` (default: `["compoundVariants", "defaultVariants"]`) 52 | 53 | Using libraries like `cva`, some of its object keys are not meant to contain classnames in its value(s). 54 | You can specify which key(s) won't be parsed by the plugin using this setting. 55 | For example, `cva` has `compoundVariants` and `defaultVariants`. 56 | NB: As `compoundVariants` can have classnames inside its `class` property, you can also use a callee to make sure this inner part gets parsed while its parent is ignored. 57 | 58 | ### `config` (default: generated by `tailwindcss/lib/lib/load-config`) 59 | 60 | By default the plugin will try to load the file returned by the official `loadConfig()` utility. 61 | 62 | This allows the plugin to use your customized `colors`, `spacing`, `screens`... 63 | 64 | You can provide another path or filename for your Tailwind CSS config file like `"config/tailwind.js"`. 65 | 66 | If the external file cannot be loaded (e.g. incorrect path or deleted file), an empty object `{}` will be used instead. 67 | 68 | It is also possible to directly inject a configuration as plain `object` like `{ prefix: "tw-", theme: { ... } }`. 69 | 70 | Finally, the plugin will [merge the provided configuration](https://tailwindcss.com/docs/configuration#referencing-in-java-script) with [Tailwind CSS's default configuration](https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js). 71 | 72 | ### `skipClassAttribute` (default: `false`) 73 | 74 | Set `skipClassAttribute` to `true` if you only want to lint the classnames inside one of the `callees`. 75 | While, this will avoid linting the `class` and `className` attributes, it will still lint matching `callees` inside of these attributes. 76 | 77 | ### `tags` (default: `[]`) 78 | 79 | Optional, if you are using tagged templates, you should provide the tags in this array. 80 | 81 | ### `classRegex` (default: `"^class(Name)?$"`) 82 | 83 | Optional, can be used to support custom attributes 84 | 85 | ## Further Reading 86 | 87 | This rule will fix the issue for you. 88 | -------------------------------------------------------------------------------- /docs/rules/enforces-negative-arbitrary-values.md: -------------------------------------------------------------------------------- 1 | # Warns about `-` prefixed classnames using arbitrary values (enforces-negative-arbitrary-values) 2 | 3 | There are 2 ways to declare **negative arbitrary values**: 4 | 5 | a) Dash prefixed classname with absolute arbitrary value like `-top-[1px]` ❌ 6 | 7 | b) Unprefixed classname (no dash) with negative value inside the square brackets like `top-[-1px]` ✅ 8 | 9 | I believe, **we should always prefer the (b) approach "Unprefixed classname"** for few reasons: 10 | 11 | - In Tailwind CSS **v2.x.x** the (a) **was not supported** (example: https://play.tailwindcss.com/fsS91hkyKx) 12 | - You can get nasty using (a) like `-top-[-1px]` 🥴 13 | - Using `var()` you simply don't know if you are dealing with a negative or positive value 14 | - [Adam recommends the unprefixed approach 🎉](https://twitter.com/adamwathan/status/1487895306847105038) 15 | 16 | ## Rule Details 17 | 18 | Examples of **incorrect** code for this rule: 19 | 20 | ```html 21 |
24 | Negative arbitrary values 25 |
26 | ``` 27 | 28 | `-right-[var(--my-var)*-1]` will generate this non sense: `right: calc(var(--my-var) * -1 * -1);` 29 | 30 | Examples of **correct** code for this rule: 31 | 32 | ```html 33 |
36 | Negative arbitrary values 37 |
38 | ``` 39 | 40 | ### Options 41 | 42 | ```js 43 | ... 44 | "tailwindcss/enforces-negative-arbitrary-values": [, { 45 | "callees": Array, 46 | "config": |, 47 | "skipClassAttribute": , 48 | "tags": Array, 49 | }] 50 | ... 51 | ``` 52 | 53 | ### `callees` (default: `["classnames", "clsx", "ctl", "cva", "tv"]`) 54 | 55 | If you use some utility library like [@netlify/classnames-template-literals](https://github.com/netlify/classnames-template-literals), you can add its name to the list to make sure it gets parsed by this rule. 56 | 57 | For best results, gather the declarative classnames together, avoid mixing conditional classnames in between, move them at the end. 58 | 59 | ### `ignoredKeys` (default: `["compoundVariants", "defaultVariants"]`) 60 | 61 | Using libraries like `cva`, some of its object keys are not meant to contain classnames in its value(s). 62 | You can specify which key(s) won't be parsed by the plugin using this setting. 63 | For example, `cva` has `compoundVariants` and `defaultVariants`. 64 | NB: As `compoundVariants` can have classnames inside its `class` property, you can also use a callee to make sure this inner part gets parsed while its parent is ignored. 65 | 66 | ### `config` (default: generated by `tailwindcss/lib/lib/load-config`) 67 | 68 | By default the plugin will try to load the file returned by the official `loadConfig()` utility. 69 | 70 | This allows the plugin to use your customized `colors`, `spacing`, `screens`... 71 | 72 | You can provide another path or filename for your Tailwind CSS config file like `"config/tailwind.js"`. 73 | 74 | If the external file cannot be loaded (e.g. incorrect path or deleted file), an empty object `{}` will be used instead. 75 | 76 | It is also possible to directly inject a configuration as plain `object` like `{ prefix: "tw-", theme: { ... } }`. 77 | 78 | Finally, the plugin will [merge the provided configuration](https://tailwindcss.com/docs/configuration#referencing-in-java-script) with [Tailwind CSS's default configuration](https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js). 79 | 80 | ### `skipClassAttribute` (default: `false`) 81 | 82 | Set `skipClassAttribute` to `true` if you only want to lint the classnames inside one of the `callees`. 83 | While, this will avoid linting the `class` and `className` attributes, it will still lint matching `callees` inside of these attributes. 84 | 85 | ### `tags` (default: `[]`) 86 | 87 | Optional, if you are using tagged templates, you should provide the tags in this array. 88 | 89 | ### `classRegex` (default: `"^class(Name)?$"`) 90 | 91 | Optional, can be used to support custom attributes 92 | -------------------------------------------------------------------------------- /lib/util/types/color.js: -------------------------------------------------------------------------------- 1 | const cssNamedColors = [ 2 | 'indianred', 3 | 'lightcoral', 4 | 'salmon', 5 | 'darksalmon', 6 | 'lightsalmon', 7 | 'crimson', 8 | 'red', 9 | 'firebrick', 10 | 'darkred', 11 | 'pink', 12 | 'lightpink', 13 | 'hotpink', 14 | 'deeppink', 15 | 'mediumvioletred', 16 | 'palevioletred', 17 | 'coral', 18 | 'tomato', 19 | 'orangered', 20 | 'darkorange', 21 | 'orange', 22 | 'gold', 23 | 'yellow', 24 | 'lightyellow', 25 | 'lemonchiffon', 26 | 'lightgoldenrodyellow', 27 | 'papayawhip', 28 | 'moccasin', 29 | 'peachpuff', 30 | 'palegoldenrod', 31 | 'khaki', 32 | 'darkkhaki', 33 | 'lavender', 34 | 'thistle', 35 | 'plum', 36 | 'violet', 37 | 'orchid', 38 | 'fuchsia', 39 | 'magenta', 40 | 'mediumorchid', 41 | 'mediumpurple', 42 | 'blueviolet', 43 | 'darkviolet', 44 | 'darkorchid', 45 | 'darkmagenta', 46 | 'purple', 47 | 'rebeccapurple', 48 | 'indigo', 49 | 'mediumslateblue', 50 | 'slateblue', 51 | 'darkslateblue', 52 | 'greenyellow', 53 | 'chartreuse', 54 | 'lawngreen', 55 | 'lime', 56 | 'limegreen', 57 | 'palegreen', 58 | 'lightgreen', 59 | 'mediumspringgreen', 60 | 'springgreen', 61 | 'mediumseagreen', 62 | 'seagreen', 63 | 'forestgreen', 64 | 'green', 65 | 'darkgreen', 66 | 'yellowgreen', 67 | 'olivedrab', 68 | 'olive', 69 | 'darkolivegreen', 70 | 'mediumaquamarine', 71 | 'darkseagreen', 72 | 'lightseagreen', 73 | 'darkcyan', 74 | 'teal', 75 | 'aqua', 76 | 'cyan', 77 | 'lightcyan', 78 | 'paleturquoise', 79 | 'aquamarine', 80 | 'turquoise', 81 | 'mediumturquoise', 82 | 'darkturquoise', 83 | 'cadetblue', 84 | 'steelblue', 85 | 'lightsteelblue', 86 | 'powderblue', 87 | 'lightblue', 88 | 'skyblue', 89 | 'lightskyblue', 90 | 'deepskyblue', 91 | 'dodgerblue', 92 | 'cornflowerblue', 93 | 'royalblue', 94 | 'blue', 95 | 'mediumblue', 96 | 'darkblue', 97 | 'navy', 98 | 'midnightblue', 99 | 'cornsilk', 100 | 'blanchedalmond', 101 | 'bisque', 102 | 'navajowhite', 103 | 'wheat', 104 | 'burlywood', 105 | 'tan', 106 | 'rosybrown', 107 | 'sandybrown', 108 | 'goldenrod', 109 | 'darkgoldenrod', 110 | 'peru', 111 | 'chocolate', 112 | 'saddlebrown', 113 | 'sienna', 114 | 'brown', 115 | 'maroon', 116 | 'white', 117 | 'snow', 118 | 'honeydew', 119 | 'mintcream', 120 | 'azure', 121 | 'aliceblue', 122 | 'ghostwhite', 123 | 'whitesmoke', 124 | 'seashell', 125 | 'beige', 126 | 'oldlace', 127 | 'floralwhite', 128 | 'ivory', 129 | 'antiquewhite', 130 | 'linen', 131 | 'lavenderblush', 132 | 'mistyrose', 133 | 'gainsboro', 134 | 'lightgray', 135 | 'lightgrey', 136 | 'silver', 137 | 'darkgray', 138 | 'darkgrey', 139 | 'gray', 140 | 'grey', 141 | 'dimgray', 142 | 'dimgrey', 143 | 'lightslategray', 144 | 'lightslategrey', 145 | 'slategray', 146 | 'slategrey', 147 | 'darkslategray', 148 | 'darkslategrey', 149 | 'black', 150 | 'transparent', 151 | 'currentColor', 152 | ]; 153 | 154 | // RGB[A] hexa: #123456AA, #B4DA55, #000A, #123 155 | const hexRGBA = '\\#(([0-9A-Fa-f]{8})|([0-9A-Fa-f]{6})|([0-9A-Fa-f]{4})|([0-9A-Fa-f]{3}))'; 156 | 157 | // RGB 0-255: rgb(10,20,30) 158 | const RGBIntegers = 'rgb\\(\\d{1,3}\\,\\d{1,3}\\,\\d{1,3}\\)'; 159 | 160 | // RGB %: rgb(25%,50%,75%) 161 | const RGBPercentages = 'rgb\\(\\d{1,3}%\\,\\d{1,3}%\\,\\d{1,3}%\\)'; 162 | 163 | // RGBA: rgba(50,100,255,.5), rgba(50,100,255,50%) 164 | const supportedRGBA = 'rgba\\(\\d{1,3}\\,\\d{1,3}\\,\\d{1,3}\\,\\d*(\\.\\d*)?%?\\)'; 165 | 166 | const RGBAPercentages = 'rgba\\(\\d{1,3}%\\,\\d{1,3}%\\,\\d{1,3}%\\,\\d*(\\.\\d*)?%?\\)'; 167 | 168 | const optionalColorPrefixedVar = '(color\\:)?var\\(\\-\\-[A-Za-z\\-]{1,}\\)'; 169 | 170 | const mandatoryColorPrefixed = 'color\\:(?!(hsla\\()).{1,}'; 171 | 172 | const notHSLAPlusWildcard = '(?!(hsla\\()).{1,}'; 173 | 174 | // HSL 175 | const supportedHSL = 'hsl\\(\\d{1,3}%?\\,\\d{1,3}%?\\,\\d{1,3}%?\\)'; 176 | 177 | // 'hsla\\(\\d{1,3}%?\\,\\d{1,3}%?\\,\\d{1,3}%?\\,\\d*(\\.\\d*)?%?\\)', 178 | 179 | const colorValues = [hexRGBA, RGBIntegers, RGBPercentages, supportedRGBA, supportedHSL]; 180 | 181 | const mergedColorValues = [...cssNamedColors, ...colorValues]; 182 | 183 | module.exports = { 184 | cssNamedColors, 185 | colorValues, 186 | mergedColorValues, 187 | RGBAPercentages, 188 | optionalColorPrefixedVar, 189 | mandatoryColorPrefixed, 190 | notHSLAPlusWildcard, 191 | }; 192 | -------------------------------------------------------------------------------- /docs/rules/no-unnecessary-arbitrary-value.md: -------------------------------------------------------------------------------- 1 | # Avoid unjustified arbitrary classnames (no-unnecessary-arbitrary-value) 2 | 3 | Arbitrary values are handy but you should stick to regular classnames defined in the Tailwind CSS config file as much as you can. 4 | 5 | ## Rule Details 6 | 7 | Given the default configuration in which `h-auto` exists... There is no need to use an arbitrary classname. 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```html 12 |
height
13 | ``` 14 | 15 | Examples of **correct** code for this rule: 16 | 17 | ```html 18 |
height
19 | ``` 20 | 21 | ### The rule can handle `0` values with or without their units 22 | 23 | Given the default configuration in which `h-0` exists... There is no need to use an arbitrary classname. 24 | 25 | Examples of **incorrect** code with `0` based value: 26 | 27 | ```html 28 |
Use `h-0` (`0px`) instead
29 | ``` 30 | 31 | Examples of **correct** code with `0` based value: 32 | 33 | ```html 34 | 35 | ``` 36 | 37 | ### The rule can handle negative & double negative 38 | 39 | Given the default configuration... There is no need to use an arbitrary classname. 40 | 41 | Examples of **incorrect** code for negative arbitrary values: 42 | 43 | ```html 44 |
[Double] negative values
45 | ``` 46 | 47 | Examples of **correct** code for negative arbitrary values: 48 | 49 | ```html 50 |
[Double] negative values
51 | ``` 52 | 53 | ### Options 54 | 55 | ```js 56 | ... 57 | "tailwindcss/no-unnecessary-arbitrary-value": [, { 58 | "callees": Array, 59 | "config": |, 60 | "skipClassAttribute": , 61 | "tags": Array, 62 | }] 63 | ... 64 | ``` 65 | 66 | ### `callees` (default: `["classnames", "clsx", "ctl", "cva", "tv"]`) 67 | 68 | If you use some utility library like [@netlify/classnames-template-literals](https://github.com/netlify/classnames-template-literals), you can add its name to the list to make sure it gets parsed by this rule. 69 | 70 | For best results, gather the declarative classnames together, avoid mixing conditional classnames in between, move them at the end. 71 | 72 | ### `ignoredKeys` (default: `["compoundVariants", "defaultVariants"]`) 73 | 74 | Using libraries like `cva`, some of its object keys are not meant to contain classnames in its value(s). 75 | You can specify which key(s) won't be parsed by the plugin using this setting. 76 | For example, `cva` has `compoundVariants` and `defaultVariants`. 77 | NB: As `compoundVariants` can have classnames inside its `class` property, you can also use a callee to make sure this inner part gets parsed while its parent is ignored. 78 | 79 | ### `config` (default: generated by `tailwindcss/lib/lib/load-config`) 80 | 81 | By default the plugin will try to load the file returned by the official `loadConfig()` utility. 82 | 83 | This allows the plugin to use your customized `colors`, `spacing`, `screens`... 84 | 85 | You can provide another path or filename for your Tailwind CSS config file like `"config/tailwind.js"`. 86 | 87 | If the external file cannot be loaded (e.g. incorrect path or deleted file), an empty object `{}` will be used instead. 88 | 89 | It is also possible to directly inject a configuration as plain `object` like `{ prefix: "tw-", theme: { ... } }`. 90 | 91 | Finally, the plugin will [merge the provided configuration](https://tailwindcss.com/docs/configuration#referencing-in-java-script) with [Tailwind CSS's default configuration](https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js). 92 | 93 | ### `skipClassAttribute` (default: `false`) 94 | 95 | Set `skipClassAttribute` to `true` if you only want to lint the classnames inside one of the `callees`. 96 | While, this will avoid linting the `class` and `className` attributes, it will still lint matching `callees` inside of these attributes. 97 | 98 | ### `tags` (default: `[]`) 99 | 100 | Optional, if you are using tagged templates, you should provide the tags in this array. 101 | 102 | ### `classRegex` (default: `"^class(Name)?$"`) 103 | 104 | Optional, can be used to support custom attributes 105 | 106 | ## Further Reading 107 | 108 | If there is exactly one equivalent regular classname, this rule will fix the issue for you by replacing the arbitrary classnames by their unique substitutes. 109 | 110 | But if there are several possible substitutes for an arbitrary classname, then you can manually perform the replacement. 111 | -------------------------------------------------------------------------------- /tests/lib/rules/no-arbitrary-value.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Forbid using arbitrary values in classnames 3 | * @author François Massart 4 | */ 5 | "use strict"; 6 | 7 | //------------------------------------------------------------------------------ 8 | // Requirements 9 | //------------------------------------------------------------------------------ 10 | 11 | var rule = require("../../../lib/rules/no-arbitrary-value"); 12 | var RuleTester = require("eslint").RuleTester; 13 | 14 | //------------------------------------------------------------------------------ 15 | // Tests 16 | //------------------------------------------------------------------------------ 17 | 18 | var parserOptions = { 19 | ecmaVersion: 2019, 20 | sourceType: "module", 21 | ecmaFeatures: { 22 | jsx: true, 23 | }, 24 | }; 25 | 26 | const skipClassAttributeOptions = [ 27 | { 28 | skipClassAttribute: true, 29 | config: { 30 | theme: {}, 31 | plugins: [], 32 | }, 33 | }, 34 | ]; 35 | 36 | var generateErrors = (classnames) => { 37 | const errors = []; 38 | if (typeof classnames === "string") { 39 | classnames = classnames.split(" "); 40 | } 41 | classnames.map((classname) => { 42 | errors.push({ 43 | messageId: "arbitraryValueDetected", 44 | data: { 45 | classname: classname, 46 | }, 47 | }); 48 | }); 49 | return errors; 50 | }; 51 | 52 | var ruleTester = new RuleTester({ parserOptions }); 53 | 54 | ruleTester.run("no-arbitrary-value", rule, { 55 | valid: [ 56 | { 57 | code: `
No arbitrary value
`, 58 | }, 59 | { 60 | code: `
No errors while typing
`, 61 | }, 62 | { 63 | code: `
Skip class attribute
`, 64 | options: skipClassAttributeOptions, 65 | }, 66 | { 67 | code: `
Issue #318
`, 68 | }, 69 | ], 70 | 71 | invalid: [ 72 | { 73 | code: `
Arbitrary width!
`, 74 | errors: generateErrors("w-[10px]"), 75 | }, 76 | { 77 | code: `
Arbitrary width in named group!
`, 78 | errors: generateErrors("group/name:w-[10px]"), 79 | }, 80 | { 81 | code: `
Arbitrary width!
`, 82 | errors: generateErrors("w-[10px]"), 83 | }, 84 | { 85 | code: `
Arbitrary values!
`, 86 | errors: generateErrors("bg-[rgba(10,20,30,0.5)] [mask-type:luminance]"), 87 | }, 88 | { 89 | code: `ctl(\` 90 | [mask-type:luminance] 91 | container 92 | flex 93 | bg-[rgba(10,20,30,0.5)] 94 | w-12 95 | sm:w-6 96 | lg:w-4 97 | \`)`, 98 | errors: generateErrors("[mask-type:luminance] bg-[rgba(10,20,30,0.5)]"), 99 | }, 100 | { 101 | code: ` 102 |