@webadeva/eslint-config 0.0.0-stage → 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/dist/index.cjs +384 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +5 -0
- package/dist/next-CWjuCwpN.mjs +9 -0
- package/dist/next.d.mts +5 -0
- package/dist/next.mjs +12 -0
- package/dist/react-C-VV0l5P.mjs +59 -0
- package/dist/react.d.mts +5 -0
- package/dist/react.mjs +10 -0
- package/dist/storybook.d.mts +5 -0
- package/dist/storybook.mjs +22 -0
- package/dist/untyped-Wrh4hLMj.mjs +353 -0
- package/package.json +129 -4
- package/tsconfig.default.json +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gobind Singh
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @webadeva/eslint-config
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@webadeva/eslint-config)
|
|
4
|
+
[](https://github.com/webadeva/eslint-config/actions/workflows/release.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Shared ESLint flat configs for TypeScript projects, with type-aware rules, Prettier, and presets for React, Next.js, and Storybook.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Node.js `>=20.19`
|
|
12
|
+
- ESLint `>=10.13`
|
|
13
|
+
- Prettier `>=3`
|
|
14
|
+
- TypeScript `>=5 <7`, installed in your project. TypeScript 7 is not supported, because typescript-eslint needs the compiler API it removed.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add -D @webadeva/eslint-config eslint prettier typescript@6
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `typescript@6`, not a bare `typescript`. A bare install pulls the latest version, which may be TypeScript 7 (or later), and that isn't supported.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Create an ESLint config file. It can be `eslint.config.ts`, `eslint.config.js`, or `eslint.config.mjs` (use `.mjs` if your project is not `"type": "module"`). For `eslint.config.ts`, also install [`jiti`](https://github.com/unjs/jiti), which ESLint uses to load TypeScript config files. The presets include types, so a TypeScript config is type-checked too.
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import config from "@webadeva/eslint-config";
|
|
30
|
+
|
|
31
|
+
export default config;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Preset | Import | Includes |
|
|
35
|
+
| --------- | ----------------------------------- | ---------------------------- |
|
|
36
|
+
| Base | `@webadeva/eslint-config` | TypeScript, Prettier, import |
|
|
37
|
+
| React | `@webadeva/eslint-config/react` | Base + React |
|
|
38
|
+
| Next.js | `@webadeva/eslint-config/next` | React + Next.js |
|
|
39
|
+
| Storybook | `@webadeva/eslint-config/storybook` | Next.js + Storybook |
|
|
40
|
+
|
|
41
|
+
`@webadeva/eslint-config/base` is an alias of the base preset. Only the base preset can be loaded with `require()`. The others are ESM-only because `@eslint-react` is.
|
|
42
|
+
|
|
43
|
+
## Customizing
|
|
44
|
+
|
|
45
|
+
Add your own config objects after the preset:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import config from "@webadeva/eslint-config/react";
|
|
49
|
+
|
|
50
|
+
export default [
|
|
51
|
+
...config,
|
|
52
|
+
{
|
|
53
|
+
rules: { "no-console": "off" },
|
|
54
|
+
settings: { webadeva: { maxLines: { warn: 250, error: 300 } } },
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`maxLines` sets the file length limits. By default a file warns above 150 lines of code and errors above 175. See [docs/rules.md](docs/rules.md) for all options.
|
|
60
|
+
|
|
61
|
+
## TypeScript and typed linting
|
|
62
|
+
|
|
63
|
+
Type-aware rules run on every file that typed linting can cover:
|
|
64
|
+
|
|
65
|
+
- **Files in a tsconfig** (the nearest `tsconfig.json`, including project references) get every rule. That includes JS files when `allowJs` is on.
|
|
66
|
+
- **Up to 8 root-level files outside any tsconfig**, such as config files and scripts, are typed with a [built-in fallback tsconfig](tsconfig.default.json). Root-level `.jsx`/`.tsx` files are included only if `@types/react` is installed.
|
|
67
|
+
- **Other files outside any tsconfig** get every rule except the type-aware ones. TS files in this situation are reported by `webadeva/typescript-file-in-project`, so they don't go unchecked unnoticed.
|
|
68
|
+
|
|
69
|
+
## Defaults worth knowing
|
|
70
|
+
|
|
71
|
+
- These paths are ignored: `dist`, `*.config.*`, `**/__tests__/**`, `**/__mocks__/**`.
|
|
72
|
+
- Prettier runs as a lint rule and uses your Prettier config.
|
|
73
|
+
- The React preset detects your React version, including in monorepos.
|
|
74
|
+
- Some rules are opinionated: `no-console` warns, and arrow functions are preferred over `function`.
|
|
75
|
+
|
|
76
|
+
## What's included
|
|
77
|
+
|
|
78
|
+
[`@eslint/js`](https://eslint.org/docs/latest/rules/), [typescript-eslint](https://typescript-eslint.io/) (recommended + stylistic, type-checked), [Prettier](https://github.com/prettier/eslint-plugin-prettier), [import](https://github.com/import-js/eslint-plugin-import), [eslint-comments](https://github.com/eslint-community/eslint-plugin-eslint-comments), [prefer-arrow-functions](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions), [@eslint-react](https://eslint-react.xyz/), [react-hooks](https://react.dev/reference/eslint-plugin-react-hooks), [react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh), [Next.js](https://nextjs.org/docs/app/api-reference/config/eslint), [Storybook](https://github.com/storybookjs/eslint-plugin-storybook), and [this package's own rules](docs/rules.md).
|
|
79
|
+
|
|
80
|
+
## Contributing
|
|
81
|
+
|
|
82
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
23
|
+
let _eslint_community_eslint_plugin_eslint_comments = require("@eslint-community/eslint-plugin-eslint-comments");
|
|
24
|
+
_eslint_community_eslint_plugin_eslint_comments = __toESM(_eslint_community_eslint_plugin_eslint_comments, 1);
|
|
25
|
+
let _eslint_js = require("@eslint/js");
|
|
26
|
+
_eslint_js = __toESM(_eslint_js, 1);
|
|
27
|
+
let eslint_plugin_import = require("eslint-plugin-import");
|
|
28
|
+
eslint_plugin_import = __toESM(eslint_plugin_import, 1);
|
|
29
|
+
let eslint_plugin_prefer_arrow_functions = require("eslint-plugin-prefer-arrow-functions");
|
|
30
|
+
eslint_plugin_prefer_arrow_functions = __toESM(eslint_plugin_prefer_arrow_functions, 1);
|
|
31
|
+
let eslint_plugin_prettier = require("eslint-plugin-prettier");
|
|
32
|
+
eslint_plugin_prettier = __toESM(eslint_plugin_prettier, 1);
|
|
33
|
+
let globals = require("globals");
|
|
34
|
+
globals = __toESM(globals, 1);
|
|
35
|
+
let node_module = require("node:module");
|
|
36
|
+
let node_fs = require("node:fs");
|
|
37
|
+
let node_path = require("node:path");
|
|
38
|
+
//#region src/plugin/rules/max-lines.ts
|
|
39
|
+
const DEFAULTS = {
|
|
40
|
+
enabled: true,
|
|
41
|
+
warn: 150,
|
|
42
|
+
error: 175,
|
|
43
|
+
skipBlankLines: true,
|
|
44
|
+
skipComments: true
|
|
45
|
+
};
|
|
46
|
+
const SETTINGS_ERROR = "@webadeva/eslint-config: `settings.webadeva.maxLines` must be `false` or `{ enabled?, warn?, error?, skipBlankLines?, skipComments? }`";
|
|
47
|
+
const isLimit = (value) => {
|
|
48
|
+
return value === false || typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
49
|
+
};
|
|
50
|
+
const readSettings = (context) => {
|
|
51
|
+
const maxLines = context.settings["webadeva"]?.maxLines;
|
|
52
|
+
if (maxLines === false) return void 0;
|
|
53
|
+
const settings = {
|
|
54
|
+
...DEFAULTS,
|
|
55
|
+
...maxLines
|
|
56
|
+
};
|
|
57
|
+
if (typeof settings.enabled !== "boolean") throw new Error(`${SETTINGS_ERROR}; \`enabled\` must be a boolean.`);
|
|
58
|
+
if (!settings.enabled) return void 0;
|
|
59
|
+
if (!isLimit(settings.warn) || !isLimit(settings.error)) throw new Error(`${SETTINGS_ERROR}; \`warn\` and \`error\` must be non-negative integers or \`false\`.`);
|
|
60
|
+
if (typeof settings.skipBlankLines !== "boolean" || typeof settings.skipComments !== "boolean") throw new Error(`${SETTINGS_ERROR}; \`skipBlankLines\` and \`skipComments\` must be booleans.`);
|
|
61
|
+
if (settings.warn !== false && settings.error !== false && settings.warn >= settings.error) throw new Error(`${SETTINGS_ERROR}; \`warn\` (${settings.warn}) must be lower than \`error\` (${settings.error}).`);
|
|
62
|
+
return settings;
|
|
63
|
+
};
|
|
64
|
+
const addLines = (lines, { start, end }) => {
|
|
65
|
+
for (let { line } = start; line <= end.line; line++) lines.add(line);
|
|
66
|
+
};
|
|
67
|
+
const countLines = (sourceCode, { skipBlankLines, skipComments }) => {
|
|
68
|
+
const lines = /* @__PURE__ */ new Set();
|
|
69
|
+
for (const token of sourceCode.ast.tokens) addLines(lines, token.loc);
|
|
70
|
+
if (!skipComments) {
|
|
71
|
+
for (const comment of sourceCode.getAllComments()) if (comment.loc) addLines(lines, comment.loc);
|
|
72
|
+
}
|
|
73
|
+
if (!skipBlankLines) {
|
|
74
|
+
const lastLine = sourceCode.lines.at(-1) === "" ? sourceCode.lines.length - 1 : sourceCode.lines.length;
|
|
75
|
+
sourceCode.lines.slice(0, lastLine).forEach((text, index) => {
|
|
76
|
+
if (text.trim() === "") lines.add(index + 1);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return lines.size;
|
|
80
|
+
};
|
|
81
|
+
const createMaxLinesRule = (level, description, message) => {
|
|
82
|
+
return {
|
|
83
|
+
meta: {
|
|
84
|
+
type: "suggestion",
|
|
85
|
+
docs: { description },
|
|
86
|
+
messages: { tooLong: message },
|
|
87
|
+
schema: []
|
|
88
|
+
},
|
|
89
|
+
create: (context) => {
|
|
90
|
+
const settings = readSettings(context);
|
|
91
|
+
const max = settings?.[level];
|
|
92
|
+
if (!settings || max === false || max === void 0) return {};
|
|
93
|
+
const upperBound = level === "warn" ? settings.error : false;
|
|
94
|
+
return { "Program:exit": () => {
|
|
95
|
+
const { sourceCode } = context;
|
|
96
|
+
const count = countLines(sourceCode, settings);
|
|
97
|
+
if (count <= max) return;
|
|
98
|
+
if (upperBound !== false && count > upperBound) return;
|
|
99
|
+
const lastLine = sourceCode.lines.length;
|
|
100
|
+
context.report({
|
|
101
|
+
loc: {
|
|
102
|
+
start: {
|
|
103
|
+
line: 1,
|
|
104
|
+
column: 0
|
|
105
|
+
},
|
|
106
|
+
end: {
|
|
107
|
+
line: lastLine,
|
|
108
|
+
column: sourceCode.lines[lastLine - 1]?.length ?? 0
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
messageId: "tooLong",
|
|
112
|
+
data: {
|
|
113
|
+
count: String(count),
|
|
114
|
+
max: String(max)
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
} };
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/plugin/index.ts
|
|
123
|
+
const plugin = {
|
|
124
|
+
meta: { name: "@webadeva/eslint-config" },
|
|
125
|
+
rules: {
|
|
126
|
+
"max-lines": createMaxLinesRule("error", "Enforce a hard limit on lines of code per file.", "File has {{count}} lines of code, over the limit of {{max}}. Split it into smaller modules."),
|
|
127
|
+
"max-lines-warning": createMaxLinesRule("warn", "Warn when a file grows past the soft lines-of-code limit.", "File has {{count}} lines of code, over the soft limit of {{max}}. Consider splitting it into smaller modules."),
|
|
128
|
+
"typescript-file-in-project": {
|
|
129
|
+
meta: {
|
|
130
|
+
type: "problem",
|
|
131
|
+
docs: { description: "Report TypeScript files that no tsconfig includes, since type-aware rules skip them." },
|
|
132
|
+
messages: { notInProject: "This file is not included in any tsconfig, so it is not type-checked and type-aware lint rules are skipped. Add it to a tsconfig's `include`." },
|
|
133
|
+
schema: []
|
|
134
|
+
},
|
|
135
|
+
create: (context) => {
|
|
136
|
+
return { Program: () => {
|
|
137
|
+
context.report({
|
|
138
|
+
loc: {
|
|
139
|
+
line: 1,
|
|
140
|
+
column: 0
|
|
141
|
+
},
|
|
142
|
+
messageId: "notInProject"
|
|
143
|
+
});
|
|
144
|
+
} };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/utils/typescript.ts
|
|
151
|
+
const cwd = process.cwd();
|
|
152
|
+
const projectRequire = (0, node_module.createRequire)((0, node_path.join)(cwd, "package.json"));
|
|
153
|
+
const packageRequire = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
154
|
+
const loadTypeScript = () => {
|
|
155
|
+
let version;
|
|
156
|
+
try {
|
|
157
|
+
({version} = projectRequire("typescript/package.json"));
|
|
158
|
+
} catch {
|
|
159
|
+
throw new Error("@webadeva/eslint-config: TypeScript is not installed in this project. This config is for TypeScript projects; install it with `pnpm add -D typescript@6` (or the npm/yarn equivalent).");
|
|
160
|
+
}
|
|
161
|
+
if (Number.parseInt(version, 10) >= 7) throw new Error(`@webadeva/eslint-config: TypeScript ${version} is not supported. typescript-eslint needs the TypeScript compiler API, which TypeScript 7 no longer ships. Install typescript@6.`);
|
|
162
|
+
return projectRequire("typescript");
|
|
163
|
+
};
|
|
164
|
+
let typescript;
|
|
165
|
+
/**
|
|
166
|
+
* Loads the project's TypeScript, throwing a readable error when it is
|
|
167
|
+
* missing or unsupported. Call it before importing anything that loads
|
|
168
|
+
* TypeScript itself (typescript-eslint, @eslint-react), which would otherwise
|
|
169
|
+
* fail with a bare "Cannot find module 'typescript'".
|
|
170
|
+
*/
|
|
171
|
+
const ensureTypeScript = () => {
|
|
172
|
+
typescript ??= loadTypeScript();
|
|
173
|
+
return typescript;
|
|
174
|
+
};
|
|
175
|
+
const LINTABLE_FILE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
176
|
+
const isLintableFile = (filePath) => {
|
|
177
|
+
return LINTABLE_FILE.test(filePath);
|
|
178
|
+
};
|
|
179
|
+
const nearestConfigCache = /* @__PURE__ */ new Map();
|
|
180
|
+
const findNearestConfig = (dir) => {
|
|
181
|
+
if (nearestConfigCache.has(dir)) return nearestConfigCache.get(dir);
|
|
182
|
+
const candidate = (0, node_path.join)(dir, "tsconfig.json");
|
|
183
|
+
const parent = (0, node_path.dirname)(dir);
|
|
184
|
+
const found = (0, node_fs.existsSync)(candidate) ? candidate : parent === dir ? void 0 : findNearestConfig(parent);
|
|
185
|
+
nearestConfigCache.set(dir, found);
|
|
186
|
+
return found;
|
|
187
|
+
};
|
|
188
|
+
const projectFilesCache = /* @__PURE__ */ new Map();
|
|
189
|
+
const getProjectFiles = (configPath, seen = /* @__PURE__ */ new Set()) => {
|
|
190
|
+
const cached = projectFilesCache.get(configPath);
|
|
191
|
+
if (cached) return cached;
|
|
192
|
+
const files = /* @__PURE__ */ new Set();
|
|
193
|
+
if (seen.has(configPath)) return files;
|
|
194
|
+
seen.add(configPath);
|
|
195
|
+
const ts = ensureTypeScript();
|
|
196
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, void 0, {
|
|
197
|
+
...ts.sys,
|
|
198
|
+
onUnRecoverableConfigFileDiagnostic: () => {}
|
|
199
|
+
});
|
|
200
|
+
for (const fileName of parsed?.fileNames ?? []) files.add((0, node_path.resolve)(fileName));
|
|
201
|
+
for (const reference of parsed?.projectReferences ?? []) {
|
|
202
|
+
const referencePath = ts.resolveProjectReferencePath(reference);
|
|
203
|
+
for (const fileName of getProjectFiles(referencePath, seen)) files.add(fileName);
|
|
204
|
+
}
|
|
205
|
+
projectFilesCache.set(configPath, files);
|
|
206
|
+
return files;
|
|
207
|
+
};
|
|
208
|
+
const isInProject = (filePath) => {
|
|
209
|
+
const configPath = findNearestConfig((0, node_path.dirname)(filePath));
|
|
210
|
+
return configPath !== void 0 && getProjectFiles(configPath).has((0, node_path.resolve)(filePath));
|
|
211
|
+
};
|
|
212
|
+
/** The tsconfig typescript-eslint uses for files outside every project. */
|
|
213
|
+
const defaultProjectConfig = packageRequire.resolve("@webadeva/eslint-config/tsconfig.default.json");
|
|
214
|
+
const canResolve = (id) => {
|
|
215
|
+
try {
|
|
216
|
+
projectRequire.resolve(id);
|
|
217
|
+
return true;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const DEFAULT_PROJECT_FILE_LIMIT = 8;
|
|
223
|
+
const GLOB_SPECIAL_CHARS = /[*?[\]{}()!+@\\]/;
|
|
224
|
+
const DEFAULT_PROJECT_FILE = canResolve("@types/react/package.json") ? /\.[cm]?[jt]sx?$/ : /\.[cm]?[jt]s$/;
|
|
225
|
+
/**
|
|
226
|
+
* Root-level files that no tsconfig includes, linted with type information
|
|
227
|
+
* through typescript-eslint's default project (`allowDefaultProject`).
|
|
228
|
+
*/
|
|
229
|
+
const defaultProjectFiles = (0, node_fs.readdirSync)(cwd, { withFileTypes: true }).filter((entry) => {
|
|
230
|
+
return entry.isFile() && DEFAULT_PROJECT_FILE.test(entry.name) && !GLOB_SPECIAL_CHARS.test(entry.name) && !isInProject((0, node_path.join)(cwd, entry.name));
|
|
231
|
+
}).map((entry) => {
|
|
232
|
+
return entry.name;
|
|
233
|
+
}).sort().slice(0, DEFAULT_PROJECT_FILE_LIMIT);
|
|
234
|
+
const defaultProjectPaths = new Set(defaultProjectFiles.map((name) => {
|
|
235
|
+
return (0, node_path.join)(cwd, name);
|
|
236
|
+
}));
|
|
237
|
+
const TYPESCRIPT_FILE = /\.[cm]?tsx?$/;
|
|
238
|
+
const isTypeScriptFile = (filePath) => {
|
|
239
|
+
return TYPESCRIPT_FILE.test(filePath);
|
|
240
|
+
};
|
|
241
|
+
/** Whether typescript-eslint can provide type information for this file. */
|
|
242
|
+
const hasTypeInformation = (filePath) => {
|
|
243
|
+
return defaultProjectPaths.has((0, node_path.resolve)(filePath)) || isInProject(filePath);
|
|
244
|
+
};
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/configs/base.ts
|
|
247
|
+
ensureTypeScript();
|
|
248
|
+
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
249
|
+
const tseslint$1 = require$1("typescript-eslint");
|
|
250
|
+
const tsPlugin = require$1("@typescript-eslint/eslint-plugin");
|
|
251
|
+
const baseConfigs = [
|
|
252
|
+
{ ignores: [
|
|
253
|
+
"**/__tests__/**/**/*",
|
|
254
|
+
"**/__mocks__/**/**/*",
|
|
255
|
+
"dist",
|
|
256
|
+
"*.config.*",
|
|
257
|
+
"node_modules/**/*.*",
|
|
258
|
+
"dist/**/*.*",
|
|
259
|
+
"!.storybook",
|
|
260
|
+
"!**/.storybook/**"
|
|
261
|
+
] },
|
|
262
|
+
{
|
|
263
|
+
name: "webadeva/plugin",
|
|
264
|
+
plugins: { webadeva: plugin }
|
|
265
|
+
},
|
|
266
|
+
_eslint_js.default.configs.recommended,
|
|
267
|
+
...tseslint$1.configs.recommendedTypeChecked,
|
|
268
|
+
{
|
|
269
|
+
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
|
|
270
|
+
languageOptions: {
|
|
271
|
+
globals: {
|
|
272
|
+
...globals.default.browser,
|
|
273
|
+
...globals.default.node
|
|
274
|
+
},
|
|
275
|
+
parser: tseslint$1.parser,
|
|
276
|
+
parserOptions: {
|
|
277
|
+
ecmaVersion: 2020,
|
|
278
|
+
sourceType: "module",
|
|
279
|
+
projectService: {
|
|
280
|
+
allowDefaultProject: defaultProjectFiles,
|
|
281
|
+
defaultProject: defaultProjectConfig
|
|
282
|
+
},
|
|
283
|
+
tsconfigRootDir: process.cwd()
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
plugins: {
|
|
287
|
+
prettier: eslint_plugin_prettier.default,
|
|
288
|
+
"prefer-arrow-functions": eslint_plugin_prefer_arrow_functions.default,
|
|
289
|
+
"@typescript-eslint": tseslint$1.plugin,
|
|
290
|
+
"eslint-comments": _eslint_community_eslint_plugin_eslint_comments.default,
|
|
291
|
+
import: eslint_plugin_import.default
|
|
292
|
+
},
|
|
293
|
+
rules: {
|
|
294
|
+
..._eslint_js.default.configs.recommended.rules,
|
|
295
|
+
...tsPlugin.configs["recommended-type-checked"]?.rules,
|
|
296
|
+
...tsPlugin.configs["stylistic-type-checked"]?.rules,
|
|
297
|
+
...(eslint_plugin_prettier.default.configs?.["recommended"])?.rules,
|
|
298
|
+
camelcase: "error",
|
|
299
|
+
"no-undef": "off",
|
|
300
|
+
"no-unused-vars": "off",
|
|
301
|
+
"no-console": "warn",
|
|
302
|
+
"consistent-return": "off",
|
|
303
|
+
"object-shorthand": "off",
|
|
304
|
+
"no-process-exit": "off",
|
|
305
|
+
"no-underscore-dangle": "off",
|
|
306
|
+
"class-methods-use-this": "off",
|
|
307
|
+
"prefer-destructuring": ["warn", {
|
|
308
|
+
object: true,
|
|
309
|
+
array: false
|
|
310
|
+
}],
|
|
311
|
+
"@typescript-eslint/no-unused-vars": ["error", {
|
|
312
|
+
argsIgnorePattern: "^_$",
|
|
313
|
+
varsIgnorePattern: "^_$"
|
|
314
|
+
}],
|
|
315
|
+
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
316
|
+
"@typescript-eslint/no-unsafe-member-access": "off",
|
|
317
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
318
|
+
"@typescript-eslint/prefer-for-of": "off",
|
|
319
|
+
"@typescript-eslint/no-unsafe-argument": "warn",
|
|
320
|
+
"@typescript-eslint/no-unsafe-call": "warn",
|
|
321
|
+
"@typescript-eslint/no-unsafe-return": "warn",
|
|
322
|
+
"@typescript-eslint/prefer-nullish-coalescing": "warn",
|
|
323
|
+
"@typescript-eslint/prefer-optional-chain": "warn",
|
|
324
|
+
"@typescript-eslint/consistent-indexed-object-style": "off",
|
|
325
|
+
"prefer-arrow-callback": "error",
|
|
326
|
+
"eslint-comments/require-description": ["error"],
|
|
327
|
+
"prefer-arrow-functions/prefer-arrow-functions": ["warn", {
|
|
328
|
+
allowNamedFunctions: false,
|
|
329
|
+
classPropertiesAllowed: false,
|
|
330
|
+
disallowPrototype: false,
|
|
331
|
+
returnStyle: "explicit",
|
|
332
|
+
singleReturnOnly: false
|
|
333
|
+
}]
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "webadeva/max-lines",
|
|
338
|
+
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
339
|
+
rules: {
|
|
340
|
+
"webadeva/max-lines-warning": "warn",
|
|
341
|
+
"webadeva/max-lines": "error"
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
];
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/utils/files-matching.ts
|
|
347
|
+
/**
|
|
348
|
+
* ESLint accepts `(filePath) => boolean` matchers in `files`, but its types
|
|
349
|
+
* only allow glob strings.
|
|
350
|
+
*/
|
|
351
|
+
const filesMatching = (matcher) => {
|
|
352
|
+
return [matcher];
|
|
353
|
+
};
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/configs/untyped.ts
|
|
356
|
+
ensureTypeScript();
|
|
357
|
+
const tseslint = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("typescript-eslint");
|
|
358
|
+
/**
|
|
359
|
+
* Turns off type-aware rules for files no TypeScript project includes (e.g.
|
|
360
|
+
* JS files without `allowJs`), which would otherwise fail to parse. Must come
|
|
361
|
+
* after every config that enables type-aware rules.
|
|
362
|
+
*/
|
|
363
|
+
const untypedFilesConfigs = (disableTypeChecked = []) => {
|
|
364
|
+
const files = filesMatching((filePath) => {
|
|
365
|
+
return isLintableFile(filePath) && !hasTypeInformation(filePath);
|
|
366
|
+
});
|
|
367
|
+
return [...[tseslint.configs.disableTypeChecked, ...disableTypeChecked].map((config) => {
|
|
368
|
+
return {
|
|
369
|
+
...config,
|
|
370
|
+
files
|
|
371
|
+
};
|
|
372
|
+
}), {
|
|
373
|
+
name: "webadeva/typescript-file-not-in-project",
|
|
374
|
+
files: filesMatching((filePath) => {
|
|
375
|
+
return isTypeScriptFile(filePath) && !hasTypeInformation(filePath);
|
|
376
|
+
}),
|
|
377
|
+
rules: { "webadeva/typescript-file-in-project": "error" }
|
|
378
|
+
}];
|
|
379
|
+
};
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/index.ts
|
|
382
|
+
const config = [...baseConfigs, ...untypedFilesConfigs()];
|
|
383
|
+
//#endregion
|
|
384
|
+
module.exports = config;
|
package/dist/index.d.cts
ADDED
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import nextPlugin from "@next/eslint-plugin-next";
|
|
2
|
+
//#region src/configs/next.ts
|
|
3
|
+
const nextConfigs = [{
|
|
4
|
+
plugins: { "@next/next": nextPlugin },
|
|
5
|
+
rules: { "@next/next/no-img-element": "warn" },
|
|
6
|
+
settings: { next: { rootDir: "." } }
|
|
7
|
+
}];
|
|
8
|
+
//#endregion
|
|
9
|
+
export { nextConfigs as t };
|
package/dist/next.d.mts
ADDED
package/dist/next.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { n as baseConfigs, t as untypedFilesConfigs } from "./untyped-Wrh4hLMj.mjs";
|
|
2
|
+
import { n as reactDisableTypeChecked, t as reactConfigs } from "./react-C-VV0l5P.mjs";
|
|
3
|
+
import { t as nextConfigs } from "./next-CWjuCwpN.mjs";
|
|
4
|
+
//#region src/next.ts
|
|
5
|
+
const config = [
|
|
6
|
+
...baseConfigs,
|
|
7
|
+
...reactConfigs,
|
|
8
|
+
...nextConfigs,
|
|
9
|
+
...untypedFilesConfigs(reactDisableTypeChecked)
|
|
10
|
+
];
|
|
11
|
+
//#endregion
|
|
12
|
+
export { config as default };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { r as ensureTypeScript } from "./untyped-Wrh4hLMj.mjs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import pluginReactHooks from "eslint-plugin-react-hooks";
|
|
6
|
+
import pluginReactRefresh from "eslint-plugin-react-refresh";
|
|
7
|
+
//#region src/utils/detect-react-version.ts
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const FALLBACK_VERSION = "19.0";
|
|
10
|
+
const readReactVersion = (fromDir) => {
|
|
11
|
+
const pkgPath = require.resolve("react/package.json", { paths: [fromDir] });
|
|
12
|
+
return String(require(pkgPath).version);
|
|
13
|
+
};
|
|
14
|
+
const detectReactVersion = () => {
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
try {
|
|
17
|
+
return readReactVersion(cwd);
|
|
18
|
+
} catch {}
|
|
19
|
+
try {
|
|
20
|
+
const ws = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8")).workspaces ?? [];
|
|
21
|
+
const patterns = Array.isArray(ws) ? ws : ws.packages ?? [];
|
|
22
|
+
for (const pattern of patterns) {
|
|
23
|
+
const baseDir = join(cwd, String(pattern).replace(/\/\*.*$/, ""));
|
|
24
|
+
try {
|
|
25
|
+
const entries = readdirSync(baseDir, { withFileTypes: true });
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (!entry.isDirectory()) continue;
|
|
28
|
+
try {
|
|
29
|
+
return readReactVersion(join(baseDir, entry.name));
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
return FALLBACK_VERSION;
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/configs/react.ts
|
|
39
|
+
ensureTypeScript();
|
|
40
|
+
const { default: eslintReact } = await import("@eslint-react/eslint-plugin");
|
|
41
|
+
const reactConfigs = [
|
|
42
|
+
eslintReact.configs["recommended-typescript"],
|
|
43
|
+
pluginReactHooks.configs.flat["recommended-latest"],
|
|
44
|
+
pluginReactRefresh.configs.recommended,
|
|
45
|
+
{
|
|
46
|
+
settings: { react: { version: detectReactVersion() } },
|
|
47
|
+
rules: {
|
|
48
|
+
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
|
49
|
+
"react-hooks/exhaustive-deps": "warn",
|
|
50
|
+
"react-hooks/rules-of-hooks": "error",
|
|
51
|
+
"react-hooks/set-state-in-effect": "off",
|
|
52
|
+
"@eslint-react/set-state-in-effect": "off",
|
|
53
|
+
"@eslint-react/dom-no-unknown-property": ["error", { ignore: ["css"] }]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
const reactDisableTypeChecked = [eslintReact.configs["disable-type-checked"]];
|
|
58
|
+
//#endregion
|
|
59
|
+
export { reactDisableTypeChecked as n, reactConfigs as t };
|
package/dist/react.d.mts
ADDED
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { n as baseConfigs, t as untypedFilesConfigs } from "./untyped-Wrh4hLMj.mjs";
|
|
2
|
+
import { n as reactDisableTypeChecked, t as reactConfigs } from "./react-C-VV0l5P.mjs";
|
|
3
|
+
//#region src/react.ts
|
|
4
|
+
const config = [
|
|
5
|
+
...baseConfigs,
|
|
6
|
+
...reactConfigs,
|
|
7
|
+
...untypedFilesConfigs(reactDisableTypeChecked)
|
|
8
|
+
];
|
|
9
|
+
//#endregion
|
|
10
|
+
export { config as default };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { n as baseConfigs, t as untypedFilesConfigs } from "./untyped-Wrh4hLMj.mjs";
|
|
2
|
+
import { n as reactDisableTypeChecked, t as reactConfigs } from "./react-C-VV0l5P.mjs";
|
|
3
|
+
import { t as nextConfigs } from "./next-CWjuCwpN.mjs";
|
|
4
|
+
import storybook from "eslint-plugin-storybook";
|
|
5
|
+
//#region src/configs/storybook.ts
|
|
6
|
+
const storybookConfigs = [...storybook.configs["flat/recommended"], { rules: {
|
|
7
|
+
"node/no-unsupported-features/es-syntax": "off",
|
|
8
|
+
"node/no-missing-import": "off",
|
|
9
|
+
"react/jsx-props-no-spreading": "off",
|
|
10
|
+
"react/react-in-jsx-scope": "off"
|
|
11
|
+
} }];
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/storybook.ts
|
|
14
|
+
const config = [
|
|
15
|
+
...baseConfigs,
|
|
16
|
+
...reactConfigs,
|
|
17
|
+
...nextConfigs,
|
|
18
|
+
...storybookConfigs,
|
|
19
|
+
...untypedFilesConfigs(reactDisableTypeChecked)
|
|
20
|
+
];
|
|
21
|
+
//#endregion
|
|
22
|
+
export { config as default };
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import eslintComments from "@eslint-community/eslint-plugin-eslint-comments";
|
|
3
|
+
import pluginJs from "@eslint/js";
|
|
4
|
+
import importPlugin from "eslint-plugin-import";
|
|
5
|
+
import preferArrowFunctions from "eslint-plugin-prefer-arrow-functions";
|
|
6
|
+
import prettier from "eslint-plugin-prettier";
|
|
7
|
+
import globals from "globals";
|
|
8
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
//#region src/plugin/rules/max-lines.ts
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
warn: 150,
|
|
14
|
+
error: 175,
|
|
15
|
+
skipBlankLines: true,
|
|
16
|
+
skipComments: true
|
|
17
|
+
};
|
|
18
|
+
const SETTINGS_ERROR = "@webadeva/eslint-config: `settings.webadeva.maxLines` must be `false` or `{ enabled?, warn?, error?, skipBlankLines?, skipComments? }`";
|
|
19
|
+
const isLimit = (value) => {
|
|
20
|
+
return value === false || typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
21
|
+
};
|
|
22
|
+
const readSettings = (context) => {
|
|
23
|
+
const maxLines = context.settings["webadeva"]?.maxLines;
|
|
24
|
+
if (maxLines === false) return void 0;
|
|
25
|
+
const settings = {
|
|
26
|
+
...DEFAULTS,
|
|
27
|
+
...maxLines
|
|
28
|
+
};
|
|
29
|
+
if (typeof settings.enabled !== "boolean") throw new Error(`${SETTINGS_ERROR}; \`enabled\` must be a boolean.`);
|
|
30
|
+
if (!settings.enabled) return void 0;
|
|
31
|
+
if (!isLimit(settings.warn) || !isLimit(settings.error)) throw new Error(`${SETTINGS_ERROR}; \`warn\` and \`error\` must be non-negative integers or \`false\`.`);
|
|
32
|
+
if (typeof settings.skipBlankLines !== "boolean" || typeof settings.skipComments !== "boolean") throw new Error(`${SETTINGS_ERROR}; \`skipBlankLines\` and \`skipComments\` must be booleans.`);
|
|
33
|
+
if (settings.warn !== false && settings.error !== false && settings.warn >= settings.error) throw new Error(`${SETTINGS_ERROR}; \`warn\` (${settings.warn}) must be lower than \`error\` (${settings.error}).`);
|
|
34
|
+
return settings;
|
|
35
|
+
};
|
|
36
|
+
const addLines = (lines, { start, end }) => {
|
|
37
|
+
for (let { line } = start; line <= end.line; line++) lines.add(line);
|
|
38
|
+
};
|
|
39
|
+
const countLines = (sourceCode, { skipBlankLines, skipComments }) => {
|
|
40
|
+
const lines = /* @__PURE__ */ new Set();
|
|
41
|
+
for (const token of sourceCode.ast.tokens) addLines(lines, token.loc);
|
|
42
|
+
if (!skipComments) {
|
|
43
|
+
for (const comment of sourceCode.getAllComments()) if (comment.loc) addLines(lines, comment.loc);
|
|
44
|
+
}
|
|
45
|
+
if (!skipBlankLines) {
|
|
46
|
+
const lastLine = sourceCode.lines.at(-1) === "" ? sourceCode.lines.length - 1 : sourceCode.lines.length;
|
|
47
|
+
sourceCode.lines.slice(0, lastLine).forEach((text, index) => {
|
|
48
|
+
if (text.trim() === "") lines.add(index + 1);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return lines.size;
|
|
52
|
+
};
|
|
53
|
+
const createMaxLinesRule = (level, description, message) => {
|
|
54
|
+
return {
|
|
55
|
+
meta: {
|
|
56
|
+
type: "suggestion",
|
|
57
|
+
docs: { description },
|
|
58
|
+
messages: { tooLong: message },
|
|
59
|
+
schema: []
|
|
60
|
+
},
|
|
61
|
+
create: (context) => {
|
|
62
|
+
const settings = readSettings(context);
|
|
63
|
+
const max = settings?.[level];
|
|
64
|
+
if (!settings || max === false || max === void 0) return {};
|
|
65
|
+
const upperBound = level === "warn" ? settings.error : false;
|
|
66
|
+
return { "Program:exit": () => {
|
|
67
|
+
const { sourceCode } = context;
|
|
68
|
+
const count = countLines(sourceCode, settings);
|
|
69
|
+
if (count <= max) return;
|
|
70
|
+
if (upperBound !== false && count > upperBound) return;
|
|
71
|
+
const lastLine = sourceCode.lines.length;
|
|
72
|
+
context.report({
|
|
73
|
+
loc: {
|
|
74
|
+
start: {
|
|
75
|
+
line: 1,
|
|
76
|
+
column: 0
|
|
77
|
+
},
|
|
78
|
+
end: {
|
|
79
|
+
line: lastLine,
|
|
80
|
+
column: sourceCode.lines[lastLine - 1]?.length ?? 0
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
messageId: "tooLong",
|
|
84
|
+
data: {
|
|
85
|
+
count: String(count),
|
|
86
|
+
max: String(max)
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
} };
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/plugin/index.ts
|
|
95
|
+
const plugin = {
|
|
96
|
+
meta: { name: "@webadeva/eslint-config" },
|
|
97
|
+
rules: {
|
|
98
|
+
"max-lines": createMaxLinesRule("error", "Enforce a hard limit on lines of code per file.", "File has {{count}} lines of code, over the limit of {{max}}. Split it into smaller modules."),
|
|
99
|
+
"max-lines-warning": createMaxLinesRule("warn", "Warn when a file grows past the soft lines-of-code limit.", "File has {{count}} lines of code, over the soft limit of {{max}}. Consider splitting it into smaller modules."),
|
|
100
|
+
"typescript-file-in-project": {
|
|
101
|
+
meta: {
|
|
102
|
+
type: "problem",
|
|
103
|
+
docs: { description: "Report TypeScript files that no tsconfig includes, since type-aware rules skip them." },
|
|
104
|
+
messages: { notInProject: "This file is not included in any tsconfig, so it is not type-checked and type-aware lint rules are skipped. Add it to a tsconfig's `include`." },
|
|
105
|
+
schema: []
|
|
106
|
+
},
|
|
107
|
+
create: (context) => {
|
|
108
|
+
return { Program: () => {
|
|
109
|
+
context.report({
|
|
110
|
+
loc: {
|
|
111
|
+
line: 1,
|
|
112
|
+
column: 0
|
|
113
|
+
},
|
|
114
|
+
messageId: "notInProject"
|
|
115
|
+
});
|
|
116
|
+
} };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/utils/typescript.ts
|
|
123
|
+
const cwd = process.cwd();
|
|
124
|
+
const projectRequire = createRequire(join(cwd, "package.json"));
|
|
125
|
+
const packageRequire = createRequire(import.meta.url);
|
|
126
|
+
const loadTypeScript = () => {
|
|
127
|
+
let version;
|
|
128
|
+
try {
|
|
129
|
+
({version} = projectRequire("typescript/package.json"));
|
|
130
|
+
} catch {
|
|
131
|
+
throw new Error("@webadeva/eslint-config: TypeScript is not installed in this project. This config is for TypeScript projects; install it with `pnpm add -D typescript@6` (or the npm/yarn equivalent).");
|
|
132
|
+
}
|
|
133
|
+
if (Number.parseInt(version, 10) >= 7) throw new Error(`@webadeva/eslint-config: TypeScript ${version} is not supported. typescript-eslint needs the TypeScript compiler API, which TypeScript 7 no longer ships. Install typescript@6.`);
|
|
134
|
+
return projectRequire("typescript");
|
|
135
|
+
};
|
|
136
|
+
let typescript;
|
|
137
|
+
/**
|
|
138
|
+
* Loads the project's TypeScript, throwing a readable error when it is
|
|
139
|
+
* missing or unsupported. Call it before importing anything that loads
|
|
140
|
+
* TypeScript itself (typescript-eslint, @eslint-react), which would otherwise
|
|
141
|
+
* fail with a bare "Cannot find module 'typescript'".
|
|
142
|
+
*/
|
|
143
|
+
const ensureTypeScript = () => {
|
|
144
|
+
typescript ??= loadTypeScript();
|
|
145
|
+
return typescript;
|
|
146
|
+
};
|
|
147
|
+
const LINTABLE_FILE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
148
|
+
const isLintableFile = (filePath) => {
|
|
149
|
+
return LINTABLE_FILE.test(filePath);
|
|
150
|
+
};
|
|
151
|
+
const nearestConfigCache = /* @__PURE__ */ new Map();
|
|
152
|
+
const findNearestConfig = (dir) => {
|
|
153
|
+
if (nearestConfigCache.has(dir)) return nearestConfigCache.get(dir);
|
|
154
|
+
const candidate = join(dir, "tsconfig.json");
|
|
155
|
+
const parent = dirname(dir);
|
|
156
|
+
const found = existsSync(candidate) ? candidate : parent === dir ? void 0 : findNearestConfig(parent);
|
|
157
|
+
nearestConfigCache.set(dir, found);
|
|
158
|
+
return found;
|
|
159
|
+
};
|
|
160
|
+
const projectFilesCache = /* @__PURE__ */ new Map();
|
|
161
|
+
const getProjectFiles = (configPath, seen = /* @__PURE__ */ new Set()) => {
|
|
162
|
+
const cached = projectFilesCache.get(configPath);
|
|
163
|
+
if (cached) return cached;
|
|
164
|
+
const files = /* @__PURE__ */ new Set();
|
|
165
|
+
if (seen.has(configPath)) return files;
|
|
166
|
+
seen.add(configPath);
|
|
167
|
+
const ts = ensureTypeScript();
|
|
168
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, void 0, {
|
|
169
|
+
...ts.sys,
|
|
170
|
+
onUnRecoverableConfigFileDiagnostic: () => {}
|
|
171
|
+
});
|
|
172
|
+
for (const fileName of parsed?.fileNames ?? []) files.add(resolve(fileName));
|
|
173
|
+
for (const reference of parsed?.projectReferences ?? []) {
|
|
174
|
+
const referencePath = ts.resolveProjectReferencePath(reference);
|
|
175
|
+
for (const fileName of getProjectFiles(referencePath, seen)) files.add(fileName);
|
|
176
|
+
}
|
|
177
|
+
projectFilesCache.set(configPath, files);
|
|
178
|
+
return files;
|
|
179
|
+
};
|
|
180
|
+
const isInProject = (filePath) => {
|
|
181
|
+
const configPath = findNearestConfig(dirname(filePath));
|
|
182
|
+
return configPath !== void 0 && getProjectFiles(configPath).has(resolve(filePath));
|
|
183
|
+
};
|
|
184
|
+
/** The tsconfig typescript-eslint uses for files outside every project. */
|
|
185
|
+
const defaultProjectConfig = packageRequire.resolve("@webadeva/eslint-config/tsconfig.default.json");
|
|
186
|
+
const canResolve = (id) => {
|
|
187
|
+
try {
|
|
188
|
+
projectRequire.resolve(id);
|
|
189
|
+
return true;
|
|
190
|
+
} catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const DEFAULT_PROJECT_FILE_LIMIT = 8;
|
|
195
|
+
const GLOB_SPECIAL_CHARS = /[*?[\]{}()!+@\\]/;
|
|
196
|
+
const DEFAULT_PROJECT_FILE = canResolve("@types/react/package.json") ? /\.[cm]?[jt]sx?$/ : /\.[cm]?[jt]s$/;
|
|
197
|
+
/**
|
|
198
|
+
* Root-level files that no tsconfig includes, linted with type information
|
|
199
|
+
* through typescript-eslint's default project (`allowDefaultProject`).
|
|
200
|
+
*/
|
|
201
|
+
const defaultProjectFiles = readdirSync(cwd, { withFileTypes: true }).filter((entry) => {
|
|
202
|
+
return entry.isFile() && DEFAULT_PROJECT_FILE.test(entry.name) && !GLOB_SPECIAL_CHARS.test(entry.name) && !isInProject(join(cwd, entry.name));
|
|
203
|
+
}).map((entry) => {
|
|
204
|
+
return entry.name;
|
|
205
|
+
}).sort().slice(0, DEFAULT_PROJECT_FILE_LIMIT);
|
|
206
|
+
const defaultProjectPaths = new Set(defaultProjectFiles.map((name) => {
|
|
207
|
+
return join(cwd, name);
|
|
208
|
+
}));
|
|
209
|
+
const TYPESCRIPT_FILE = /\.[cm]?tsx?$/;
|
|
210
|
+
const isTypeScriptFile = (filePath) => {
|
|
211
|
+
return TYPESCRIPT_FILE.test(filePath);
|
|
212
|
+
};
|
|
213
|
+
/** Whether typescript-eslint can provide type information for this file. */
|
|
214
|
+
const hasTypeInformation = (filePath) => {
|
|
215
|
+
return defaultProjectPaths.has(resolve(filePath)) || isInProject(filePath);
|
|
216
|
+
};
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/configs/base.ts
|
|
219
|
+
ensureTypeScript();
|
|
220
|
+
const require = createRequire(import.meta.url);
|
|
221
|
+
const tseslint$1 = require("typescript-eslint");
|
|
222
|
+
const tsPlugin = require("@typescript-eslint/eslint-plugin");
|
|
223
|
+
const baseConfigs = [
|
|
224
|
+
{ ignores: [
|
|
225
|
+
"**/__tests__/**/**/*",
|
|
226
|
+
"**/__mocks__/**/**/*",
|
|
227
|
+
"dist",
|
|
228
|
+
"*.config.*",
|
|
229
|
+
"node_modules/**/*.*",
|
|
230
|
+
"dist/**/*.*",
|
|
231
|
+
"!.storybook",
|
|
232
|
+
"!**/.storybook/**"
|
|
233
|
+
] },
|
|
234
|
+
{
|
|
235
|
+
name: "webadeva/plugin",
|
|
236
|
+
plugins: { webadeva: plugin }
|
|
237
|
+
},
|
|
238
|
+
pluginJs.configs.recommended,
|
|
239
|
+
...tseslint$1.configs.recommendedTypeChecked,
|
|
240
|
+
{
|
|
241
|
+
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
|
|
242
|
+
languageOptions: {
|
|
243
|
+
globals: {
|
|
244
|
+
...globals.browser,
|
|
245
|
+
...globals.node
|
|
246
|
+
},
|
|
247
|
+
parser: tseslint$1.parser,
|
|
248
|
+
parserOptions: {
|
|
249
|
+
ecmaVersion: 2020,
|
|
250
|
+
sourceType: "module",
|
|
251
|
+
projectService: {
|
|
252
|
+
allowDefaultProject: defaultProjectFiles,
|
|
253
|
+
defaultProject: defaultProjectConfig
|
|
254
|
+
},
|
|
255
|
+
tsconfigRootDir: process.cwd()
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
plugins: {
|
|
259
|
+
prettier,
|
|
260
|
+
"prefer-arrow-functions": preferArrowFunctions,
|
|
261
|
+
"@typescript-eslint": tseslint$1.plugin,
|
|
262
|
+
"eslint-comments": eslintComments,
|
|
263
|
+
import: importPlugin
|
|
264
|
+
},
|
|
265
|
+
rules: {
|
|
266
|
+
...pluginJs.configs.recommended.rules,
|
|
267
|
+
...tsPlugin.configs["recommended-type-checked"]?.rules,
|
|
268
|
+
...tsPlugin.configs["stylistic-type-checked"]?.rules,
|
|
269
|
+
...(prettier.configs?.["recommended"])?.rules,
|
|
270
|
+
camelcase: "error",
|
|
271
|
+
"no-undef": "off",
|
|
272
|
+
"no-unused-vars": "off",
|
|
273
|
+
"no-console": "warn",
|
|
274
|
+
"consistent-return": "off",
|
|
275
|
+
"object-shorthand": "off",
|
|
276
|
+
"no-process-exit": "off",
|
|
277
|
+
"no-underscore-dangle": "off",
|
|
278
|
+
"class-methods-use-this": "off",
|
|
279
|
+
"prefer-destructuring": ["warn", {
|
|
280
|
+
object: true,
|
|
281
|
+
array: false
|
|
282
|
+
}],
|
|
283
|
+
"@typescript-eslint/no-unused-vars": ["error", {
|
|
284
|
+
argsIgnorePattern: "^_$",
|
|
285
|
+
varsIgnorePattern: "^_$"
|
|
286
|
+
}],
|
|
287
|
+
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
288
|
+
"@typescript-eslint/no-unsafe-member-access": "off",
|
|
289
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
290
|
+
"@typescript-eslint/prefer-for-of": "off",
|
|
291
|
+
"@typescript-eslint/no-unsafe-argument": "warn",
|
|
292
|
+
"@typescript-eslint/no-unsafe-call": "warn",
|
|
293
|
+
"@typescript-eslint/no-unsafe-return": "warn",
|
|
294
|
+
"@typescript-eslint/prefer-nullish-coalescing": "warn",
|
|
295
|
+
"@typescript-eslint/prefer-optional-chain": "warn",
|
|
296
|
+
"@typescript-eslint/consistent-indexed-object-style": "off",
|
|
297
|
+
"prefer-arrow-callback": "error",
|
|
298
|
+
"eslint-comments/require-description": ["error"],
|
|
299
|
+
"prefer-arrow-functions/prefer-arrow-functions": ["warn", {
|
|
300
|
+
allowNamedFunctions: false,
|
|
301
|
+
classPropertiesAllowed: false,
|
|
302
|
+
disallowPrototype: false,
|
|
303
|
+
returnStyle: "explicit",
|
|
304
|
+
singleReturnOnly: false
|
|
305
|
+
}]
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "webadeva/max-lines",
|
|
310
|
+
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
311
|
+
rules: {
|
|
312
|
+
"webadeva/max-lines-warning": "warn",
|
|
313
|
+
"webadeva/max-lines": "error"
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
];
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/utils/files-matching.ts
|
|
319
|
+
/**
|
|
320
|
+
* ESLint accepts `(filePath) => boolean` matchers in `files`, but its types
|
|
321
|
+
* only allow glob strings.
|
|
322
|
+
*/
|
|
323
|
+
const filesMatching = (matcher) => {
|
|
324
|
+
return [matcher];
|
|
325
|
+
};
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/configs/untyped.ts
|
|
328
|
+
ensureTypeScript();
|
|
329
|
+
const tseslint = createRequire(import.meta.url)("typescript-eslint");
|
|
330
|
+
/**
|
|
331
|
+
* Turns off type-aware rules for files no TypeScript project includes (e.g.
|
|
332
|
+
* JS files without `allowJs`), which would otherwise fail to parse. Must come
|
|
333
|
+
* after every config that enables type-aware rules.
|
|
334
|
+
*/
|
|
335
|
+
const untypedFilesConfigs = (disableTypeChecked = []) => {
|
|
336
|
+
const files = filesMatching((filePath) => {
|
|
337
|
+
return isLintableFile(filePath) && !hasTypeInformation(filePath);
|
|
338
|
+
});
|
|
339
|
+
return [...[tseslint.configs.disableTypeChecked, ...disableTypeChecked].map((config) => {
|
|
340
|
+
return {
|
|
341
|
+
...config,
|
|
342
|
+
files
|
|
343
|
+
};
|
|
344
|
+
}), {
|
|
345
|
+
name: "webadeva/typescript-file-not-in-project",
|
|
346
|
+
files: filesMatching((filePath) => {
|
|
347
|
+
return isTypeScriptFile(filePath) && !hasTypeInformation(filePath);
|
|
348
|
+
}),
|
|
349
|
+
rules: { "webadeva/typescript-file-in-project": "error" }
|
|
350
|
+
}];
|
|
351
|
+
};
|
|
352
|
+
//#endregion
|
|
353
|
+
export { baseConfigs as n, ensureTypeScript as r, untypedFilesConfigs as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,131 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webadeva/eslint-config",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Shared ESLint flat configs for TypeScript projects: type-aware rules, Prettier, and presets for React, Next.js, and Storybook.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslintconfig",
|
|
8
|
+
"eslint-config",
|
|
9
|
+
"eslint-flat-config",
|
|
10
|
+
"flat-config",
|
|
11
|
+
"typescript",
|
|
12
|
+
"typescript-eslint",
|
|
13
|
+
"prettier",
|
|
14
|
+
"react",
|
|
15
|
+
"nextjs",
|
|
16
|
+
"storybook",
|
|
17
|
+
"lint"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/webadeva/eslint-config#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/webadeva/eslint-config/issues"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/webadeva/eslint-config.git"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": {
|
|
29
|
+
"name": "Gobind Singh",
|
|
30
|
+
"email": "thesinghgobind@gmail.com",
|
|
31
|
+
"url": "https://www.thegobindsingh.com"
|
|
32
|
+
},
|
|
33
|
+
"type": "module",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"import": "./dist/index.mjs",
|
|
37
|
+
"require": "./dist/index.cjs"
|
|
38
|
+
},
|
|
39
|
+
"./next": {
|
|
40
|
+
"import": "./dist/next.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./react": {
|
|
43
|
+
"import": "./dist/react.mjs"
|
|
44
|
+
},
|
|
45
|
+
"./storybook": {
|
|
46
|
+
"import": "./dist/storybook.mjs"
|
|
47
|
+
},
|
|
48
|
+
"./package.json": "./package.json",
|
|
49
|
+
"./base": {
|
|
50
|
+
"import": "./dist/index.mjs",
|
|
51
|
+
"require": "./dist/index.cjs"
|
|
52
|
+
},
|
|
53
|
+
"./tsconfig.default.json": "./tsconfig.default.json"
|
|
54
|
+
},
|
|
55
|
+
"main": "./dist/index.cjs",
|
|
56
|
+
"module": "./dist/index.mjs",
|
|
57
|
+
"types": "./dist/index.d.cts",
|
|
58
|
+
"files": [
|
|
59
|
+
"dist",
|
|
60
|
+
"tsconfig.default.json"
|
|
61
|
+
],
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=20.19.0"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "tsdown",
|
|
70
|
+
"dev": "tsdown --watch",
|
|
71
|
+
"typecheck": "tsc --noEmit",
|
|
72
|
+
"lint": "eslint .",
|
|
73
|
+
"format": "prettier --write .",
|
|
74
|
+
"format:check": "prettier --check .",
|
|
75
|
+
"test": "node --test \"src/tests/**/*.test.ts\"",
|
|
76
|
+
"check": "pnpm typecheck && pnpm lint && pnpm build && pnpm test",
|
|
77
|
+
"prepare": "husky",
|
|
78
|
+
"prepublishOnly": "pnpm check"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"eslint": ">=10.13.0",
|
|
82
|
+
"prettier": ">=3.0.0",
|
|
83
|
+
"typescript": ">=5.0.0 <7.0.0"
|
|
84
|
+
},
|
|
85
|
+
"peerDependenciesMeta": {
|
|
86
|
+
"eslint": {
|
|
87
|
+
"optional": false
|
|
88
|
+
},
|
|
89
|
+
"prettier": {
|
|
90
|
+
"optional": false
|
|
91
|
+
},
|
|
92
|
+
"typescript": {
|
|
93
|
+
"optional": false
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"dependencies": {
|
|
97
|
+
"@eslint-community/eslint-plugin-eslint-comments": "^4.8.1",
|
|
98
|
+
"@eslint-react/eslint-plugin": "^5.28.2",
|
|
99
|
+
"@eslint/js": "^10.0.1",
|
|
100
|
+
"@next/eslint-plugin-next": "^16.4.2",
|
|
101
|
+
"@typescript-eslint/eslint-plugin": "^8.71.2",
|
|
102
|
+
"@typescript-eslint/parser": "^8.71.2",
|
|
103
|
+
"eslint-config-prettier": "^10.1.8",
|
|
104
|
+
"eslint-plugin-import": "^2.32.0",
|
|
105
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
106
|
+
"eslint-plugin-prefer-arrow-functions": "^3.10.2",
|
|
107
|
+
"eslint-plugin-prettier": "^5.5.7",
|
|
108
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
109
|
+
"eslint-plugin-react-refresh": "^0.5.7",
|
|
110
|
+
"eslint-plugin-storybook": "^10.6.2",
|
|
111
|
+
"globals": "^17.13.0",
|
|
112
|
+
"typescript-eslint": "^8.71.2"
|
|
113
|
+
},
|
|
114
|
+
"devDependencies": {
|
|
115
|
+
"@arethetypeswrong/core": "^0.18.5",
|
|
116
|
+
"@commitlint/cli": "^21.2.4",
|
|
117
|
+
"@commitlint/config-conventional": "^21.2.4",
|
|
118
|
+
"@tsconfig/strictest": "^2.0.8",
|
|
119
|
+
"@types/node": "^26.6.5",
|
|
120
|
+
"@types/react": "^19.3.0",
|
|
121
|
+
"eslint": "^10.13.0",
|
|
122
|
+
"husky": "^9.1.7",
|
|
123
|
+
"jiti": "^2.7.0",
|
|
124
|
+
"prettier": "^3.9.10",
|
|
125
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
126
|
+
"publint": "^0.3.25",
|
|
127
|
+
"tsdown": "^0.23.1",
|
|
128
|
+
"typescript": "6.0.3"
|
|
129
|
+
},
|
|
130
|
+
"packageManager": "pnpm@12.14.0"
|
|
131
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://www.schemastore.org/tsconfig",
|
|
3
|
+
"//": "Used by typescript-eslint for files that no tsconfig/jsconfig includes.",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "Preserve",
|
|
8
|
+
"moduleResolution": "Bundler",
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"checkJs": false,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"noEmit": true
|
|
15
|
+
}
|
|
16
|
+
}
|