Skip to content

Commit b18bfab

Browse files
alpha/v4-settings (#416)
* refactor * playing with settings * settings
1 parent 0605eeb commit b18bfab

File tree

14 files changed

+193
-42
lines changed

14 files changed

+193
-42
lines changed

docs/rules/my-rule.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ Examples would normally go here.
1616

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

19-
| Name | Description | Type | Choices | Default |
20-
| :--------- | :-------------------- | :------ | :---------------- | :------- |
21-
| `someBool` | someBool description. | Boolean | | `true` |
22-
| `someEnum` | someEnum description. | String | `always`, `never` | `always` |
19+
| Name | Description | Type | Choices | Default |
20+
| :------------------- | :------------------------------------------------------------------- | :------- | :---------------- | :--------------------- |
21+
| `callees` | List of function names to validate classnames | String[] | | [`ctl`] |
22+
| `cssConfigPath` | Path to the Tailwind CSS configuration file (*.css) | String | | `default-path/app.css` |
23+
| `removeDuplicates` | Remove duplicated classnames | Boolean | | `true` |
24+
| `skipClassAttribute` | If you only want to lint the classnames inside one of the `callees`. | Boolean | | `false` |
25+
| `someBool` | someBool description. | Boolean | | `false` |
26+
| `someEnum` | someEnum description. | String | `always`, `never` | `always` |
27+
| `tags` | List of tags to be detected in template literals | String[] | | [`tw`] |
2328

2429
<!-- end auto-generated rule options list -->

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ module.exports = {
66
testMatch: ["**/*.spec.ts"],
77
// Jest running with imported vitest utils causes errors like:
88
// Vitest cannot be imported in a CommonJS module using require(). Please use "import" instead.
9-
modulePathIgnorePatterns: ["<rootDir>/src/util/tailwindcss-api/worker/"],
9+
modulePathIgnorePatterns: ["<rootDir>/src/utils/tailwindcss-api/worker/"],
1010
};

src/index.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
1-
import { RuleModule } from "@typescript-eslint/utils/ts-eslint";
2-
import { ESLint } from "eslint";
1+
import * as parserBase from "@typescript-eslint/parser";
2+
import { TSESLint } from "@typescript-eslint/utils";
3+
import { Linter } from "@typescript-eslint/utils/ts-eslint";
34

45
import { rules } from "./rules";
56

6-
type RuleKey = keyof typeof rules;
7-
8-
interface Plugin extends Omit<ESLint.Plugin, "rules"> {
9-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10-
rules: Record<RuleKey, RuleModule<any, any, any>>;
11-
}
7+
export const parser: TSESLint.FlatConfig.Parser = {
8+
meta: parserBase.meta,
9+
parseForESLint: parserBase.parseForESLint,
10+
};
1211

1312
const { name, version } =
1413
// `import`ing here would bypass the TSConfig's `"rootDir": "src"`
14+
// Also an import statement will make TSC copy the package.json to the dist folder
1515
// eslint-disable-next-line @typescript-eslint/no-require-imports
16-
require("../package.json") as typeof import("../package.json");
16+
require("../package.json") as {
17+
name: string;
18+
version: string;
19+
};
20+
21+
/**
22+
* TODO: Add configs (recommended, etc.)
23+
* @see https://github.com/typescript-eslint/examples/blob/main/packages/eslint-plugin-example-typed-linting/src/index.ts
24+
*/
1725

18-
const plugin: Plugin = {
19-
/**
20-
* TODO: Add configs (recommended, etc.)
21-
* @see https://github.com/typescript-eslint/examples/blob/main/packages/eslint-plugin-example-typed-linting/src/index.ts
22-
*/
26+
// Plugin not fully initialized yet.
27+
// See https://eslint.org/docs/latest/extend/plugins#configs-in-plugins
28+
const plugin = {
29+
// `configs`, assigned later
30+
configs: {},
31+
rules,
2332
meta: {
2433
name,
2534
version,
2635
},
27-
rules,
28-
};
36+
} satisfies Linter.Plugin;
2937

30-
export = plugin;
38+
export default plugin;

src/rules/my-rule.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,30 @@ ruleTester.run(RULE_NAME, myRule, {
1919
{
2020
// a code snippet that should pass the linter
2121
code: `const x = 5;`,
22+
options: [
23+
{
24+
callees: ["ctl2"],
25+
someBool: true,
26+
someEnum: "always",
27+
},
28+
],
2229
},
2330
{
2431
code: `let y = 'abc123';`,
2532
},
2633
{
2734
code: `<button onClick={() => { const name = 'John'; alert(name); }}>JSX</button>`,
2835
},
36+
{
37+
// a code snippet that should pass the linter
38+
code: `var x = 5;`,
39+
options: [
40+
{
41+
someBool: true,
42+
someEnum: "always",
43+
},
44+
],
45+
},
2946
],
3047
invalid: [
3148
{

src/rules/my-rule.ts

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import { RuleCreator } from "@typescript-eslint/utils/eslint-utils";
22

33
import { PluginSharedSettings } from "../types";
44
import urlCreator from "../url-creator";
5+
import {
6+
DEFAULTS,
7+
parsePluginSettings,
8+
type PluginSettings,
9+
sharedSettingsSchema,
10+
} from "../utils/parse-plugin-settings";
511

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

@@ -10,13 +16,28 @@ export const RULE_NAME = "my-rule";
1016
// Message IDs don't need to be prefixed, I just find it easier to keep track of them this way
1117
type MessageIds = "issue:var" | "fix:let" | "fix:const";
1218

13-
// The options that the rule can take
14-
type Options = [
15-
{
16-
someBool: boolean;
17-
someEnum: string;
18-
}
19-
];
19+
/**
20+
* The extra options that the rule can accept.
21+
* These options get merged with the shared settings.
22+
* The typing is not used by `eslint-doc-generator` which uses the `schema` property in the rule's metadata.
23+
* Yet, it is useful for the IDE to provide autocompletion and type checking.
24+
*/
25+
type RuleOptions = {
26+
someBool: boolean;
27+
someEnum: string;
28+
};
29+
30+
export type MergedOptions = RuleOptions & PluginSettings;
31+
32+
const RULE_DEFAULT: RuleOptions = {
33+
someBool: false,
34+
someEnum: "always",
35+
};
36+
37+
// TODO which one ?
38+
// - type Options = [RuleOptions];
39+
// - type Options = [MergedOptions];
40+
type Options = [MergedOptions];
2041

2142
// The Rule creator returns a function that is used to create a well-typed ESLint rule
2243
// The parameter passed into RuleCreator is a URL generator function.
@@ -34,20 +55,22 @@ export const myRule = createRule<Options, MessageIds>({
3455
"fix:let": "Replace this `var` declaration with `let`",
3556
"fix:const": "Replace this `var` declaration with `const`",
3657
},
58+
// Schema is also parsed by `eslint-doc-generator`
3759
schema: [
3860
{
3961
type: "object",
4062
properties: {
63+
...sharedSettingsSchema,
4164
someBool: {
4265
description: "someBool description.",
4366
type: "boolean",
44-
default: true,
67+
default: RULE_DEFAULT.someBool,
4568
},
4669
someEnum: {
4770
description: "someEnum description.",
4871
type: "string",
4972
enum: ["always", "never"],
50-
default: "always",
73+
default: RULE_DEFAULT.someEnum,
5174
},
5275
},
5376
additionalProperties: false,
@@ -62,21 +85,37 @@ export const myRule = createRule<Options, MessageIds>({
6285
* - If some configuration is provided as the second argument, it is ignored, not merged
6386
* - In other words, the `defaultOptions` is only used when the rule is used without configuration
6487
*/
65-
defaultOptions: [{ someBool: false, someEnum: "always" }],
88+
defaultOptions: [
89+
{
90+
...DEFAULTS,
91+
someBool: false,
92+
someEnum: "always",
93+
},
94+
],
6695
create: (context, options) => {
96+
// Reading inline configuration
97+
console.log("\n", new Date(), "\n", "Options (rule):", "\n", options[0]);
98+
99+
// Shared settings
100+
const sharedSettings = (context.settings?.tailwindcss ||
101+
DEFAULTS) as PluginSharedSettings;
102+
console.log("\n", "sharedSettings (rule):", "\n", sharedSettings);
103+
104+
// Merged settings
105+
// const merged = parsePluginSettings(options[0]) as RuleOptions;
106+
const merged = parsePluginSettings<RuleOptions>({
107+
tailwindcss: options[0],
108+
}) as RuleOptions;
109+
console.log("\n", "merged (rule):", "\n", merged);
110+
67111
return {
68112
VariableDeclaration: (node) => {
113+
console.log("\n", "merged.someBool:", "\n", merged.someBool);
114+
if (merged.someBool === true) {
115+
console.log("someBool is true, processing VariableDeclaration");
116+
return;
117+
}
69118
if (node.kind === "var") {
70-
// Reading inline configuration
71-
console.log("\n", "Options:", "\n", options[0]);
72-
73-
// Shared settings
74-
const sharedSettings = (context.settings?.tailwindcss || {
75-
stylesheet: "",
76-
functions: [],
77-
}) as PluginSharedSettings;
78-
console.log("\n", "sharedSettings:", "\n", sharedSettings);
79-
80119
const rangeStart = node.range[0];
81120
const range: readonly [number, number] = [
82121
rangeStart,

src/utils/parse-plugin-settings.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Inspired by https://github.com/vitest-dev/eslint-plugin-vitest/blob/7fb864c28f91a92891c7a4fa025ca9d2a9780d49/src/utils/parse-plugin-settings.ts
2+
3+
import { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
4+
import type { SharedConfigurationSettings } from "@typescript-eslint/utils/ts-eslint";
5+
6+
/**
7+
* Typing of the shared settings of the eslint-plugin-tailwindcss.
8+
*/
9+
export type PluginSettings = {
10+
callees?: Array<string>; // TODO can we use a {Set<string>} instead ?
11+
cssConfigPath?: string;
12+
removeDuplicates?: boolean;
13+
skipClassAttribute?: boolean;
14+
tags?: Array<string>; // TODO can we use a {Set<string>} instead ?
15+
};
16+
17+
/**
18+
* The default values for the shared settings.
19+
*/
20+
export const DEFAULTS: PluginSettings = {
21+
callees: ["ctl"],
22+
cssConfigPath: "default-path/app.css",
23+
removeDuplicates: true,
24+
skipClassAttribute: false,
25+
tags: ["tw"],
26+
};
27+
28+
/**
29+
* The JSON schema for the shared settings to be reused in many of the rule's configuration.
30+
*/
31+
export const sharedSettingsSchema: Record<keyof PluginSettings, JSONSchema4> = {
32+
callees: {
33+
description: "List of function names to validate classnames",
34+
type: "array",
35+
items: { type: "string", minLength: 0 },
36+
uniqueItems: true,
37+
default: DEFAULTS.callees,
38+
},
39+
cssConfigPath: {
40+
description: "Path to the Tailwind CSS configuration file (*.css)",
41+
type: "string",
42+
default: DEFAULTS.cssConfigPath,
43+
},
44+
removeDuplicates: {
45+
description: "Remove duplicated classnames",
46+
type: "boolean",
47+
default: DEFAULTS.removeDuplicates,
48+
},
49+
skipClassAttribute: {
50+
description:
51+
"If you only want to lint the classnames inside one of the `callees`.",
52+
type: "boolean",
53+
default: DEFAULTS.skipClassAttribute,
54+
},
55+
tags: {
56+
description: "List of tags to be detected in template literals",
57+
type: "array",
58+
items: { type: "string", minLength: 0 },
59+
uniqueItems: true,
60+
default: DEFAULTS.tags,
61+
},
62+
};
63+
64+
/**
65+
* @description Parses the global eslint settings and merge.
66+
* @param settings The shared settings from the ESLint configuration.
67+
* @returns The parsed plugin settings.
68+
*/
69+
export function parsePluginSettings<RuleOptions>(
70+
settings: SharedConfigurationSettings
71+
): PluginSettings & RuleOptions {
72+
const tailwindcssSettings = (
73+
typeof settings.tailwindcss !== "object" || settings.tailwindcss === null
74+
? {}
75+
: settings.tailwindcss
76+
) as PluginSettings & RuleOptions;
77+
78+
return {
79+
...DEFAULTS,
80+
...tailwindcssSettings,
81+
};
82+
}
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)