├── .changeset
├── README.md
└── config.json
├── .eslint-doc-generatorrc.js
├── .github
└── workflows
│ ├── main.yml
│ └── publish.yml
├── .gitignore
├── .markdownlint.json
├── .markdownlintignore
├── .prettierrc.js
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs
└── rules
│ ├── forbid-array-expressions.md
│ ├── forbid-false-inside-object-expressions.md
│ ├── forbid-true-inside-object-expressions.md
│ ├── no-redundant-clsx.md
│ ├── no-spreading.md
│ ├── prefer-logical-over-objects.md
│ ├── prefer-merged-neighboring-elements.md
│ └── prefer-objects-over-logical.md
├── eslint.config.mjs
├── generate-all-rules.js
├── package-lock.json
├── package.json
├── src
├── PluginDocs.ts
├── createRule.ts
├── index.ts
├── rules
│ ├── allRules.generated.ts
│ ├── forbid-array-expressions.ts
│ ├── forbid-false-inside-object-expressions.ts
│ ├── forbid-true-inside-object-expressions.ts
│ ├── no-redundant-clsx.test.ts
│ ├── no-redundant-clsx.ts
│ ├── no-spreading.ts
│ ├── prefer-logical-over-objects.ts
│ ├── prefer-merged-neighboring-elements.ts
│ ├── prefer-objects-over-logical.test.ts
│ ├── prefer-objects-over-logical.ts
│ └── tests-setup.mjs
└── utils.ts
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.mts
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "public",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": []
11 | }
12 |
--------------------------------------------------------------------------------
/.eslint-doc-generatorrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-require-imports */
2 | const REPO_URL = require('./package.json')
3 | .repository.url.replace(/^git\+/, '')
4 | .replace(/\.git$/, '');
5 |
6 | /** @type {import('eslint-doc-generator').GenerateOptions} */
7 | module.exports = {
8 | ignoreConfig: ['all'],
9 | ruleDocSectionInclude: ['Rule Details'],
10 | urlConfigs: `${REPO_URL}#presets`,
11 | };
12 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - "**"
6 | pull_request:
7 | branches:
8 | - "**"
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: 22.x
17 | cache: npm
18 |
19 | - run: npm ci
20 | - run: npm run lint
21 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | push:
4 | branches:
5 | - "main"
6 |
7 | concurrency: ${{ github.workflow }}-${{ github.ref }}
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: 22.x
17 | cache: npm
18 |
19 | - run: npm ci
20 | - name: Create Release Pull Request or Publish
21 | id: changesets
22 | uses: changesets/action@v1
23 | with:
24 | publish: npm run release
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .vscode
4 | .DS_Store
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "line-length": false,
3 | "no-inline-html": false
4 | }
--------------------------------------------------------------------------------
/.markdownlintignore:
--------------------------------------------------------------------------------
1 | CHANGELOG.md
2 | LICENSE.md
3 | node_modules
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | tabWidth: 4,
3 | printWidth: 100,
4 | singleQuote: true,
5 | trailingComma: 'es5',
6 | proseWrap: 'never',
7 | arrowParens: 'always',
8 | quoteProps: 'preserve',
9 | htmlWhitespaceSensitivity: 'strict',
10 | };
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # eslint-plugin-clsx
2 |
3 | ## 0.0.10
4 |
5 | ### Patch Changes
6 |
7 | - 56fbdbd: fix peer dependency warning
8 |
9 | ## 0.0.9
10 |
11 | ### Patch Changes
12 |
13 | - 2241036: fix no-redundant-clsx autofixing object literal wrapped with clsx
14 |
15 | ## 0.0.8
16 |
17 | ### Patch Changes
18 |
19 | - 50b38fc: add redundancy selector for no-redundant-clsx
20 |
21 | ## 0.0.7
22 |
23 | ### Patch Changes
24 |
25 | - 8594323: cleanup published files
26 |
27 | ## 0.0.6
28 |
29 | ### Patch Changes
30 |
31 | - 75d5227: fix #16 critical build issue
32 |
33 | ## 0.0.5
34 |
35 | ### Patch Changes
36 |
37 | - 93d93a8: fix #6 `prefer-objects-over-logical` autofix
38 |
39 | ## 0.0.4
40 |
41 | ### Patch Changes
42 |
43 | - 5eb6a1e: no-redundant-clsx improvement
44 |
45 | ## 0.0.3
46 |
47 | ### Patch Changes
48 |
49 | - 2204a6c: fix `forbid-true-inside-object-expressions` conversion bug
50 |
51 | ## 0.0.2
52 |
53 | ### Patch Changes
54 |
55 | - d3b4c51: switched the project to typescript
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Artem Baranov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # eslint-plugin-clsx
2 |
3 | An ESLint plugin for clsx/classnames
4 |
5 |
6 | * [Installation](#Installation)
7 | * [Usage](#Usage)
8 | * [Rules](#Rules)
9 | * [Presets](#Presets)
10 | * [Preset usage](#Presetusage)
11 |
12 |
16 |
17 |
18 | ## Installation
19 |
20 | You'll first need to install [ESLint](https://eslint.org):
21 |
22 | ```sh
23 | npm i eslint --save-dev
24 | ```
25 |
26 | Next, install `eslint-plugin-clsx`:
27 |
28 | ```sh
29 | npm install eslint-plugin-clsx --save-dev
30 | ```
31 |
32 | ## Usage
33 |
34 | Here's an example ESLint configuration that:
35 |
36 | * Enables the `recommended` configuration
37 | * Enables an optional/non-recommended rule
38 |
39 | ```json
40 | {
41 | "extends": ["plugin:clsx/recommended"],
42 | "rules": {
43 | "clsx/no-redundant-clsx": "error"
44 | }
45 | }
46 | ```
47 |
48 | ## Rules
49 |
50 |
51 |
52 | ⚠️ [Configurations](https://github.com/temoncher/eslint-plugin-clsx#presets) set to warn in.\
53 | ✅ Set in the `recommended` [configuration](https://github.com/temoncher/eslint-plugin-clsx#presets).\
54 | 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
55 |
56 | | Name | Description | ⚠️ | 🔧 |
57 | | :--------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :- | :- |
58 | | [forbid-array-expressions](docs/rules/forbid-array-expressions.md) | forbid usage of array expressions inside clsx | ✅ | 🔧 |
59 | | [forbid-false-inside-object-expressions](docs/rules/forbid-false-inside-object-expressions.md) | forbid usage of false literal inside object expressions of clsx | ✅ | 🔧 |
60 | | [forbid-true-inside-object-expressions](docs/rules/forbid-true-inside-object-expressions.md) | forbid usage of true literal inside object expressions of clsx | ✅ | 🔧 |
61 | | [no-redundant-clsx](docs/rules/no-redundant-clsx.md) | disallow redundant clsx usage | ✅ | 🔧 |
62 | | [no-spreading](docs/rules/no-spreading.md) | forbid usage of object expression inside clsx | ✅ | 🔧 |
63 | | [prefer-logical-over-objects](docs/rules/prefer-logical-over-objects.md) | forbid usage of object expression inside clsx | | 🔧 |
64 | | [prefer-merged-neighboring-elements](docs/rules/prefer-merged-neighboring-elements.md) | enforce merging of neighboring elements | ✅ | 🔧 |
65 | | [prefer-objects-over-logical](docs/rules/prefer-objects-over-logical.md) | forbid usage of logical expressions inside clsx | | 🔧 |
66 |
67 |
68 |
69 | ## Presets
70 |
71 | | | Name | Description |
72 | |:--|:-----|:------------|
73 | | ✅ | `recommended` | enables all recommended rules in this plugin |
74 | | | `all` | enables all rules in this plugin |
75 |
76 | ### Preset usage
77 |
78 | Presets are enabled by adding a line to the `extends` list in your config file. For example, to enable the `recommended` preset, use:
79 |
80 | ```json
81 | {
82 | "extends": ["plugin:clsx/recommended"]
83 | }
84 | ```
85 |
86 | ## Settings
87 |
88 | This rule can optionally be configured with an object that represents imports that should be considered an clsx usage
89 |
90 | ```json
91 | {
92 | "settings": {
93 | "clsxOptions": {
94 | "myclsx": "default"
95 | }
96 | }
97 | }
98 | ```
99 |
100 | Examples of **incorrect** code for the `{ myclsx: 'default' }` setting (with no-redundant-clsx rule enabled):
101 |
102 | ```js
103 | import mc from 'myclsx';
104 |
105 | const singleClasses = mc('single-class');
106 | ```
107 |
108 | Examples of **incorrect** code for the `{ myclsx: 'cn' }` setting (with no-redundant-clsx rule enabled):
109 |
110 | ```js
111 | import { cn } from 'myclsx';
112 |
113 | const singleClasses = cn('single-class');
114 | ```
115 |
116 | Examples of **incorrect** code for the `{ myclsx: ['default', 'cn'] }` setting (with no-redundant-clsx rule enabled):
117 |
118 | ```js
119 | import mc, { cn } from 'myclsx';
120 |
121 | // both report errors
122 | const singleClasses = cn('single-class');
123 | const singleClasses = mc('single-class');
124 | ```
125 |
126 | Default setting value is `{ clsx: ['default', 'clsx'], classnames: 'default' }`
127 |
--------------------------------------------------------------------------------
/docs/rules/forbid-array-expressions.md:
--------------------------------------------------------------------------------
1 | # Forbid usage of array expressions inside clsx (`clsx/forbid-array-expressions`)
2 |
3 | ⚠️ This rule _warns_ in the ✅ `recommended` [config](https://github.com/temoncher/eslint-plugin-clsx#presets).
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | ## Rule Details
10 |
11 | This rule aims to forbid usage of array expressions inside clsx
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```js
16 | /* eslint clsx/forbid-array-expressions: error */
17 |
18 | const singleClasses = clsx(['single-class']);
19 | ```
20 |
21 | Examples of **correct** code for this rule:
22 |
23 | ```js
24 | /* eslint clsx/forbid-array-expressions: error */
25 |
26 | const singleClasses = clsx('single-class');
27 | const twoClasseses = clsx(['first-class', 'second-class']);
28 | const classes = ['first-class', 'second-class'];
29 | const dynamic = clsx('some-class', classes);
30 | ```
31 |
32 | ## Options
33 |
34 | ### always (default)
35 |
36 | Examples of **incorrect** code for the `always` option:
37 |
38 | ```js
39 | /* eslint clsx/forbid-array-expressions: ['error', 'always'] */
40 |
41 | const twoClasseses = clsx(['first-class', 'second-class']);
42 | ```
43 |
44 | Examples of **correct** code for the `always` option:
45 |
46 | ```js
47 | /* eslint clsx/forbid-array-expressions: ['error', 'always'] */
48 |
49 | const singleClasses = clsx('single-class');
50 | const twoClasseses = clsx('first-class', 'second-class');
51 | const classes = ['first-class', 'second-class'];
52 | const dynamic = clsx('some-class', classes);
53 | ```
54 |
55 | ### onlySingleElement
56 |
57 | Examples of **incorrect** code for the `onlySingleElement` option:
58 |
59 | ```js
60 | /* eslint clsx/forbid-array-expressions: ['error', 'onlySingleElement'] */
61 |
62 | const singleClasses = clsx(['single-class']);
63 | ```
64 |
65 | Examples of **correct** code for the `onlySingleElement` option:
66 |
67 | ```js
68 | /* eslint clsx/forbid-array-expressions: ['error', 'onlySingleElement'] */
69 |
70 | const singleClasses = clsx('single-class');
71 | const twoClasseses = clsx(['first-class', 'second-class']);
72 | const classes = ['first-class', 'second-class'];
73 | const dynamic = clsx('some-class', classes);
74 | ```
75 |
76 | ## When Not To Use It
77 |
78 | If you don't want to enforce specific usage of array expressions inside clsx
79 |
--------------------------------------------------------------------------------
/docs/rules/forbid-false-inside-object-expressions.md:
--------------------------------------------------------------------------------
1 | # Forbid usage of false literal inside object expressions of clsx (`clsx/forbid-false-inside-object-expressions`)
2 |
3 | ⚠️ This rule _warns_ in the ✅ `recommended` [config](https://github.com/temoncher/eslint-plugin-clsx#presets).
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | ## Rule Details
10 |
11 | This rule aims to enforce specific usage of object expressions inside clsx
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```js
16 | /* eslint clsx/forbid-false-inside-object-expressions: 'error' */
17 |
18 | const falseClasses = clsx({ 'dynamic-condition-class': condition, 'false-class': false });
19 | ```
20 |
21 | Examples of **correct** code for this rule:
22 |
23 | ```js
24 | /* eslint clsx/forbid-false-inside-object-expressions: 'error' */
25 |
26 | const falseClasses = clsx({ 'dynamic-condition-class': condition });
27 | ```
28 |
29 | ## When Not To Use It
30 |
31 | If you don't want to enforce specific usage of object expressions inside clsx
32 |
--------------------------------------------------------------------------------
/docs/rules/forbid-true-inside-object-expressions.md:
--------------------------------------------------------------------------------
1 | # Forbid usage of true literal inside object expressions of clsx (`clsx/forbid-true-inside-object-expressions`)
2 |
3 | ⚠️ This rule _warns_ in the ✅ `recommended` [config](https://github.com/temoncher/eslint-plugin-clsx#presets).
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | ## Rule Details
10 |
11 | This rule aims to forbid usage of true literals inside object expressions of clsx
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```js
16 | /* eslint clsx/forbid-true-inside-object-expressions: error */
17 |
18 | const trueClasses = clsx({ 'true-class-1': true, 'true-class-2': true });
19 | ```
20 |
21 | Examples of **correct** code for this rule:
22 |
23 | ```js
24 | /* eslint clsx/forbid-true-inside-object-expressions: error */
25 |
26 | const trueClasses = clsx('true-class-1', 'true-class-2');
27 | const dynamicClasses = clsx({ 'dynamic-condition-class': condition });
28 | ```
29 |
30 | ## Options
31 |
32 | ### allowMixed (default)
33 |
34 | Examples of **incorrect** code for the `allowMixed` option:
35 |
36 | ```js
37 | /* eslint clsx/forbid-true-inside-object-expressions: ['error', 'allowMixed'] */
38 |
39 | const trueClasses = clsx({ 'true-class-1': true, 'true-class-2': true });
40 | ```
41 |
42 | Examples of **correct** code for the `allowMixed` option:
43 |
44 | ```js
45 | /* eslint clsx/forbid-true-inside-object-expressions: ['error', 'allowMixed'] */
46 |
47 | const trueClasses = clsx('true-class-1', 'true-class-2');
48 | const trueClasses2 = clsx({ 'dynamic-condition-class': condition, 'true-class-2': true });
49 | ```
50 |
51 | ### always
52 |
53 | Examples of **incorrect** code for the `always` option:
54 |
55 | ```js
56 | /* eslint clsx/forbid-true-inside-object-expressions: ['error', 'always'] */
57 |
58 | const trueClasses = clsx({ 'true-class-1': true, 'true-class-2': true });
59 | const trueClasses2 = clsx({ 'dynamic-condition-class': condition, 'true-class-2': true });
60 | ```
61 |
62 | Examples of **correct** code for the `always` option:
63 |
64 | ```js
65 | /* eslint clsx/forbid-true-inside-object-expressions: ['error', 'always'] */
66 |
67 | const trueClasses = clsx('true-class-1', 'true-class-2');
68 | const trueClasses2 = clsx('true-class-2', { 'dynamic-condition-class': condition });
69 | ```
70 |
71 | ## When Not To Use It
72 |
73 | If you don't want to forbid usage of true literals inside object expressions of clsx
74 |
--------------------------------------------------------------------------------
/docs/rules/no-redundant-clsx.md:
--------------------------------------------------------------------------------
1 | # Disallow redundant clsx usage (`clsx/no-redundant-clsx`)
2 |
3 | ⚠️ This rule _warns_ in the ✅ `recommended` [config](https://github.com/temoncher/eslint-plugin-clsx#presets).
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | ## Rule Details
10 |
11 | This rule aims to restrict unnecessary usage of clsx
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```js
16 | /* eslint clsx/no-redundant-clsx: error */
17 |
18 | const singleClasses = clsx('single-class');
19 | ```
20 |
21 | Examples of **correct** code for this rule:
22 |
23 | ```js
24 | /* eslint clsx/no-redundant-clsx: error */
25 |
26 | const singleClasses = 'single-class';
27 | const twoClasseses = clsx('first-class', 'second-class')
28 | ```
29 |
30 | ## Options
31 |
32 | This rule has an object option with one optional property: "selector"
33 |
34 | ### selector
35 |
36 | "selector" accepts [esquery selector](https://eslint.org/docs/latest/extend/selectors) to apply to signle argument of `clsx` usage. If argument matches this selector clsx usage is considered redundant
37 |
38 | Examples of **incorrect** code for the `{ selector: ":matches(Literal, TemplateLiteral, MemberExpression[object.name="styles"])" }` option:
39 |
40 | ```js
41 | /* eslint clsx/no-spreading: ['error', { selector: ":matches(Literal, TemplateLiteral, MemberExpression[object.name="styles"])" }] */
42 |
43 | const classes = clsx(styles.myStyle);
44 | ```
45 |
46 | Examples of **correct** code for the `{ selector: ":matches(Literal, TemplateLiteral, MemberExpression[object.name="styles"])" }` option:
47 |
48 | ```js
49 | /* eslint clsx/no-spreading: ['error', { selector: ":matches(Literal, TemplateLiteral, MemberExpression[object.name="styles"])" }] */
50 |
51 | const classes = styles.myStyle;
52 | ```
53 |
54 | Default value is `{ selector: ":matches(Literal, TemplateLiteral)" }`
55 |
56 | ## When Not To Use It
57 |
58 | If you're ok with clsx used redundantly
59 |
--------------------------------------------------------------------------------
/docs/rules/no-spreading.md:
--------------------------------------------------------------------------------
1 | # Forbid usage of object expression inside clsx (`clsx/no-spreading`)
2 |
3 | ⚠️ This rule _warns_ in the ✅ `recommended` [config](https://github.com/temoncher/eslint-plugin-clsx#presets).
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | ## Rule Details
10 |
11 | This rule aims to forbid usage of true literals inside object expressions of clsx
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```js
16 | /* eslint clsx/no-spreading: 'error' */
17 |
18 | const objectClasses = clsx({ ...firstObj, ...secondObj, 'class-1': true, 'class-2': true, ...someObj, 'class-3': true && true });
19 | ```
20 |
21 | Examples of **correct** code for this rule:
22 |
23 | ```js
24 | /* eslint clsx/no-spreading: error */
25 |
26 | const objectClasses = clsx(firstObj, secondObj, { 'class-1': true, 'class-2': true }, someObj, { 'class-3': true && true });
27 | ```
28 |
29 | ## Options
30 |
31 | The rule accepts an array of following values
32 |
33 | ### object
34 |
35 | Examples of **incorrect** code for the `['object']` option:
36 |
37 | ```js
38 | /* eslint clsx/no-spreading: ['error', ['object']] */
39 |
40 | const objectClasses = clsx({ ...firstObj, ...secondObj, 'class-1': true, 'class-2': true, ...someObj, 'class-3': true && true });
41 | ```
42 |
43 | Examples of **correct** code for the `['object']` option:
44 |
45 | ```js
46 | /* eslint clsx/no-spreading: ['error', ['object']] */
47 |
48 | const objectClasses = clsx(firstObj, secondObj, { 'class-1': true, 'class-2': true }, someObj, { 'class-3': true && true });
49 | ```
50 |
51 | Default value is `['object']`
52 |
53 | ## When Not To Use It
54 |
55 | If you don't want to forbid usage of true literals inside object expressions of clsx
56 |
--------------------------------------------------------------------------------
/docs/rules/prefer-logical-over-objects.md:
--------------------------------------------------------------------------------
1 | # Forbid usage of object expression inside clsx (`clsx/prefer-logical-over-objects`)
2 |
3 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4 |
5 |
6 |
7 | ## Rule Details
8 |
9 | This rule aims to forbid usage of object expressions inside clsx
10 |
11 | Examples of **incorrect** code for this rule:
12 |
13 | ```js
14 | /* eslint clsx/prefer-logical-over-objects: error */
15 |
16 | const objectLiteralClasses = clsx({ 'dynamic-condition-class': condition, 'third-class': true });
17 | ```
18 |
19 | Examples of **correct** code for this rule:
20 |
21 | ```js
22 | /* eslint clsx/prefer-logical-over-objects: error */
23 |
24 | const objectLiteralClasses = clsx(condition && 'dynamic-condition-class', true && 'third-class');
25 | ```
26 |
27 | ## Options
28 |
29 | This rule has an object option with two optional properties: "startingFrom" and "endingWith"
30 |
31 | ### startingFrom (default: 0)
32 |
33 | Determines minimum number of properties on an object expression to report
34 |
35 | Examples of **incorrect** code for `{ startingFrom: 2 }`:
36 |
37 | ```js
38 | /* eslint clsx/prefer-objects-over-logical: ['error', { startingFrom: 2 }] */
39 |
40 | const classes = clsx({ 'first-class': true, 'second-class': true }, 'some-class', { 'third-class': true });
41 | ```
42 |
43 | Examples of **correct** code for this rule:
44 |
45 | ```js
46 | /* eslint clsx/prefer-objects-over-logical: ['error', { startingFrom: 2 }] */
47 |
48 | const classes = clsx(true && 'first-class', true && 'second-class', 'some-class', { 'third-class': true });
49 | ```
50 |
51 | ### endingWith (default: Infinity)
52 |
53 | Determines maximum number of properties on an object expression to report
54 |
55 | Examples of **incorrect** code for `{ endingWith: 1 }`:
56 |
57 | ```js
58 | /* eslint clsx/prefer-objects-over-logical: ['error', { endingWith: 1 }] */
59 |
60 | const classes = clsx({ 'first-class': true, 'second-class': true }, 'some-class', { 'third-class': true });
61 | ```
62 |
63 | Examples of **correct** code for this rule:
64 |
65 | ```js
66 | /* eslint clsx/prefer-objects-over-logical: ['error', { endingWith: 1 }] */
67 |
68 | const classes = clsx({ 'first-class': true, 'second-class': true }, 'some-class', true && 'third-class');
69 | ```
70 |
71 | ## When Not To Use It
72 |
73 | If you don't want to enforce usage of logical expressions over objects
74 |
75 | ## Related rules
76 |
77 | - [https://github.com/temoncher/eslint-plugin-clsx/blob/main/docs/rules/prefer-objects-over-logical.md](prefer-objects-over-logical) - inverse of prefer-logical-over-objects
78 | Can be combined to achieve logical for 0-2 elements and objects for larger (>2) elements
79 |
80 | Examples of **incorrect** code :
81 |
82 | ```js
83 | /* eslint clsx/prefer-logical-over-objects: ['error', { endingWith: 3 }] */
84 | /* eslint clsx/prefer-objects-over-logical: ['error', { startingFrom: 3 }] */
85 |
86 | const classes = clsx(true && 'first-class', true && 'second-class', 'some-class', { 'third-class': true });
87 | ```
88 |
89 | Examples of **correct** code :
90 |
91 | ```js
92 | /* eslint clsx/prefer-logical-over-objects: ['error', { endingWith: 3 }] */
93 | /* eslint clsx/prefer-objects-over-logical: ['error', { startingFrom: 3 }] */
94 |
95 | const classes = clsx({ 'first-class': true, 'second-class': true }, 'some-class', true && 'third-class');
96 | ```
97 |
--------------------------------------------------------------------------------
/docs/rules/prefer-merged-neighboring-elements.md:
--------------------------------------------------------------------------------
1 | # Enforce merging of neighboring elements (`clsx/prefer-merged-neighboring-elements`)
2 |
3 | ⚠️ This rule _warns_ in the ✅ `recommended` [config](https://github.com/temoncher/eslint-plugin-clsx#presets).
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | ## Rule Details
10 |
11 | This rule aims to reduce consequent elements into one inside clsx
12 |
13 | Examples of **incorrect** code for this rule:
14 |
15 | ```js
16 | /* eslint clsx/prefer-merged-neighboring-elements: 'error' */
17 |
18 | const objectClasses = clsx({ ...firstObj }, { ...secondObj }, { 'class-1': true , 'class-2': true }, someObj, { 'class-3': true && true });
19 | ```
20 |
21 | Examples of **correct** code for this rule:
22 |
23 | ```js
24 | /* eslint clsx/prefer-merged-neighboring-elements: error */
25 |
26 | const objectClasses = clsx({ ...firstObj, ...secondObj, 'class-1': true , 'class-2': true }, someObj, { 'class-3': true && true });
27 | ```
28 |
29 | ## Options
30 |
31 | The rule accepts an array of following values
32 |
33 | ### object
34 |
35 | Examples of **incorrect** code for the `['object']` option:
36 |
37 | ```js
38 | /* eslint clsx/prefer-merged-neighboring-elements: ['error', ['object']] */
39 |
40 | const objectClasses = clsx({ ...firstObj }, { ...secondObj }, { 'class-1': true , 'class-2': true }, someObj, { 'class-3': true && true });
41 | ```
42 |
43 | Examples of **correct** code for the `['object']` option:
44 |
45 | ```js
46 | /* eslint clsx/prefer-merged-neighboring-elements: ['error', ['object']] */
47 |
48 | const objectClasses = clsx({ ...firstObj, ...secondObj, 'class-1': true , 'class-2': true }, someObj, { 'class-3': true && true });
49 | ```
50 |
51 | Default value is `['object']`
52 |
53 | ## When Not To Use It
54 |
55 | If you don't want to reduce consequent elements into one inside clsx
56 |
--------------------------------------------------------------------------------
/docs/rules/prefer-objects-over-logical.md:
--------------------------------------------------------------------------------
1 | # Forbid usage of logical expressions inside clsx (`clsx/prefer-objects-over-logical`)
2 |
3 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4 |
5 |
6 |
7 | ## Rule Details
8 |
9 | This rule aims to forbid usage of logical expressions inside clsx
10 |
11 | Examples of **incorrect** code for this rule:
12 |
13 | ```js
14 | /* eslint clsx/prefer-objects-over-logical: error */
15 |
16 | const classWithLogicalExpression = clsx(true && 'single-class');
17 | ```
18 |
19 | Examples of **correct** code for this rule:
20 |
21 | ```js
22 | /* eslint clsx/prefer-objects-over-logical: error */
23 |
24 | const classWithLogicalExpression = clsx('single-class');
25 | ```
26 |
27 | ## Options
28 |
29 | This rule has an object option with two optional properties: "startingFrom" and "endingWith"
30 |
31 | ### startingFrom (default: 0)
32 |
33 | Determines minimum number of consequent logical expressions to report
34 |
35 | Examples of **incorrect** code for `{ startingFrom: 2 }`:
36 |
37 | ```js
38 | /* eslint clsx/prefer-objects-over-logical: ['error', { startingFrom: 2 }] */
39 |
40 | const classes = clsx(true && 'first-class', true && 'second-class', 'some-class', true && 'third-class');
41 | ```
42 |
43 | Examples of **correct** code for this rule:
44 |
45 | ```js
46 | /* eslint clsx/prefer-objects-over-logical: ['error', { startingFrom: 2 }] */
47 |
48 | const classes = clsx({ 'first-class': true, 'second-class': true }, 'some-class', true && 'third-class');
49 | ```
50 |
51 | ### endingWith (default: Infinity)
52 |
53 | Determines maximum number of consequent logical expressions to report
54 |
55 | Examples of **incorrect** code for `{ endingWith: 1 }`:
56 |
57 | ```js
58 | /* eslint clsx/prefer-objects-over-logical: ['error', { endingWith: 1 }] */
59 |
60 | const classes = clsx(true && 'first-class', true && 'second-class', 'some-class', true && 'third-class');
61 | ```
62 |
63 | Examples of **correct** code for this rule:
64 |
65 | ```js
66 | /* eslint clsx/prefer-objects-over-logical: ['error', { endingWith: 1 }] */
67 |
68 | const classes = clsx(true && 'first-class', true && 'second-class', 'some-class', { 'third-class': true });
69 | ```
70 |
71 | ## When Not To Use It
72 |
73 | If you don't want to enforce usage of objects over logical expressions
74 |
75 | ## Related rules
76 |
77 | - [https://github.com/temoncher/eslint-plugin-clsx/blob/main/docs/rules/prefer-logical-over-objects.md](prefer-logical-over-objects) - inverse of prefer-objects-over-logical
78 | Can be combined to achieve logical for 0-2 elements and objects for larger (>2) elements
79 |
80 | Examples of **incorrect** code :
81 |
82 | ```js
83 | /* eslint clsx/prefer-logical-over-objects: ['error', { endingWith: 3 }] */
84 | /* eslint clsx/prefer-objects-over-logical: ['error', { startingFrom: 3 }] */
85 |
86 | const classes = clsx(true && 'first-class', true && 'second-class', 'some-class', { 'third-class': true });
87 | ```
88 |
89 | Examples of **correct** code :
90 |
91 | ```js
92 | /* eslint clsx/prefer-logical-over-objects: ['error', { endingWith: 3 }] */
93 | /* eslint clsx/prefer-objects-over-logical: ['error', { startingFrom: 3 }] */
94 |
95 | const classes = clsx({ 'first-class': true, 'second-class': true }, 'some-class', true && 'third-class');
96 | ```
97 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import tsParser from '@typescript-eslint/parser';
2 | import importPlugin from 'eslint-plugin-import';
3 | import prettierRecommended from 'eslint-plugin-prettier/recommended';
4 | import tseslint from 'typescript-eslint';
5 |
6 | export default tseslint.config(
7 | {
8 | ignores: ['node_modules/', 'dist/'],
9 | },
10 | {
11 | extends: [
12 | tseslint.configs.recommended,
13 | importPlugin.flatConfigs.recommended,
14 | prettierRecommended,
15 | ],
16 | languageOptions: {
17 | parser: tsParser,
18 | parserOptions: {
19 | ecmaVersion: 'latest',
20 | sourceType: 'module',
21 | },
22 | },
23 | linterOptions: {
24 | reportUnusedDisableDirectives: true,
25 | },
26 | rules: {
27 | 'global-require': 0,
28 | 'no-continue': 0,
29 | 'no-case-declarations': 2,
30 | 'prefer-destructuring': 1,
31 | 'no-param-reassign': 2,
32 | 'no-console': 2,
33 | 'no-self-compare': 2,
34 | 'no-irregular-whitespace': 2,
35 | 'arrow-body-style': 1,
36 | complexity: [1, 7],
37 | 'array-bracket-newline': [1, 'consistent'],
38 | 'function-call-argument-newline': [1, 'consistent'],
39 | 'func-style': [1, 'declaration'],
40 | 'prefer-exponentiation-operator': 2,
41 |
42 | 'padding-line-between-statements': [
43 | 1,
44 | {
45 | blankLine: 'always',
46 | prev: ['const', 'let', 'var'],
47 | next: '*',
48 | },
49 | {
50 | blankLine: 'always',
51 | prev: '*',
52 | next: ['if', 'try', 'class', 'export'],
53 | },
54 | {
55 | blankLine: 'always',
56 | prev: ['if', 'try', 'class', 'export'],
57 | next: '*',
58 | },
59 | {
60 | blankLine: 'any',
61 | prev: ['const', 'let', 'var', 'export'],
62 | next: ['const', 'let', 'var', 'export'],
63 | },
64 | {
65 | blankLine: 'always',
66 | prev: ['expression'],
67 | next: ['const', 'let', 'var'],
68 | },
69 | {
70 | blankLine: 'always',
71 | prev: '*',
72 | next: ['return'],
73 | },
74 | ],
75 |
76 | 'arrow-spacing': 1,
77 |
78 | 'no-restricted-exports': [
79 | 1,
80 | {
81 | restrictedNamedExports: ['default', 'then'],
82 | },
83 | ],
84 |
85 | 'import/order': [
86 | 1,
87 | {
88 | groups: ['builtin', 'external', 'parent', 'sibling', 'index'],
89 | pathGroupsExcludedImportTypes: ['constants'],
90 | 'newlines-between': 'always',
91 |
92 | alphabetize: {
93 | order: 'asc',
94 | caseInsensitive: false,
95 | },
96 |
97 | warnOnUnassignedImports: true,
98 | },
99 | ],
100 |
101 | 'import/prefer-default-export': 0,
102 | 'import/extensions': 0,
103 | 'import/no-unresolved': 0,
104 | },
105 | },
106 | {
107 | files: ['**/*.ts', '**/*.tsx'],
108 | extends: [
109 | tseslint.configs.recommendedTypeChecked,
110 | tseslint.configs.strict,
111 | importPlugin.flatConfigs.typescript,
112 | prettierRecommended,
113 | ],
114 | languageOptions: {
115 | parser: tsParser,
116 | parserOptions: {
117 | project: './tsconfig.json',
118 | },
119 | },
120 | rules: {
121 | '@typescript-eslint/no-unused-vars': [1, { argsIgnorePattern: '^_.*' }],
122 | },
123 | }
124 | );
125 |
--------------------------------------------------------------------------------
/generate-all-rules.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-require-imports */
2 | const fs = require('fs');
3 |
4 | const packageJson = require('./package.json');
5 |
6 | function camelize(s) {
7 | return s.replace(/-./g, (x) => x[1].toUpperCase());
8 | }
9 |
10 | const ruleNames = fs
11 | .readdirSync(`${__dirname}/src/rules`)
12 | .filter(
13 | (fileName) =>
14 | !fileName.includes('.test') &&
15 | !fileName.includes('.generated') &&
16 | fileName !== 'tests-setup.mjs' &&
17 | fileName !== 'index.ts'
18 | )
19 | .map((fileName) => fileName.replace(/\.ts$/, ''));
20 |
21 | const imports = ruleNames.map((name) => `import ${camelize(name)} from './${name}';`).join('');
22 |
23 | const pairs = ruleNames.map((name) => `"${name}": ${camelize(name)}`).join(',');
24 | const REPO_URL = packageJson.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
25 |
26 | const content = `${imports}\nexport const allRules = {${pairs}};\nexport const REPO_URL="${REPO_URL}"`;
27 |
28 | fs.writeFileSync(`${__dirname}/src/rules/allRules.generated.ts`, content);
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-clsx",
3 | "version": "0.0.10",
4 | "description": "An ESLint plugin for clsx/classnames",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "build": "rimraf dist && npm run generate-rules && tsup",
8 | "lint": "npm-run-all \"lint:*\"",
9 | "lint:tsc": "tsc",
10 | "lint:docs": "markdownlint \"**/*.md\"",
11 | "lint:eslint-docs": "npm run update:eslint-docs -- --check",
12 | "lint:js": "eslint .",
13 | "update:eslint-docs": "npm run build && eslint-doc-generator",
14 | "generate-rules": "node ./generate-all-rules.js && eslint ./src/rules/*.generated.ts --fix",
15 | "release": "npm run build && changeset publish",
16 | "test": "vitest run"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/temoncher/eslint-plugin-clsx.git"
21 | },
22 | "keywords": [
23 | "eslint",
24 | "eslintplugin",
25 | "eslint-plugin",
26 | "clsx",
27 | "classnames"
28 | ],
29 | "author": "Artem Baranov ",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/temoncher/eslint-plugin-clsx/issues"
33 | },
34 | "homepage": "https://github.com/temoncher/eslint-plugin-clsx#readme",
35 | "devDependencies": {
36 | "@changesets/cli": "^2.27.12",
37 | "@types/eslint": "^9.6.1",
38 | "@types/esquery": "^1.5.4",
39 | "@types/node": "^22.13.0",
40 | "@typescript-eslint/eslint-plugin": "^8.23.0",
41 | "@typescript-eslint/parser": "^8.23.0",
42 | "@typescript-eslint/rule-tester": "^8.23.0",
43 | "eslint": "^9.19.0",
44 | "eslint-config-prettier": "^10.0.1",
45 | "eslint-doc-generator": "2.0.2",
46 | "eslint-plugin-import": "^2.31.0",
47 | "eslint-plugin-prettier": "^5.2.3",
48 | "markdownlint-cli": "^0.44.0",
49 | "npm-run-all": "^4.1.5",
50 | "prettier": "^3.4.2",
51 | "rimraf": "^6.0.1",
52 | "tsup": "^8.3.6",
53 | "typescript": "^5.7.3",
54 | "typescript-eslint": "^8.23.0",
55 | "vitest": "^3.0.5"
56 | },
57 | "dependencies": {
58 | "@typescript-eslint/types": "^8.23.0",
59 | "@typescript-eslint/utils": "^8.23.0",
60 | "esquery": "^1.6.0",
61 | "remeda": "^2.20.0"
62 | },
63 | "peerDependencies": {
64 | "eslint": "^8 || ^9"
65 | },
66 | "files": [
67 | "*.md",
68 | "!{CONTRIBUTING,RELEASE}.md",
69 | "LICENSE",
70 | "docs",
71 | "dist"
72 | ]
73 | }
74 |
--------------------------------------------------------------------------------
/src/PluginDocs.ts:
--------------------------------------------------------------------------------
1 | export type PluginDocs = { recommended?: boolean };
2 |
--------------------------------------------------------------------------------
/src/createRule.ts:
--------------------------------------------------------------------------------
1 | import { ESLintUtils } from '@typescript-eslint/utils';
2 |
3 | import type { PluginDocs } from './PluginDocs';
4 |
5 | export const createRule = ESLintUtils.RuleCreator(
6 | (name) => `https://github.com/temoncher/eslint-plugin-clsx/blob/main/docs/rules/${name}.md`
7 | );
8 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { RuleModule } from '@typescript-eslint/utils/ts-eslint';
2 | import * as R from 'remeda';
3 |
4 | import { PluginDocs } from './PluginDocs';
5 | import { allRules, REPO_URL } from './rules/allRules.generated';
6 |
7 | const configFilters = {
8 | all: () => true,
9 | recommended: (rule: RuleModule) => rule.meta?.docs?.recommended,
10 | };
11 |
12 | export = {
13 | rules: R.mapValues(allRules, (rule, ruleName) => ({
14 | ...rule,
15 | meta: {
16 | ...rule.meta,
17 | url: `${REPO_URL}/tree/HEAD/docs/rules/${ruleName}.md`,
18 | },
19 | })),
20 | configs: R.mapValues(configFilters, (configPredicate) => ({
21 | plugins: ['clsx'],
22 | rules: Object.fromEntries(
23 | Object.entries(allRules)
24 | .filter(([_ruleName, rule]) => configPredicate(rule))
25 | .map(([ruleName]) => [`clsx/${ruleName}`, 1])
26 | ),
27 | })),
28 | };
29 |
--------------------------------------------------------------------------------
/src/rules/allRules.generated.ts:
--------------------------------------------------------------------------------
1 | import forbidArrayExpressions from './forbid-array-expressions';
2 | import forbidFalseInsideObjectExpressions from './forbid-false-inside-object-expressions';
3 | import forbidTrueInsideObjectExpressions from './forbid-true-inside-object-expressions';
4 | import noRedundantClsx from './no-redundant-clsx';
5 | import noSpreading from './no-spreading';
6 | import preferLogicalOverObjects from './prefer-logical-over-objects';
7 | import preferMergedNeighboringElements from './prefer-merged-neighboring-elements';
8 | import preferObjectsOverLogical from './prefer-objects-over-logical';
9 |
10 | export const allRules = {
11 | 'forbid-array-expressions': forbidArrayExpressions,
12 | 'forbid-false-inside-object-expressions': forbidFalseInsideObjectExpressions,
13 | 'forbid-true-inside-object-expressions': forbidTrueInsideObjectExpressions,
14 | 'no-redundant-clsx': noRedundantClsx,
15 | 'no-spreading': noSpreading,
16 | 'prefer-logical-over-objects': preferLogicalOverObjects,
17 | 'prefer-merged-neighboring-elements': preferMergedNeighboringElements,
18 | 'prefer-objects-over-logical': preferObjectsOverLogical,
19 | };
20 | export const REPO_URL = 'https://github.com/temoncher/eslint-plugin-clsx';
21 |
--------------------------------------------------------------------------------
/src/rules/forbid-array-expressions.ts:
--------------------------------------------------------------------------------
1 | import { TSESTree } from '@typescript-eslint/types';
2 |
3 | import { createRule } from '../createRule';
4 | import * as utils from '../utils';
5 |
6 | export = createRule({
7 | name: 'forbid-array-expressions',
8 | defaultOptions: ['always' as 'onlySingleElement' | 'always'],
9 | meta: {
10 | type: 'suggestion',
11 | docs: {
12 | description: 'forbid usage of array expressions inside clsx',
13 | recommended: true,
14 | },
15 | fixable: 'code',
16 | schema: [{ type: 'string', enum: ['onlySingleElement', 'always'] }],
17 | messages: {
18 | onlySingleElement: 'Single element arrays are forbidden inside clsx',
19 | always: 'Usage of array expressions inside clsx is forbidden',
20 | },
21 | },
22 | create(context, [ruleOptions]) {
23 | const { sourceCode } = context;
24 | const clsxOptions = utils.extractClsxOptions(context);
25 |
26 | return {
27 | ImportDeclaration(importNode) {
28 | const assignedClsxName = utils.findClsxImport(importNode, clsxOptions);
29 |
30 | if (!assignedClsxName) {
31 | return;
32 | }
33 |
34 | const clsxUsages = utils.getClsxUsages(importNode, sourceCode, assignedClsxName);
35 |
36 | clsxUsages
37 | .flatMap((clsxCallNode) => clsxCallNode.arguments)
38 | .forEach((argumentNode) => {
39 | if (argumentNode.type !== TSESTree.AST_NODE_TYPES.ArrayExpression) return;
40 |
41 | // TODO?: move this check out of visitor for performance
42 | if (ruleOptions === 'always') {
43 | context.report({
44 | messageId: 'always',
45 | node: argumentNode,
46 | fix: (fixer) =>
47 | fixer.replaceText(
48 | argumentNode,
49 | argumentNode.elements
50 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
51 | .map((el) => sourceCode.getText(el!))
52 | .join(', ')
53 | ),
54 | });
55 |
56 | return;
57 | }
58 |
59 | if (
60 | ruleOptions === 'onlySingleElement' &&
61 | argumentNode.elements.length === 1
62 | ) {
63 | context.report({
64 | messageId: 'onlySingleElement',
65 | node: argumentNode,
66 | fix: (fixer) =>
67 | fixer.replaceText(
68 | argumentNode,
69 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
70 | sourceCode.getText(argumentNode.elements[0]!)
71 | ),
72 | });
73 | }
74 | });
75 | },
76 | };
77 | },
78 | });
79 |
--------------------------------------------------------------------------------
/src/rules/forbid-false-inside-object-expressions.ts:
--------------------------------------------------------------------------------
1 | import { TSESTree } from '@typescript-eslint/types';
2 |
3 | import { createRule } from '../createRule';
4 | import * as utils from '../utils';
5 |
6 | export = createRule({
7 | name: 'forbid-false-inside-object-expressions',
8 | defaultOptions: [],
9 | meta: {
10 | type: 'suggestion',
11 | docs: {
12 | description: 'forbid usage of false literal inside object expressions of clsx',
13 | recommended: true,
14 | },
15 | fixable: 'code',
16 | schema: [],
17 | messages: {
18 | falseLiterals: 'Object expression inside clsx should not contain false literals',
19 | },
20 | },
21 | create(context) {
22 | const { sourceCode } = context;
23 | const clsxOptions = utils.extractClsxOptions(context);
24 |
25 | return {
26 | ImportDeclaration(importNode) {
27 | const assignedClsxName = utils.findClsxImport(importNode, clsxOptions);
28 |
29 | if (!assignedClsxName) {
30 | return;
31 | }
32 |
33 | const clsxUsages = utils.getClsxUsages(importNode, sourceCode, assignedClsxName);
34 |
35 | clsxUsages
36 | .flatMap((clsxCallNode) => clsxCallNode.arguments)
37 | // TODO: autofix deep into arrays
38 | .forEach((argumentNode) => {
39 | if (argumentNode.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) return;
40 |
41 | const propsWithoutFalseLiterals = argumentNode.properties.filter(
42 | (prop) =>
43 | !(
44 | prop.type === TSESTree.AST_NODE_TYPES.Property &&
45 | prop.value.type === TSESTree.AST_NODE_TYPES.Literal &&
46 | prop.value.value === false
47 | )
48 | );
49 |
50 | if (propsWithoutFalseLiterals.length !== argumentNode.properties.length) {
51 | const propsText = propsWithoutFalseLiterals
52 | .map((prop) => sourceCode.getText(prop))
53 | .join(', ');
54 |
55 | context.report({
56 | messageId: 'falseLiterals',
57 | node: argumentNode,
58 | fix: (fixer) => fixer.replaceText(argumentNode, `{ ${propsText} }`),
59 | });
60 | }
61 | });
62 | },
63 | };
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/src/rules/forbid-true-inside-object-expressions.ts:
--------------------------------------------------------------------------------
1 | import { TSESTree } from '@typescript-eslint/types';
2 | import * as R from 'remeda';
3 |
4 | import { createRule } from '../createRule';
5 | import * as utils from '../utils';
6 |
7 | export = createRule({
8 | name: 'forbid-true-inside-object-expressions',
9 | defaultOptions: ['allowMixed' as 'always' | 'allowMixed'],
10 | meta: {
11 | type: 'suggestion',
12 | docs: {
13 | description: 'forbid usage of true literal inside object expressions of clsx',
14 | recommended: true,
15 | },
16 | fixable: 'code',
17 | schema: [{ type: 'string', enum: ['always', 'allowMixed'] }],
18 | messages: {
19 | default: 'Object expression inside clsx should not contain true literals',
20 | },
21 | },
22 | create(context, [allowTrueLiterals]) {
23 | const { sourceCode } = context;
24 | const clsxOptions = utils.extractClsxOptions(context);
25 |
26 | return {
27 | ImportDeclaration(importNode) {
28 | const assignedClsxName = utils.findClsxImport(importNode, clsxOptions);
29 |
30 | if (!assignedClsxName) {
31 | return;
32 | }
33 |
34 | const clsxUsages = utils.getClsxUsages(importNode, sourceCode, assignedClsxName);
35 |
36 | clsxUsages
37 | .flatMap((clsxCallNode) => clsxCallNode.arguments)
38 | // TODO: autofix deep into arrays
39 | .forEach((argumentNode) => {
40 | if (argumentNode.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) return;
41 |
42 | const [trueLiteralProps, otherProps] = R.partition(
43 | argumentNode.properties,
44 | (prop) =>
45 | prop.type === TSESTree.AST_NODE_TYPES.Property &&
46 | prop.value.type === TSESTree.AST_NODE_TYPES.Literal &&
47 | prop.value.value === true
48 | );
49 |
50 | if (
51 | trueLiteralProps.length !== 0 &&
52 | (allowTrueLiterals === 'always' ||
53 | (allowTrueLiterals === 'allowMixed' && otherProps.length === 0))
54 | ) {
55 | const trueLiteralPropsText = (trueLiteralProps as TSESTree.Property[])
56 | .map((el) => {
57 | const keyText = sourceCode.getText(el.key);
58 |
59 | return el.computed ? keyText : `'${keyText}'`;
60 | })
61 | .join(', ');
62 | const otherPropsText = otherProps
63 | .map((prop) => sourceCode.getText(prop))
64 | .join(', ');
65 | const otherPropsWrappedInObject = otherPropsText
66 | ? `{ ${otherPropsText} }`
67 | : undefined;
68 |
69 | context.report({
70 | messageId: 'default',
71 | node: argumentNode,
72 | fix: (fixer) =>
73 | fixer.replaceText(
74 | argumentNode,
75 | [trueLiteralPropsText, otherPropsWrappedInObject]
76 | .filter(R.isDefined)
77 | .join(', ')
78 | ),
79 | });
80 | }
81 | });
82 | },
83 | };
84 | },
85 | });
86 |
--------------------------------------------------------------------------------
/src/rules/no-redundant-clsx.test.ts:
--------------------------------------------------------------------------------
1 | import { RuleTester } from '@typescript-eslint/rule-tester';
2 |
3 | import rule from './no-redundant-clsx';
4 |
5 | new RuleTester().run('no-redundant-clsx', rule, {
6 | valid: [
7 | {
8 | name: 'Default selector. Two classes with Literals',
9 | code: `
10 | import clsx from 'clsx';
11 |
12 | const classes = clsx('flex', 'flex-col');
13 | `,
14 | },
15 | {
16 | name: 'Default selector. Object literal',
17 | code: `
18 | import clsx from 'clsx';
19 |
20 | const condition = true;
21 | const classes = clsx({ 'flex': condition });
22 | `,
23 | },
24 | {
25 | name: 'Default selector. Array literal',
26 | code: `
27 | import clsx from 'clsx';
28 |
29 | const condition = true;
30 | const classes = clsx(['flex']);
31 | `,
32 | },
33 | {
34 | name: 'Only Literal selector. Template literal',
35 | options: [{ selector: 'Literal' }],
36 | code: `
37 | import clsx from 'clsx';
38 |
39 | const col = 'col';
40 | const classes = clsx(\`flex-\${col}\`);
41 | `,
42 | },
43 | ],
44 | invalid: [
45 | {
46 | name: 'Default selector. One class with Literal',
47 | code: `
48 | import clsx from 'clsx';
49 |
50 | const classes = clsx('flex');
51 | `,
52 | errors: [
53 | {
54 | messageId: 'default',
55 | line: 4,
56 | column: 17,
57 | },
58 | ],
59 | output: `
60 | import clsx from 'clsx';
61 |
62 | const classes = 'flex';
63 | `,
64 | },
65 | {
66 | name: 'Default selector. One class with TemplateLiteral',
67 | code: `
68 | import clsx from 'clsx';
69 |
70 | const col = 'col';
71 | const classes = clsx(\`flex-\${col}\`);
72 | `,
73 | errors: [
74 | {
75 | messageId: 'default',
76 | line: 5,
77 | column: 17,
78 | },
79 | ],
80 | output: `
81 | import clsx from 'clsx';
82 |
83 | const col = 'col';
84 | const classes = \`flex-\${col}\`;
85 | `,
86 | },
87 | {
88 | name: 'Custom selector for css modules. One class with `styles.prop` property access',
89 | options: [
90 | {
91 | selector:
92 | ':matches(Literal, TemplateLiteral, MemberExpression[object.name="styles"])',
93 | },
94 | ],
95 | code: `
96 | import clsx from 'clsx';
97 | import styles from './some-css-file.css';
98 |
99 | const classes = clsx(styles.myStyle);
100 | `,
101 | errors: [
102 | {
103 | messageId: 'default',
104 | line: 5,
105 | column: 17,
106 | },
107 | ],
108 | output: `
109 | import clsx from 'clsx';
110 | import styles from './some-css-file.css';
111 |
112 | const classes = styles.myStyle;
113 | `,
114 | },
115 | ],
116 | });
117 |
--------------------------------------------------------------------------------
/src/rules/no-redundant-clsx.ts:
--------------------------------------------------------------------------------
1 | import { matches, parse } from 'esquery';
2 |
3 | import { createRule } from '../createRule';
4 | import * as utils from '../utils';
5 |
6 | export = createRule({
7 | name: 'no-redundant-clsx',
8 | defaultOptions: [{ selector: ':matches(Literal, TemplateLiteral)' }],
9 | meta: {
10 | type: 'suggestion',
11 | docs: {
12 | description: 'disallow redundant clsx usage',
13 | recommended: true,
14 | },
15 | fixable: 'code',
16 | schema: [
17 | {
18 | type: 'object',
19 | properties: {
20 | selector: { type: 'string' },
21 | },
22 | },
23 | ],
24 | messages: {
25 | default: 'clsx usage is redundant',
26 | },
27 | },
28 | create(context, [{ selector }]) {
29 | const { sourceCode } = context;
30 | const clsxOptions = utils.extractClsxOptions(context);
31 |
32 | return {
33 | ImportDeclaration(importNode) {
34 | const assignedClsxName = utils.findClsxImport(importNode, clsxOptions);
35 |
36 | if (!assignedClsxName) {
37 | return;
38 | }
39 |
40 | const clsxUsages = utils.getClsxUsages(importNode, sourceCode, assignedClsxName);
41 |
42 | clsxUsages.forEach((clsxCallNode) => {
43 | if (clsxCallNode.arguments.length !== 1) {
44 | return;
45 | }
46 |
47 | // eslint-disable-next-line prefer-destructuring
48 | const firstArg = clsxCallNode.arguments[0];
49 |
50 | if (matches(firstArg as import('estree').Node, parse(selector))) {
51 | context.report({
52 | messageId: 'default',
53 | node: clsxCallNode,
54 | fix: (fixer) =>
55 | fixer.replaceText(clsxCallNode, sourceCode.getText(firstArg)),
56 | });
57 | }
58 | });
59 | },
60 | };
61 | },
62 | });
63 |
--------------------------------------------------------------------------------
/src/rules/no-spreading.ts:
--------------------------------------------------------------------------------
1 | import { TSESTree } from '@typescript-eslint/types';
2 |
3 | import { createRule } from '../createRule';
4 | import * as utils from '../utils';
5 |
6 | export = createRule({
7 | name: 'no-spreading',
8 | defaultOptions: ['object' as const],
9 | meta: {
10 | type: 'suggestion',
11 | docs: {
12 | description: 'forbid usage of object expression inside clsx',
13 | recommended: true,
14 | },
15 | fixable: 'code',
16 | schema: [{ type: 'array', items: { type: 'string', enum: ['object'] } }],
17 | messages: {
18 | default: 'Usage of object expression inside clsx is forbidden',
19 | },
20 | },
21 | create(context, [forbiddenFor]) {
22 | const { sourceCode } = context;
23 | const clsxOptions = utils.extractClsxOptions(context);
24 |
25 | return {
26 | ImportDeclaration(importNode) {
27 | const assignedClsxName = utils.findClsxImport(importNode, clsxOptions);
28 |
29 | if (!assignedClsxName) {
30 | return;
31 | }
32 |
33 | const clsxUsages = utils.getClsxUsages(importNode, sourceCode, assignedClsxName);
34 |
35 | clsxUsages
36 | .flatMap((clsxCallNode) => clsxCallNode.arguments)
37 | // TODO: autofix deep into arrays
38 | .forEach((argumentNode) => {
39 | if (
40 | forbiddenFor.includes('object') &&
41 | argumentNode.type === TSESTree.AST_NODE_TYPES.ObjectExpression &&
42 | argumentNode.properties.some(
43 | (prop) => prop.type === TSESTree.AST_NODE_TYPES.SpreadElement
44 | )
45 | ) {
46 | const alternatingSpreadsAndProps = utils.chunkBy(
47 | argumentNode.properties,
48 | (prop) => prop.type === TSESTree.AST_NODE_TYPES.Property
49 | );
50 |
51 | const args = alternatingSpreadsAndProps.map((chunk) => {
52 | if (chunk[0]?.type === TSESTree.AST_NODE_TYPES.SpreadElement) {
53 | const spreadsArr = chunk as TSESTree.SpreadElement[];
54 |
55 | return spreadsArr
56 | .map((se) => sourceCode.getText(se.argument))
57 | .join(', ');
58 | }
59 |
60 | const propsArr = chunk as TSESTree.Property[];
61 | const propsText = propsArr
62 | .map((prop) => {
63 | const keyText = sourceCode.getText(prop.key);
64 | const valueText = sourceCode.getText(prop.value);
65 |
66 | return `${
67 | prop.computed ? `[${keyText}]` : keyText
68 | }: ${valueText}`;
69 | })
70 | .join(', ');
71 |
72 | return `{ ${propsText} }`;
73 | });
74 |
75 | context.report({
76 | messageId: 'default',
77 | node: argumentNode,
78 | fix: (fixer) => fixer.replaceText(argumentNode, args.join(', ')),
79 | });
80 | }
81 |
82 | // TODO: add support for arrays
83 | });
84 | },
85 | };
86 | },
87 | });
88 |
--------------------------------------------------------------------------------
/src/rules/prefer-logical-over-objects.ts:
--------------------------------------------------------------------------------
1 | import { TSESTree } from '@typescript-eslint/types';
2 |
3 | import { createRule } from '../createRule';
4 | import * as utils from '../utils';
5 |
6 | export = createRule({
7 | name: 'prefer-logical-over-objects',
8 | defaultOptions: [{ startingFrom: 0, endingWith: 999 }],
9 | meta: {
10 | type: 'suggestion',
11 | docs: {
12 | description: 'forbid usage of object expression inside clsx',
13 | },
14 | fixable: 'code',
15 | schema: [
16 | {
17 | type: 'object',
18 | properties: {
19 | startingFrom: { type: 'number' },
20 | endingWith: { type: 'number' },
21 | },
22 | },
23 | ],
24 | messages: {
25 | default: 'Usage of logical expressions is preferred over object expressions',
26 | },
27 | },
28 | create(context, [{ startingFrom, endingWith }]) {
29 | const { sourceCode } = context;
30 | const clsxOptions = utils.extractClsxOptions(context);
31 |
32 | return {
33 | ImportDeclaration(importNode) {
34 | const assignedClsxName = utils.findClsxImport(importNode, clsxOptions);
35 |
36 | if (!assignedClsxName) {
37 | return;
38 | }
39 |
40 | const clsxUsages = utils.getClsxUsages(importNode, sourceCode, assignedClsxName);
41 |
42 | clsxUsages
43 | .flatMap((clsxCallNode) => clsxCallNode.arguments)
44 | // TODO: autofix deep into arrays
45 | .forEach((argumentNode) => {
46 | if (
47 | argumentNode.type !== TSESTree.AST_NODE_TYPES.ObjectExpression ||
48 | argumentNode.properties.length < startingFrom ||
49 | argumentNode.properties.length >= endingWith
50 | )
51 | return;
52 |
53 | const alternatingSpreadsAndProps = utils.chunkBy(
54 | argumentNode.properties,
55 | (prop) => prop.type === TSESTree.AST_NODE_TYPES.Property
56 | );
57 |
58 | const args = alternatingSpreadsAndProps.map((chunk) => {
59 | if (chunk[0]?.type === TSESTree.AST_NODE_TYPES.SpreadElement) {
60 | const spreadsArr = chunk as TSESTree.SpreadElement[];
61 | const spreadsText = spreadsArr
62 | .map((se) => sourceCode.getText(se))
63 | .join(', ');
64 |
65 | return `{ ${spreadsText} }`;
66 | }
67 |
68 | const propsArr = chunk as TSESTree.Property[];
69 |
70 | return propsArr
71 | .map((prop) => {
72 | const keyText = sourceCode.getText(prop.key);
73 | const valueText = sourceCode.getText(prop.value);
74 | const key =
75 | !prop.computed &&
76 | prop.key.type === TSESTree.AST_NODE_TYPES.Identifier
77 | ? `'${keyText}'`
78 | : keyText;
79 |
80 | // TODO: apply `()` conditionally only as needed
81 | return `(${valueText}) && ${key}`;
82 | })
83 | .join(', ');
84 | });
85 |
86 | if (
87 | argumentNode.properties.every(
88 | (prop) => prop.type === TSESTree.AST_NODE_TYPES.SpreadElement
89 | )
90 | ) {
91 | context.report({
92 | messageId: 'default',
93 | node: argumentNode,
94 | });
95 |
96 | return;
97 | }
98 |
99 | context.report({
100 | messageId: 'default',
101 | node: argumentNode,
102 | fix: (fixer) => fixer.replaceText(argumentNode, args.join(', ')),
103 | });
104 | });
105 | },
106 | };
107 | },
108 | });
109 |
--------------------------------------------------------------------------------
/src/rules/prefer-merged-neighboring-elements.ts:
--------------------------------------------------------------------------------
1 | import { TSESTree } from '@typescript-eslint/types';
2 |
3 | import { createRule } from '../createRule';
4 | import * as utils from '../utils';
5 |
6 | export = createRule({
7 | name: 'prefer-merged-neighboring-elements',
8 | defaultOptions: ['object' as const],
9 | meta: {
10 | type: 'suggestion',
11 | docs: {
12 | description: 'enforce merging of neighboring elements',
13 | recommended: true,
14 | },
15 | fixable: 'code',
16 | schema: [{ type: 'array', items: { type: 'string', enum: ['object'] } }],
17 | messages: {
18 | object: 'Neighboring objects should be merged',
19 | },
20 | },
21 | create(context, [mergedFor]) {
22 | const { sourceCode } = context;
23 | const clsxOptions = utils.extractClsxOptions(context);
24 |
25 | return {
26 | ImportDeclaration(importNode) {
27 | const assignedClsxName = utils.findClsxImport(importNode, clsxOptions);
28 |
29 | if (!assignedClsxName) {
30 | return;
31 | }
32 |
33 | const clsxUsages = utils.getClsxUsages(importNode, sourceCode, assignedClsxName);
34 |
35 | clsxUsages
36 | .map((clsxCallNode) => ({
37 | clsxCallNode,
38 | usageChunks: utils.chunkBy(
39 | clsxCallNode.arguments,
40 | (argumentNode) => argumentNode.type
41 | ),
42 | }))
43 | // TODO: autofix deep into arrays
44 | .forEach(({ clsxCallNode, usageChunks }) => {
45 | if (
46 | mergedFor.includes('object') &&
47 | usageChunks.some(
48 | (chunk) =>
49 | chunk[0]?.type === TSESTree.AST_NODE_TYPES.ObjectExpression &&
50 | chunk.length > 1
51 | )
52 | ) {
53 | const args = usageChunks.map((chunk) => {
54 | if (chunk[0]?.type === TSESTree.AST_NODE_TYPES.ObjectExpression) {
55 | const objectsArr = chunk as TSESTree.ObjectExpression[];
56 | const newObjectPropsText = objectsArr
57 | .flatMap((se) => se.properties)
58 | .map((prop) => sourceCode.getText(prop))
59 | .join(', ');
60 |
61 | return `{ ${newObjectPropsText} }`;
62 | }
63 |
64 | return chunk.map((el) => sourceCode.getText(el)).join(', ');
65 | });
66 |
67 | context.report({
68 | messageId: 'object',
69 | node: clsxCallNode,
70 | fix: (fixer) =>
71 | fixer.replaceText(
72 | clsxCallNode,
73 | `${clsxCallNode.callee.name}(${args.join(', ')})`
74 | ),
75 | });
76 | }
77 |
78 | // TODO: add support for arrays and strings
79 | });
80 | },
81 | };
82 | },
83 | });
84 |
--------------------------------------------------------------------------------
/src/rules/prefer-objects-over-logical.test.ts:
--------------------------------------------------------------------------------
1 | import { RuleTester } from '@typescript-eslint/rule-tester';
2 |
3 | import rule from './prefer-objects-over-logical';
4 |
5 | new RuleTester().run('prefer-objects-over-logical', rule, {
6 | valid: [
7 | {
8 | code: `
9 | import clsx from 'clsx';
10 |
11 | const condition = true;
12 | clsx({ 'flex-col': condition });
13 | `,
14 | },
15 | ],
16 | invalid: [
17 | {
18 | code: `
19 | import clsx from 'clsx';
20 |
21 | const condition = true;
22 | clsx(condition && 'flex-col');
23 | `,
24 | errors: [
25 | {
26 | messageId: 'default',
27 | line: 5,
28 | column: 1,
29 | },
30 | ],
31 | output: `
32 | import clsx from 'clsx';
33 |
34 | const condition = true;
35 | clsx({ "flex-col": condition });
36 | `,
37 | },
38 | ],
39 | });
40 |
--------------------------------------------------------------------------------
/src/rules/prefer-objects-over-logical.ts:
--------------------------------------------------------------------------------
1 | import { TSESTree } from '@typescript-eslint/types';
2 |
3 | import { createRule } from '../createRule';
4 | import * as utils from '../utils';
5 |
6 | export = createRule({
7 | name: 'prefer-objects-over-logical',
8 | defaultOptions: [{ startingFrom: 0, endingWith: 999 }],
9 | meta: {
10 | type: 'suggestion',
11 | docs: {
12 | description: 'forbid usage of logical expressions inside clsx',
13 | },
14 | fixable: 'code',
15 | schema: [
16 | {
17 | type: 'object',
18 | properties: {
19 | startingFrom: { type: 'number' },
20 | endingWith: { type: 'number' },
21 | },
22 | },
23 | ],
24 | messages: {
25 | default: 'Usage of object expressions is preferred over logical expressions',
26 | },
27 | },
28 | create(context, [{ startingFrom, endingWith }]) {
29 | const { sourceCode } = context;
30 | const clsxOptions = utils.extractClsxOptions(context);
31 |
32 | return {
33 | ImportDeclaration(importNode) {
34 | const assignedClsxName = utils.findClsxImport(importNode, clsxOptions);
35 |
36 | if (!assignedClsxName) {
37 | return;
38 | }
39 |
40 | const clsxUsages = utils.getClsxUsages(importNode, sourceCode, assignedClsxName);
41 |
42 | clsxUsages
43 | .map((clsxCallNode) => ({
44 | clsxCallNode,
45 | usageChunks: utils.chunkBy(
46 | clsxCallNode.arguments,
47 | (argumentNode) => argumentNode.type
48 | ),
49 | }))
50 | .forEach(({ clsxCallNode, usageChunks }) => {
51 | if (
52 | !usageChunks.some(
53 | (chunk) =>
54 | chunk[0]?.type === TSESTree.AST_NODE_TYPES.LogicalExpression &&
55 | chunk.length >= startingFrom &&
56 | chunk.length < endingWith
57 | )
58 | ) {
59 | return;
60 | }
61 |
62 | const args = usageChunks.map((chunk) => {
63 | if (chunk[0]?.type === TSESTree.AST_NODE_TYPES.LogicalExpression) {
64 | const logicalExpressions = chunk as TSESTree.LogicalExpression[];
65 | const newObjectPropsText = logicalExpressions
66 | .map((prop) => {
67 | const keyText =
68 | prop.right.type === TSESTree.AST_NODE_TYPES.Literal &&
69 | typeof prop.right.value === 'string'
70 | ? `"${prop.right.value}"`
71 | : `[${sourceCode.getText(prop.right)}]`;
72 | const valueText = sourceCode.getText(prop.left);
73 |
74 | return `${keyText}: ${valueText}`;
75 | })
76 | .join(', ');
77 |
78 | return `{ ${newObjectPropsText} }`;
79 | }
80 |
81 | return chunk.map((el) => sourceCode.getText(el)).join(', ');
82 | });
83 |
84 | context.report({
85 | messageId: 'default',
86 | node: clsxCallNode,
87 | fix: (fixer) =>
88 | fixer.replaceText(
89 | clsxCallNode,
90 | `${clsxCallNode.callee.name}(${args.join(', ')})`
91 | ),
92 | });
93 | });
94 | },
95 | };
96 | },
97 | });
98 |
--------------------------------------------------------------------------------
/src/rules/tests-setup.mjs:
--------------------------------------------------------------------------------
1 | import { RuleTester } from '@typescript-eslint/rule-tester';
2 | import * as vitest from 'vitest';
3 |
4 | RuleTester.afterAll = vitest.afterAll;
5 |
6 | // If you are not using vitest with globals: true (https://vitest.dev/config/#globals):
7 | RuleTester.it = vitest.it;
8 | RuleTester.itOnly = vitest.it.only;
9 | RuleTester.describe = vitest.describe;
10 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { TSESTree } from '@typescript-eslint/types';
2 | import type { RuleContext, SourceCode } from '@typescript-eslint/utils/ts-eslint';
3 | import * as R from 'remeda';
4 |
5 | type ClsxOptions = Record;
6 |
7 | export function chunkBy(collection: T[], chunker: (el: T) => unknown) {
8 | const res = [] as T[][];
9 | const temp = [collection[0]] as T[];
10 |
11 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
12 | let lastChunkMarker = chunker(collection[0]!);
13 |
14 | for (const el of collection.slice(1)) {
15 | const currentChunkMarker = chunker(el);
16 |
17 | if (currentChunkMarker === lastChunkMarker) {
18 | temp.push(el);
19 | continue;
20 | }
21 |
22 | if (temp.length !== 0) {
23 | res.push([...temp]);
24 | temp.length = 0;
25 | temp.push(el);
26 | }
27 |
28 | lastChunkMarker = currentChunkMarker;
29 | }
30 |
31 | if (temp.length) res.push(temp);
32 |
33 | return res;
34 | }
35 |
36 | export function findClsxImport(importNode: TSESTree.ImportDeclaration, clsxOptions: ClsxOptions) {
37 | if (typeof importNode.source.value !== 'string') {
38 | throw new Error('import source value is not a string');
39 | }
40 |
41 | const names = clsxOptions[importNode.source.value];
42 | const importNames = typeof names === 'string' ? [names] : names;
43 |
44 | return importNames
45 | ?.map((name) => {
46 | if (name === 'default') {
47 | const defaultSpecifier = importNode.specifiers.find(
48 | (s) => s.type === TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier
49 | );
50 |
51 | return defaultSpecifier?.local.name;
52 | }
53 |
54 | const named = importNode.specifiers.find(
55 | (s) =>
56 | s.type === TSESTree.AST_NODE_TYPES.ImportSpecifier &&
57 | 'name' in s.imported &&
58 | s.imported.name === name
59 | );
60 |
61 | return named?.local.name;
62 | })
63 | .filter(R.isDefined);
64 | }
65 |
66 | function isCallExpressionWithName(name: N) {
67 | return (
68 | node: TSESTree.Node | undefined | null
69 | ): node is TSESTree.CallExpression & { callee: { name: N } } =>
70 | !!node &&
71 | node.type === TSESTree.AST_NODE_TYPES.CallExpression &&
72 | 'name' in node.callee &&
73 | node.callee.name === name;
74 | }
75 |
76 | export function getClsxUsages(
77 | importNode: TSESTree.ImportDeclaration,
78 | sourceCode: SourceCode,
79 | assignedClsxNames: string[]
80 | ) {
81 | return assignedClsxNames
82 | .flatMap((assignedClsxName) =>
83 | sourceCode.scopeManager
84 | ?.getDeclaredVariables(importNode)
85 | .find((variable) => variable.name === assignedClsxName)
86 | ?.references.map(
87 | (ref) => (ref.identifier as unknown as { parent: TSESTree.Node }).parent
88 | )
89 | .filter(R.isDefined)
90 | .filter(isCallExpressionWithName(assignedClsxName))
91 | )
92 | .filter(R.isDefined);
93 | }
94 |
95 | export function extractClsxOptions(
96 | context: RuleContext
97 | ) {
98 | return (context.settings.clsxOptions ?? {
99 | clsx: ['default', 'clsx'],
100 | classnames: 'default',
101 | }) as ClsxOptions;
102 | }
103 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2016",
4 | "lib": ["ESNext"],
5 | "module": "NodeNext",
6 | "resolveJsonModule": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "noUncheckedIndexedAccess": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | entry: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/tests-setup.mjs'],
5 | bundle: false,
6 | });
7 |
--------------------------------------------------------------------------------
/vitest.config.mts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vitest/config';
3 |
4 | export default defineConfig({
5 | test: {
6 | setupFiles: './src/rules/tests-setup.mjs',
7 | },
8 | });
9 |
--------------------------------------------------------------------------------