@fenge/eslint-config 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,261 @@
1
+ import process from "node:process";
2
+ import * as fengeTsPlugin from "@fenge/eslint-plugin-ts";
3
+ import tsParser from "@typescript-eslint/parser";
4
+ import deprecationPlugin from "eslint-plugin-deprecation";
5
+ import { javascript } from "./javascript.js";
6
+
7
+ export function typescript(project?: string) {
8
+ const jsConfig = javascript()[0];
9
+
10
+ const getTsExtensionRules = () => {
11
+ // https://typescript-eslint.io/rules/?=extension
12
+ const extensionRuleKeys = [
13
+ "block-spacing",
14
+ "brace-style",
15
+ "class-methods-use-this",
16
+ "comma-dangle",
17
+ "comma-spacing",
18
+ "consistent-return",
19
+ "default-param-last",
20
+ "dot-notation",
21
+ "func-call-spacing",
22
+ "indent",
23
+ "init-declarations",
24
+ "key-spacing",
25
+ "keyword-spacing",
26
+ "lines-around-comment",
27
+ "lines-between-class-members",
28
+ "max-params",
29
+ "no-array-constructor",
30
+ "no-dupe-class-members",
31
+ "no-empty-function",
32
+ "no-extra-parens",
33
+ "no-extra-semi",
34
+ "no-implied-eval",
35
+ "no-invalid-this",
36
+ "no-loop-func",
37
+ // "no-loss-of-precision",
38
+ "no-magic-numbers",
39
+ "no-redeclare",
40
+ "no-restricted-imports",
41
+ "no-shadow",
42
+ "no-throw-literal",
43
+ "no-unused-expressions",
44
+ "no-unused-vars",
45
+ "no-use-before-define",
46
+ "no-useless-constructor",
47
+ "object-curly-spacing",
48
+ "only-throw-error", // this rule based on 'eslint/no-throw-literal'
49
+ "padding-line-between-statements",
50
+ "prefer-destructuring",
51
+ "prefer-promise-reject-errors",
52
+ "quotes",
53
+ "require-await",
54
+ "return-await", // this rule based on 'eslint/no-return-await' instead of 'eslint/return-await'
55
+ "semi",
56
+ "space-before-blocks",
57
+ "space-before-function-paren",
58
+ "space-infix-ops",
59
+ ] as const;
60
+ type ExtensionRuleKey = (typeof extensionRuleKeys)[number];
61
+ type JsConfigRuleKey = keyof typeof jsConfig.rules;
62
+
63
+ type JsExtensionKey = Extract<ExtensionRuleKey, JsConfigRuleKey>; // Extract
64
+ type TsExtensionKey = `@typescript-eslint/${JsExtensionKey}`;
65
+
66
+ type GetJsKey<T extends TsExtensionKey> =
67
+ T extends `@typescript-eslint/${infer K}` ? K : never;
68
+ type JsResult = {
69
+ [Key in JsExtensionKey]: "off";
70
+ };
71
+ type TsResult = {
72
+ [Key in TsExtensionKey]: (typeof jsConfig.rules)[GetJsKey<Key>];
73
+ };
74
+ type Result = JsResult & TsResult;
75
+
76
+ const extensionRuleKeySet = new Set<string>(extensionRuleKeys);
77
+ return Object.entries(jsConfig.rules)
78
+ .filter(([key]) => extensionRuleKeySet.has(key))
79
+ .reduce(
80
+ (prev, [jsRuleKey, jsRuleValue]) => ({
81
+ ...prev,
82
+ [jsRuleKey]: "off",
83
+ [`@typescript-eslint/${jsRuleKey}`]: jsRuleValue,
84
+ }),
85
+ {} as Result,
86
+ );
87
+ };
88
+
89
+ return [
90
+ {
91
+ ...jsConfig,
92
+ name: "fenge/typescript",
93
+ files: ["**/*.{ts,cts,mts,tsx}"],
94
+ languageOptions: {
95
+ ...jsConfig.languageOptions,
96
+ parser: tsParser, // Unfortunately parser cannot be a string. Eslint should support it. https://eslint.org/docs/latest/use/configure/configuration-files-new#configuring-a-custom-parser-and-its-options
97
+ parserOptions: {
98
+ ...jsConfig.languageOptions.parserOptions,
99
+ tsconfigRootDir: process.cwd(),
100
+ project: project ?? "tsconfig.json",
101
+ },
102
+ },
103
+ plugins: {
104
+ ...jsConfig.plugins,
105
+ deprecation: deprecationPlugin,
106
+ "@fenge-ts": fengeTsPlugin,
107
+ },
108
+ rules: {
109
+ ...jsConfig.rules,
110
+ ...getTsExtensionRules(),
111
+
112
+ // deprecation
113
+ "deprecation/deprecation": "error",
114
+ // fenge
115
+ "@fenge-ts/exact-map-set-type": "error",
116
+ "@fenge-ts/no-const-enum": "error",
117
+ "@fenge-ts/no-declares-in-ts-file": "error",
118
+ "@fenge-ts/no-export-assignment": "error",
119
+ "@fenge-ts/no-property-decorator": "error",
120
+ "@fenge-ts/no-untyped-empty-array": "error",
121
+ // typescript
122
+ "@typescript-eslint/adjacent-overload-signatures": "error",
123
+ // "@typescript-eslint/array-type": ["error", 'array-simple'], // The default option is 'array'. Not very sure if we need to change the option. So disabled it.
124
+ "@typescript-eslint/await-thenable": "error",
125
+ "@typescript-eslint/ban-types": "error",
126
+ "@typescript-eslint/consistent-generic-constructors": "error",
127
+ "@typescript-eslint/consistent-indexed-object-style": "error",
128
+ "@typescript-eslint/consistent-type-assertions": [
129
+ "error",
130
+ {
131
+ assertionStyle: "as",
132
+ objectLiteralTypeAssertions: "allow-as-parameter",
133
+ },
134
+ ],
135
+ "@typescript-eslint/consistent-type-definitions": [
136
+ "error",
137
+ "interface",
138
+ ], // TODO should we change to 'type'?
139
+ "@typescript-eslint/consistent-type-exports": "error",
140
+ // "@typescript-eslint/consistent-type-imports": "error,
141
+ "@typescript-eslint/dot-notation": ["error", { allowKeywords: true }],
142
+ "@typescript-eslint/method-signature-style": "error",
143
+ "@typescript-eslint/naming-convention": [
144
+ "error",
145
+ {
146
+ selector: "function",
147
+ format: ["camelCase", "PascalCase"],
148
+ },
149
+ {
150
+ selector: "variable",
151
+ types: ["function"],
152
+ format: ["camelCase", "PascalCase"], // decorators need PascalCase
153
+ },
154
+ {
155
+ selector: "class",
156
+ format: ["PascalCase"],
157
+ },
158
+ {
159
+ selector: "interface",
160
+ format: ["PascalCase"],
161
+ },
162
+ {
163
+ selector: "typeAlias",
164
+ format: ["PascalCase"],
165
+ },
166
+ {
167
+ selector: "typeParameter",
168
+ format: ["UPPER_CASE", "PascalCase"],
169
+ },
170
+ ],
171
+ "@typescript-eslint/no-array-delete": "error",
172
+ "@typescript-eslint/no-base-to-string": [
173
+ "error",
174
+ { ignoredTypeNames: [] },
175
+ ],
176
+ "@typescript-eslint/no-confusing-non-null-assertion": "error",
177
+ "@typescript-eslint/no-duplicate-enum-values": "error",
178
+ "@typescript-eslint/no-duplicate-type-constituents": "error",
179
+ "@typescript-eslint/no-empty-object-type": "error",
180
+ "@typescript-eslint/no-extra-non-null-assertion": "error",
181
+ "@typescript-eslint/no-floating-promises": [
182
+ "error",
183
+ {
184
+ ignoreVoid: false,
185
+ },
186
+ ],
187
+ "@typescript-eslint/no-for-in-array": "error",
188
+ "@typescript-eslint/no-import-type-side-effects": "error",
189
+ "@typescript-eslint/no-inferrable-types": "error",
190
+ "@typescript-eslint/no-misused-new": "error",
191
+ "@typescript-eslint/no-misused-promises": "error",
192
+ "@typescript-eslint/no-mixed-enums": "error",
193
+ "@typescript-eslint/no-namespace": "error",
194
+ "@typescript-eslint/no-non-null-asserted-optional-chain": "error",
195
+ "@typescript-eslint/no-non-null-assertion": "error",
196
+ "@typescript-eslint/no-require-imports": "error",
197
+ "@typescript-eslint/no-this-alias": "error",
198
+ "@typescript-eslint/no-unnecessary-condition": "error",
199
+ "@typescript-eslint/no-unnecessary-parameter-property-assignment":
200
+ "error",
201
+ "@typescript-eslint/no-unnecessary-template-expression": "error", // js also need this rule
202
+ "@typescript-eslint/no-unnecessary-type-assertion": "error",
203
+ "@typescript-eslint/no-unnecessary-type-constraint": "error",
204
+ "@typescript-eslint/no-unsafe-declaration-merging": "error",
205
+ // '@typescript-eslint/no-unsafe-function-type': "error",
206
+ // "@typescript-eslint/no-wrapper-object-types": "error",
207
+ "@typescript-eslint/only-throw-error": "error",
208
+ "@typescript-eslint/prefer-as-const": "error",
209
+ "@typescript-eslint/prefer-function-type": "error",
210
+ "@typescript-eslint/prefer-optional-chain": "error",
211
+ "@typescript-eslint/prefer-readonly": "error",
212
+ "@typescript-eslint/prefer-ts-expect-error": "error",
213
+ "@typescript-eslint/restrict-plus-operands": [
214
+ "error",
215
+ {
216
+ // allowAny: false,
217
+ allowBoolean: false,
218
+ allowNullish: false,
219
+ allowNumberAndString: false,
220
+ allowRegExp: false,
221
+ },
222
+ ],
223
+ // "@typescript-eslint/restrict-template-expressions": "error",
224
+ "@typescript-eslint/return-await": ["error", "always"],
225
+ "@typescript-eslint/switch-exhaustiveness-check": [
226
+ "error",
227
+ { requireDefaultForNonUnion: true },
228
+ ],
229
+ "@typescript-eslint/unbound-method": "error",
230
+ "@typescript-eslint/unified-signatures": "error",
231
+ },
232
+ },
233
+ {
234
+ name: "fenge/typescript/config",
235
+ files: ["**/*.config.{ts,cts,mts,tsx}"],
236
+ rules: {
237
+ "import/no-default-export": "off",
238
+ },
239
+ },
240
+ {
241
+ name: "fenge/typescript/declaration",
242
+ files: ["**/*.d.{ts,cts,mts,tsx}"],
243
+ rules: {
244
+ "import/no-default-export": "off",
245
+ },
246
+ },
247
+ {
248
+ // https://github.com/motemen/minimatch-cheat-sheet
249
+ name: "fenge/typescript/test",
250
+ files: [
251
+ "**/__tests__/**/*.{ts,cts,mts,tsx}",
252
+ "**/*.{test,spec}.{ts,cts,mts,tsx}",
253
+ ],
254
+ rules: {
255
+ "@typescript-eslint/no-floating-promises": "off",
256
+ "@typescript-eslint/unbound-method": "off",
257
+ "esm/no-phantom-dep-imports": ["error", { allowDevDependencies: true }],
258
+ },
259
+ },
260
+ ] as const;
261
+ }
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import config from "./eslint.config.js";
4
+
5
+ await describe("eslint.config", async () => {
6
+ await it("length of default export should be 9", () => {
7
+ assert.strictEqual(config.length, 9);
8
+ });
9
+ await it("no warns", () => {
10
+ config.forEach((configItem) => {
11
+ if (
12
+ "rules" in configItem &&
13
+ typeof configItem.rules === "object" &&
14
+ configItem.rules
15
+ ) {
16
+ Object.values(configItem.rules).forEach((value) => {
17
+ assert.notStrictEqual(getValueString(value), "warn");
18
+ });
19
+ }
20
+ });
21
+ });
22
+ });
23
+
24
+ function getValueString(value: unknown): string {
25
+ if (typeof value === "string") {
26
+ return value;
27
+ } else if (Array.isArray(value) && typeof value[0] === "string") {
28
+ return value[0];
29
+ } else {
30
+ throw new Error("unknown value");
31
+ }
32
+ }
@@ -0,0 +1,100 @@
1
+ import { gitignore } from "./config/gitignore.js";
2
+ import { ignore } from "./config/ignore.js";
3
+ import { javascript } from "./config/javascript.js";
4
+ import { packagejson } from "./config/packagejson.js";
5
+ import { typescript } from "./config/typescript.js";
6
+
7
+ type JsRuleKey = keyof ReturnType<typeof javascript>[0]["rules"];
8
+ type TsRuleKey = keyof ReturnType<typeof typescript>[0]["rules"];
9
+ type PkgRuleKey = keyof ReturnType<typeof packagejson>[0]["rules"];
10
+
11
+ interface Options<T extends string> {
12
+ pick?: T[];
13
+ omit?: T[];
14
+ extend?: Record<
15
+ string,
16
+ "error" | "warn" | "off" | ["error" | "warn", ...unknown[]]
17
+ >;
18
+ override?: Partial<
19
+ Record<T, "error" | "warn" | "off" | ["error" | "warn", ...unknown[]]>
20
+ >;
21
+ }
22
+
23
+ export class Builder {
24
+ private readonly configs: object[] = [...gitignore(), ...ignore()];
25
+
26
+ toConfig() {
27
+ return this.configs;
28
+ }
29
+
30
+ private setup(
31
+ [mainConfig, ...otherConfigs]: readonly [
32
+ { plugins: object; rules: object },
33
+ ...object[],
34
+ ],
35
+ { pick, omit, extend = {}, override = {} }: Options<string>,
36
+ ) {
37
+ const select = (ruleKey: string) => {
38
+ if (!pick && !omit) {
39
+ return true;
40
+ } else if (pick && !omit) {
41
+ return pick.includes(ruleKey);
42
+ } else if (!pick && omit) {
43
+ return !omit.includes(ruleKey);
44
+ } else {
45
+ throw new Error("You cannot specify both pick and omit");
46
+ }
47
+ };
48
+ const rules = Object.fromEntries(
49
+ Object.entries(mainConfig.rules).filter(([ruleKey]) => select(ruleKey)),
50
+ );
51
+ // check `override` field
52
+ Object.keys(override).forEach((key) => {
53
+ if (!(key in rules)) {
54
+ throw new Error(`The overriding rule key ${key} is not existing.`);
55
+ }
56
+ });
57
+ // check `extend` field
58
+ Object.keys(extend).forEach((key) => {
59
+ if (key in rules) {
60
+ throw new Error(`The extending rule key ${key} is already existing.`);
61
+ }
62
+ if (key.includes("/")) {
63
+ const pluginName = key.split("/")[0];
64
+ if (!pluginName)
65
+ throw new Error(`The extending rule key '${key}' is invalid`);
66
+ if (!(pluginName in mainConfig.plugins)) {
67
+ const supportedPlugins = Object.keys(mainConfig.plugins)
68
+ .map((k) => `'${k}'`)
69
+ .join(",");
70
+ throw new Error(
71
+ `The plugin name '${pluginName}' of extending rule key '${key}' is not supported. Only ${supportedPlugins} plugins are supported.`,
72
+ );
73
+ }
74
+ }
75
+ });
76
+ this.configs.push(
77
+ { ...mainConfig, rules: { ...rules, ...override, ...extend } },
78
+ ...otherConfigs,
79
+ );
80
+ return this;
81
+ }
82
+
83
+ enableTypescript(options: Options<TsRuleKey> & { project?: string } = {}) {
84
+ return this.setup(typescript(options.project), options);
85
+ }
86
+
87
+ enableJavascript(options: Options<JsRuleKey> = {}) {
88
+ return this.setup(javascript(), options);
89
+ }
90
+
91
+ enablePackagejson(options: Options<PkgRuleKey> = {}) {
92
+ return this.setup(packagejson(), options);
93
+ }
94
+ }
95
+
96
+ export default new Builder()
97
+ .enablePackagejson()
98
+ .enableJavascript()
99
+ .enableTypescript()
100
+ .toConfig();
@@ -0,0 +1,8 @@
1
+ declare module "eslint-plugin-*" {
2
+ declare const plugin: unknown;
3
+ export default plugin;
4
+ }
5
+ declare module "confusing-browser-globals" {
6
+ declare const keys: string[];
7
+ export default keys;
8
+ }
@@ -0,0 +1,61 @@
1
+ import assert from "node:assert";
2
+ import fs from "node:fs/promises";
3
+ import { describe, it } from "node:test";
4
+ import prettier from "prettier";
5
+ import { javascript } from "../src/config/javascript.js";
6
+ import { typescript } from "../src/config/typescript.js";
7
+
8
+ function count(content: string, substring: string) {
9
+ return (content.match(new RegExp(`"${substring}"`, "g")) ?? []).length;
10
+ }
11
+
12
+ await describe("no duplicated", async () => {
13
+ await it("no duplicated js rules is defined", async () => {
14
+ const configContent = await prettier.format(
15
+ (await fs.readFile("./src/config/javascript.ts", "utf-8")).replace(
16
+ "// prettier-ignore",
17
+ "",
18
+ ),
19
+ { parser: "typescript", quoteProps: "consistent" },
20
+ );
21
+
22
+ Object.keys(javascript()[0].rules)
23
+ .filter(
24
+ (rule) =>
25
+ !["import/no-default-export", "esm/no-phantom-dep-imports"].includes(
26
+ rule,
27
+ ),
28
+ )
29
+ .forEach((rule) => {
30
+ assert.strictEqual(count(configContent, rule), 1);
31
+ });
32
+ });
33
+
34
+ await it("no duplicated ts rules is defined", async () => {
35
+ const configContent = await prettier.format(
36
+ (await fs.readFile("./src/config/typescript.ts", "utf-8")).replace(
37
+ "// prettier-ignore",
38
+ "",
39
+ ),
40
+ { parser: "typescript", quoteProps: "consistent" },
41
+ );
42
+
43
+ typescript()
44
+ .flatMap((c) => Object.keys(c.rules))
45
+ .forEach((rule) => {
46
+ if (
47
+ [
48
+ // "@typescript-eslint/consistent-type-assertions",
49
+ "@typescript-eslint/no-floating-promises",
50
+ // "@typescript-eslint/no-non-null-assertion",
51
+ "@typescript-eslint/unbound-method",
52
+ "import/no-default-export",
53
+ ].includes(rule)
54
+ ) {
55
+ assert.strictEqual(count(configContent, rule), 2);
56
+ } else {
57
+ assert.strictEqual(count(configContent, rule) <= 1, true);
58
+ }
59
+ });
60
+ });
61
+ });
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import prettierConfig from "eslint-config-prettier";
4
+ import { javascript } from "../src/config/javascript.js";
5
+ import { packagejson } from "../src/config/packagejson.js";
6
+ import { typescript } from "../src/config/typescript.js";
7
+
8
+ await describe("prettier", async () => {
9
+ await it("prettier config should be standard", () => {
10
+ const properties = Object.keys(prettierConfig);
11
+ assert.deepStrictEqual(properties, ["rules"]);
12
+
13
+ const ruleValues = [...new Set(Object.values(prettierConfig.rules))];
14
+ assert.strictEqual(ruleValues.length, 2);
15
+ assert.strictEqual(ruleValues.includes(0), true);
16
+ assert.strictEqual(ruleValues.includes("off"), true);
17
+ });
18
+
19
+ await it("should not have prettier-conflicted rules", () => {
20
+ const included = (rule: string) => rule in prettierConfig.rules;
21
+
22
+ // 1
23
+ const jsForbidRule = Object.keys(javascript()[0].rules).find((rule) =>
24
+ included(rule),
25
+ );
26
+ assert.strictEqual(jsForbidRule, undefined);
27
+
28
+ // 2
29
+ assert.strictEqual(typeof typescript()[0], "object");
30
+ assert.strictEqual(typescript().length, 4);
31
+ const tsForbidRule = typescript()
32
+ .flatMap((config) => Object.keys(config.rules))
33
+ .find((rule) => included(rule));
34
+ assert.strictEqual(tsForbidRule, undefined);
35
+
36
+ // 3
37
+ const packagejsonRule = Object.keys(packagejson()[0].rules).find((rule) =>
38
+ included(rule),
39
+ );
40
+ assert.strictEqual(packagejsonRule, undefined);
41
+ });
42
+ });
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "./tsconfig",
3
+ "include": ["src"],
4
+ "exclude": ["**/*.spec.ts", "**/*.test.ts"]
5
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../tsconfig"
3
+ }