Skip to content

feat(rule): classnames-order #417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: alpha/v4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
name: Bug report
about: Create a report to help us improve
title: "[BUG] "
labels: 'bug'
assignees: ''

labels: "bug"
assignees: ""
---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'

1. Go to '…'
2. Click on '…'
3. Scroll down to '…'
4. See error

**Expected behavior**
Expand All @@ -24,10 +24,11 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.

**Environment (please complete the following information):**
- OS: [e.g. macOS, windows 10]
- Softwares + version used:
- [e.g. VSCode 1.54.3]
- [... Terminal 2.9.5, npm 6.14.5, node v14.5.0]

- OS: [e.g. macOS, windows 10]
- Softwares + version used:
- [e.g. VSCode 1.54.3]
- [… Terminal 2.9.5, npm 6.14.5, node v14.5.0]

**Additional context**
Add any other context about the problem here.
Expand Down
7 changes: 3 additions & 4 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
name: Feature request
about: Suggest an idea for this project
title: "[Feature request] "
labels: 'enhancement'
assignees: ''

labels: "enhancement"
assignees: ""
---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
A clear and concise description of what the problem is. Ex. I'm always frustrated when []

**Describe the solution you'd like**
A clear and concise description of what you want to happen.
Expand Down
4 changes: 2 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ Please also list any relevant details for your test configuration
**Test Configuration**:

- OS + version: e.g. macOS Mojave
- NPM version: ...
- Node version: ...
- NPM version:
- Node version:

## Checklist:

Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ This version is far from finished, yet it is available and open for contribution

- restore the automated tests running on the merge requests of the repo
- implement and test the usage of `tailwind-api-utils`
- read the settings from eslint (shared settings & rules settings)

### Next steps

- read the settings from eslint (shared settings & rules settings)
- create the config utility
- implement the `classnames-order` rule and its tests

Expand All @@ -58,7 +58,7 @@ or

#### `jest` or `vitest`

Tests were setup to work with `jest` and `vitest` both comes with pros and cons...
Tests were setup to work with `jest` and `vitest` both comes with pros and cons

I would recommend Vitest but I also added Jest in case you want it.

Expand Down Expand Up @@ -99,19 +99,21 @@ You can see an example of generated documentation in the next section.

<!-- begin auto-generated rules list -->

🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).

| Name | Description | 💡 |
| :------------------------------- | :--------------------- | :- |
| [my-rule](docs/rules/my-rule.md) | An example ESLint rule | 💡 |
| Name             | Description | 🔧 | 💡 |
| :------------------------------------------------- | :-------------------------------------------------------------------- | :-- | :-- |
| [classnames-order](docs/rules/classnames-order.md) | Enforce a consistent and logical order of the Tailwind CSS classnames | 🔧 | 💡 |
| [my-rule](docs/rules/my-rule.md) | An example ESLint rule | | 💡 |

<!-- end auto-generated rules list -->

## Additional resources

See [`eslint-plugin-example-typed-linting`](https://github.com/typescript-eslint/examples/tree/main/packages/eslint-plugin-example-typed-linting) for an example plugin that supports typed linting.

Another example of eslint-plugin using `typescript-eslint` is [`eslint-plugin-vitest`](https://github.com/vitest-dev/eslint-plugin-vitest)...
Another example of eslint-plugin using `typescript-eslint` is [`eslint-plugin-vitest`](https://github.com/vitest-dev/eslint-plugin-vitest)

## 🤝 Support `eslint-plugin-tailwindcss`

Expand Down
2 changes: 1 addition & 1 deletion docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Running `pnpm docs:update` will update the existing docs.

## Generated sections

The generated sections are surrounded by HTML comments mentioning "auto-generated".
The generated sections are surrounded by HTML comments mentioning "auto-generated".
19 changes: 19 additions & 0 deletions docs/rules/classnames-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Enforce a consistent and logical order of the Tailwind CSS classnames

🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).

<!-- end auto-generated rule header -->

## Options

<!-- begin auto-generated rule options list -->

| Name | Description | Type | Default |
| :------------------- | :------------------------------------------------------------------- | :------- | :--------------------- |
| `callees` | List of function names to validate classnames | String[] | [`ctl`] |
| `cssConfigPath` | Path to the Tailwind CSS configuration file (*.css) | String | `default-path/app.css` |
| `removeDuplicates` | Remove duplicated classnames | Boolean | `true` |
| `skipClassAttribute` | If you only want to lint the classnames inside one of the `callees`. | Boolean | `false` |
| `tags` | List of tags to be detected in template literals | String[] | [`tw`] |

<!-- end auto-generated rule options list -->
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const { name, version } =
/**
* TODO: Add configs (recommended, etc.)
* @see https://github.com/typescript-eslint/examples/blob/main/packages/eslint-plugin-example-typed-linting/src/index.ts
* @see eslint-plugin-vitest/src/index.ts
*/

// Plugin not fully initialized yet.
Expand Down
39 changes: 39 additions & 0 deletions src/rules/classnames-order.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as Parser from "@typescript-eslint/parser";
import { RuleTester } from "@typescript-eslint/rule-tester";

import { classnamesOrder, RULE_NAME } from "./classnames-order";

const ruleTester = new RuleTester({
languageOptions: {
parser: Parser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
});

ruleTester.run(RULE_NAME, classnamesOrder, {
valid: [
{
code: `<div className="flex">flex</div>`,
},
],
invalid: [
{
code: `<div className={ctl('flex')}>cn flex</div>`,
options: [
{
callees: ["ctl"],
},
],
errors: [
{
messageId: "fix:sort",
},
],
output: `<div className={TLC('flex')}>cn flex</div>`,
},
],
});
94 changes: 94 additions & 0 deletions src/rules/classnames-order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { RuleCreator } from "@typescript-eslint/utils/eslint-utils";

import urlCreator from "../url-creator";
import {
DEFAULTS,
parsePluginSettings,
type PluginSettings,
sharedSettingsSchema,
} from "../utils/parse-plugin-settings";

export { ESLintUtils } from "@typescript-eslint/utils";

export const RULE_NAME = "classnames-order";

// Message IDs don't need to be prefixed, I just find it easier to keep track of them this way
type MessageIds = "fix:sort";

export type MergedOptions = PluginSettings;

// TODO which one ?
// - type Options = [RuleOptions];
// - type Options = [MergedOptions];
type Options = [MergedOptions];

// The Rule creator returns a function that is used to create a well-typed ESLint rule
// The parameter passed into RuleCreator is a URL generator function.
export const createRule = RuleCreator(urlCreator);

export const classnamesOrder = createRule<Options, MessageIds>({
name: RULE_NAME,
meta: {
docs: {
description:
"Enforce a consistent and logical order of the Tailwind CSS classnames",
},
hasSuggestions: true,
messages: {
"fix:sort": "Invalid Tailwind CSS classnames order",
},
fixable: "code",
// Schema is also parsed by `eslint-doc-generator`
schema: [
{
type: "object",
properties: {
...sharedSettingsSchema,
},
additionalProperties: false,
},
],
type: "suggestion",
},
/**
* About `defaultOptions`:
* - `defaultOptions` is not used in the generated documentation
* - `defaultOptions` is used when options are not provided in the rules configuration
* - If some configuration is provided as the second argument, it is ignored, not merged
* - In other words, the `defaultOptions` is only used when the rule is used without configuration
*/
defaultOptions: [
{
...DEFAULTS,
},
],
create: (context, options) => {
// Merged settings
// const merged = parsePluginSettings(options[0]) as RuleOptions;
const merged = parsePluginSettings<MergedOptions>({
tailwindcss: options[0],
}) as MergedOptions;
console.log("\n", "merged (rule):", "\n", merged);

return {
CallExpression(node) {
if (
node.callee &&
((node.callee.type === "Identifier" && node.callee.name === "ctl") ||
(node.callee.type === "MemberExpression" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "ctl"))
) {
// Add your logic here for when the callee is "ctl"
const rangeStart = node.range[0];
const range: readonly [number, number] = [rangeStart, rangeStart + 3];
context.report({
node,
messageId: "fix:sort", // Prints the message with this ID when a problem is found
fix: (fixer) => fixer.replaceTextRange(range, "TLC"), // TODO use the correct value
});
}
},
};
},
});
5 changes: 5 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {
classnamesOrder,
RULE_NAME as CLASSNAMES_ORDER,
} from "./classnames-order";
import { myRule, RULE_NAME as MY_RULE } from "./my-rule";

export const rules = {
[CLASSNAMES_ORDER]: classnamesOrder,
[MY_RULE]: myRule,
};
3 changes: 0 additions & 3 deletions src/rules/my-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ const RULE_DEFAULT: RuleOptions = {
someEnum: "always",
};

// TODO which one ?
// - type Options = [RuleOptions];
// - type Options = [MergedOptions];
type Options = [MergedOptions];

// The Rule creator returns a function that is used to create a well-typed ESLint rule
Expand Down
4 changes: 2 additions & 2 deletions src/utils/tailwindcss-api/worker/get-sorted-class-names.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* ⚠️ This is a worker script which is ran in node's `worker_threads`.
* 🤓 This means that it is not executed in the main thread, but in a separate thread.
* 😅 Because of this, expect some disturbances, like:
* - `console.log` won't work `vitest`... (seems to work with `jest`).
* - `console.log` won't work `vitest` (seems to work with `jest`).
* - You cannot pass complex objects as arguments, only serializable ones.
* - You cannot retun complex objects, only serializable ones.
* - e.g. You cannot return the `utils.context` directly, but you can return some of its properties...
* - e.g. You cannot return the `utils.context` directly, but you can return some of its properties
*
* ℹ️ It uses the `*.mjs` extension to indicate that it is an ES module.
* ✅ We still check the syntax with TypeScript, but it is not a TypeScript file.
Expand Down
4 changes: 2 additions & 2 deletions src/utils/tailwindcss-api/worker/is-valid-class-name.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* ⚠️ This is a worker script which is ran in node's `worker_threads`.
* 🤓 This means that it is not executed in the main thread, but in a separate thread.
* 😅 Because of this, expect some disturbances, like:
* - `console.log` won't work `vitest`... (seems to work with `jest`).
* - `console.log` won't work `vitest` (seems to work with `jest`).
* - You cannot pass complex objects as arguments, only serializable ones.
* - You cannot retun complex objects, only serializable ones.
* - e.g. You cannot return the `utils.context` directly, but you can return some of its properties...
* - e.g. You cannot return the `utils.context` directly, but you can return some of its properties
*
* ℹ️ It uses the `*.mjs` extension to indicate that it is an ES module.
* ✅ We still check the syntax with TypeScript, but it is not a TypeScript file.
Expand Down
4 changes: 2 additions & 2 deletions src/utils/tailwindcss-api/worker/load-theme.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* ⚠️ This is a worker script which is ran in node's `worker_threads`.
* 🤓 This means that it is not executed in the main thread, but in a separate thread.
* 😅 Because of this, expect some disturbances, like:
* - `console.log` won't work `vitest`... (seems to work with `jest`).
* - `console.log` won't work `vitest` (seems to work with `jest`).
* - You cannot pass complex objects as arguments, only serializable ones.
* - You cannot retun complex objects, only serializable ones.
* - e.g. You cannot return the `utils.context` directly, but you can return some of its properties...
* - e.g. You cannot return the `utils.context` directly, but you can return some of its properties
*
* ℹ️ It uses the `*.mjs` extension to indicate that it is an ES module.
* ✅ We still check the syntax with TypeScript, but it is not a TypeScript file.
Expand Down