@exadev/eslint-config 2.9.1 → 2.10.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/README.md +68 -4
- package/dist/index.cjs +101 -2
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +99 -2
- package/package.json +24 -2
package/README.md
CHANGED
|
@@ -101,6 +101,63 @@ export default tseslint.config(
|
|
|
101
101
|
|
|
102
102
|
**`plugin.configs.recommended`/`plugin.configs.barrel` carry no `files`/`ignores` and are safe unscoped** -- `no-side-effects-in-index` and `no-non-barrel-reexport` each check `context.filename` themselves (self-scoping). For a barrel not at `src/index.ts`, or a project-specific exception, layer an override on top (e.g. `{ files: ['lib/other.ts'], rules: { 'exadev/no-non-barrel-reexport': 'off' } }`) rather than wiring all four rules individually.
|
|
103
103
|
|
|
104
|
+
## Optional React and Next.js support
|
|
105
|
+
|
|
106
|
+
`import exadev from '@exadev/eslint-config'` keeps working unchanged -- it's now literally `exadevConfig()` called with no arguments, no migration required. React/hooks/a11y and Next.js rule blocks are folded in automatically, with no separate import or config needed, gated on two independent, always-both-required conditions:
|
|
107
|
+
|
|
108
|
+
1. **The corresponding package must actually be resolvable.** `eslint-plugin-react`, `eslint-plugin-react-hooks`, `eslint-plugin-jsx-a11y`, and `@next/eslint-plugin-next` are all *optional* peer dependencies (`peerDependenciesMeta.<pkg>.optional: true`) -- install only whichever your project actually needs:
|
|
109
|
+
```sh
|
|
110
|
+
pnpm add -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y # React support
|
|
111
|
+
pnpm add -D @next/eslint-plugin-next # Next.js support
|
|
112
|
+
```
|
|
113
|
+
If none of these resolve, `@exadev/eslint-config`'s default export is byte-for-byte identical to the plain TypeScript ruleset -- nothing about the base package changes.
|
|
114
|
+
2. **For React specifically, the file must actually be `.jsx`/`.tsx`.** The React/hooks/a11y rule block is scoped to `files: ['**/*.jsx', '**/*.tsx']`, so even if `eslint-plugin-react` is resolvable only incidentally (e.g. hoisted as a transitive dependency of something unrelated in a monorepo, with zero real JSX anywhere in the linted project), its rules are never matched against a file that isn't JSX -- ESLint's flat-config `files` matching happens per linted file, at lint time, not at config-build time. `@next/eslint-plugin-next`'s block carries no such glob: its own presence is already an unambiguous signal on its own (nothing installs it except a real Next.js project).
|
|
115
|
+
|
|
116
|
+
### Explicit control
|
|
117
|
+
|
|
118
|
+
Two ways to override the automatic behaviour, for anyone who doesn't want to rely on it:
|
|
119
|
+
|
|
120
|
+
**`plugin.configs.react`/`plugin.configs.nextjs`** -- explicit tier selection, mirroring `plugin.configs.recommended`/`.barrel`. Unlike those two, which only ever reference this package's own always-present rules, selecting `configs.react`/`.nextjs` is itself an explicit request: it **throws** a clear, actionable error if the underlying peer isn't installed, rather than silently returning nothing.
|
|
121
|
+
```ts
|
|
122
|
+
import { plugin } from '@exadev/eslint-config';
|
|
123
|
+
import tseslint from 'typescript-eslint';
|
|
124
|
+
|
|
125
|
+
export default tseslint.config(
|
|
126
|
+
// ...your own config...
|
|
127
|
+
{
|
|
128
|
+
files: ['**/*.tsx'],
|
|
129
|
+
plugins: { exadev: plugin },
|
|
130
|
+
extends: [plugin.configs.react], // throws if eslint-plugin-react isn't installed
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**`exadevConfig(options, ...userConfigs)`** -- the named factory export, for fine-grained tri-state control per feature:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// eslint.config.ts
|
|
139
|
+
import { exadevConfig } from '@exadev/eslint-config';
|
|
140
|
+
import tseslint from 'typescript-eslint';
|
|
141
|
+
|
|
142
|
+
export default tseslint.config(
|
|
143
|
+
{
|
|
144
|
+
languageOptions: {
|
|
145
|
+
parserOptions: { project: './tsconfig.json', tsconfigRootDir: import.meta.dirname },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
...exadevConfig({ react: true, nextjs: false }),
|
|
149
|
+
// ...your own config...
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Value | React (`options.react`) | Next.js (`options.nextjs`) |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| `true` | Force on -- throws if `eslint-plugin-react` isn't resolvable | Force on -- throws if `@next/eslint-plugin-next` isn't resolvable |
|
|
156
|
+
| `false` | Force off -- always `[]`, no resolution attempted | Force off -- always `[]`, no resolution attempted |
|
|
157
|
+
| `undefined` / omitted | Auto-detect (the default) | Auto-detect (the default) |
|
|
158
|
+
|
|
159
|
+
Trailing arguments are arbitrary flat-config objects, appended in order after everything else -- `exadevConfig({}, { rules: { 'no-console': 'warn' } })` is equivalent to spreading the default export plus one more config object.
|
|
160
|
+
|
|
104
161
|
## Rules
|
|
105
162
|
|
|
106
163
|
| Rule | Fixable | Description |
|
|
@@ -155,17 +212,23 @@ The `lint`/`typecheck`/`test`/`build` npm scripts wrap turbo tasks named `_lint`
|
|
|
155
212
|
|
|
156
213
|
## Architecture
|
|
157
214
|
|
|
158
|
-
`src/plugin.ts` builds
|
|
215
|
+
`src/plugin.ts` builds a `TSESLint.FlatConfig.Plugin` (`@typescript-eslint/utils`'s own type -- not ESLint's own `ESLint.Plugin`, which can't hold a rule built with `ESLintUtils.RuleCreator`) combining `src/rules/` into a flat `rules` map. `configs.recommended`, `.barrel`, `.react`, and `.nextjs` are getters in the object literal -- each references the fully-built `plugin` (`plugins: { exadev: plugin }`), which a plain property initializer can't do mid-construction. `recommended` ships `barrel-policy` at `mode: 'banned'`; `barrel` at `mode: 'single'`; `.react`/`.nextjs` call `buildReactConfig`/`buildNextjsConfig` with `enabled: true` (see below).
|
|
216
|
+
|
|
217
|
+
`src/config-types.ts` holds `ConfigValue`/`ConfigArrayValue` (`ConfigArrayValue = Extract<ConfigValue, unknown[]>`, the array-only member of ESLint's own config-value union), shared by every file below rather than redefined per file -- annotating a config array with the wider `ConfigValue` union directly broke `...exadev` with `TS2488` ("must have a Symbol.iterator method").
|
|
218
|
+
|
|
219
|
+
`src/optional-plugin.ts` is the lazy-resolution helper behind React/Next.js support: `tryRequire` wraps `createRequire(import.meta.url)` in try/catch, returning `unknown` (never a cast) so every call site narrows explicitly before use; `readFlatConfig` walks a property path through that `unknown` value via a real type guard, normalizing a stray legacy top-level `parserOptions` key into `languageOptions.parserOptions` along the way (confirmed necessary: `eslint-plugin-jsx-a11y`'s own `configs.recommended` export carries exactly this legacy shape, which flat config's schema rejects outright rather than ignores).
|
|
220
|
+
|
|
221
|
+
`src/react.ts`/`src/nextjs.ts` each export a `build*Config(options)` function: resolve the relevant optional peer(s) via `tryRequire`, extract their real flat config via `readFlatConfig`, and return an array of 0-or-more config blocks -- `[]` if unresolvable and not explicitly forced on, a thrown `Error` if explicitly forced on (`enabled: true`) and still unresolvable. `react.ts`'s blocks are scoped to `files: ['**/*.jsx', '**/*.tsx']`; `nextjs.ts`'s is not (see [Optional React and Next.js support](#optional-react-and-nextjs-support) for why).
|
|
159
222
|
|
|
160
|
-
`src/
|
|
223
|
+
`src/create-config.ts` is config assembly's single source of truth: `exadevConfig(options, ...userConfigs)` concatenates `recommendedTypeChecked` with both builders' output (each fed the matching tri-state option) and any trailing user configs; `defaultConfig` is `exadevConfig()` evaluated once, eagerly, at module load.
|
|
161
224
|
|
|
162
|
-
`src/index.ts` is the entry point: `export { default } from './
|
|
225
|
+
`src/index.ts` is the entry point, still a pure re-export barrel (required by `no-side-effects-in-index`/`no-non-barrel-reexport`, both of which assume this file contains nothing but `export ... from ...`): `export { defaultConfig as default, exadevConfig } from './create-config'; export { default as plugin } from './plugin';`. All exports share one root module, so importing `{ plugin }` alone still resolves `typescript-eslint` via the sibling re-export -- an accepted trade-off (an earlier separate-subpath split proved more awkward in practice). React/Next.js support never adds to this cost: none of the four optional packages are ever statically imported, only passed as a runtime string to `createRequire`'s resolver, so their absence never affects module evaluation for a consumer who doesn't use them.
|
|
163
226
|
|
|
164
227
|
`pnpm-workspace.yaml` declares an empty `packages: []` -- not a real workspace, just giving turbo a root for local task caching.
|
|
165
228
|
|
|
166
229
|
## Conventions
|
|
167
230
|
|
|
168
|
-
`eslint.config.ts` dogfoods
|
|
231
|
+
`eslint.config.ts` dogfoods this package's own factory export on itself (`import { exadevConfig } from './src/index'`), spreading `exadevConfig({ react: false, nextjs: false })` -- forced off explicitly, not the plain auto-detecting default, since `eslint-plugin-react`/`@next/eslint-plugin-next` are real devDependencies of *this* repo (needed to test `src/react.ts`/`src/nextjs.ts`'s own "package is resolvable" branch) even though this repo is neither a React nor a Next.js project. `no-side-effects-in-index` and `no-non-barrel-reexport` self-scope to `src/index.ts` internally, so no `files`/`ignores` wiring is needed here. Plugin construction lives in `src/plugin.ts` specifically so `src/index.ts` stays a pure re-export point.
|
|
169
232
|
|
|
170
233
|
`tsconfig.json` enables `verbatimModuleSyntax` (`import type`/`export type` required for type-only imports -- also enforced by `consistent-type-imports`) and `noUncheckedIndexedAccess` (narrow indexed access before use rather than asserting).
|
|
171
234
|
|
|
@@ -177,6 +240,7 @@ Conventional commits are enforced by commitlint, restricted to the type-enum def
|
|
|
177
240
|
- `src/index.ts` mixing a default export with a named one triggers rolldown's `MIXED_EXPORTS` warning: a raw CommonJS `require()` would see the raw exports object instead of the default. ESM `import` (the actual consumer path) resolves both correctly; `attw --pack` and `publint` report no problems, so the warning is accepted (see `tsdown.config.ts`).
|
|
178
241
|
- Husky hooks: `pre-commit` runs lint-staged (`eslint --fix` on staged `*.ts`), `commit-msg` runs commitlint, `pre-push` runs typecheck + test + build.
|
|
179
242
|
- The CI release job sets `HUSKY=0` (commit-msg hook skips the automated release commit) and blanks `NPM_TOKEN`/`NODE_AUTH_TOKEN` explicitly so an inherited token can't win over OIDC trusted publishing.
|
|
243
|
+
- A consumer who already has `eslint-plugin-react`/`@next/eslint-plugin-next` resolvable for unrelated reasons (e.g. hoisted in a monorepo) and writes `.jsx`/`.tsx` files may see new rule activity the moment they upgrade to a version of this package that ships React/Next.js support -- with zero action on their part. This is the normal, widely-accepted ESLint-ecosystem convention that adding rules to a shared/recommended config is a minor bump even though it can newly trip an existing `--max-warnings 0` gate, not a breaking change; see [Optional React and Next.js support](#optional-react-and-nextjs-support) for the `react`/`nextjs` options to force it off explicitly if needed.
|
|
180
244
|
|
|
181
245
|
## Contributing
|
|
182
246
|
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
enumerable: true
|
|
25
25
|
}) : target, mod));
|
|
26
26
|
//#endregion
|
|
27
|
+
let node_module = require("node:module");
|
|
28
|
+
let _eslint_js = require("@eslint/js");
|
|
29
|
+
_eslint_js = __toESM(_eslint_js, 1);
|
|
27
30
|
let typescript_eslint = require("typescript-eslint");
|
|
28
31
|
typescript_eslint = __toESM(typescript_eslint, 1);
|
|
29
32
|
let node_path = require("node:path");
|
|
@@ -31,8 +34,85 @@ let _typescript_eslint_utils = require("@typescript-eslint/utils");
|
|
|
31
34
|
let typescript = require("typescript");
|
|
32
35
|
typescript = __toESM(typescript, 1);
|
|
33
36
|
let ts_api_utils = require("ts-api-utils");
|
|
37
|
+
//#region src/optional-plugin.ts
|
|
38
|
+
const nodeRequire = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
39
|
+
function tryRequire(specifier, requireFn = nodeRequire) {
|
|
40
|
+
try {
|
|
41
|
+
return requireFn(specifier);
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isRecord(value) {
|
|
47
|
+
return typeof value === "object" && value !== null;
|
|
48
|
+
}
|
|
49
|
+
function isFlatConfig$1(value) {
|
|
50
|
+
return isRecord(value);
|
|
51
|
+
}
|
|
52
|
+
function normalizeLegacyParserOptions(record) {
|
|
53
|
+
if (!("parserOptions" in record)) return record;
|
|
54
|
+
const { parserOptions, languageOptions, ...rest } = record;
|
|
55
|
+
const existingLanguageOptions = isRecord(languageOptions) ? languageOptions : {};
|
|
56
|
+
return {
|
|
57
|
+
...rest,
|
|
58
|
+
languageOptions: {
|
|
59
|
+
...existingLanguageOptions,
|
|
60
|
+
parserOptions
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function readFlatConfig(module, path) {
|
|
65
|
+
let current = module;
|
|
66
|
+
for (const key of path) {
|
|
67
|
+
if (!isRecord(current)) return void 0;
|
|
68
|
+
current = current[key];
|
|
69
|
+
}
|
|
70
|
+
if (!isRecord(current)) return void 0;
|
|
71
|
+
const normalized = normalizeLegacyParserOptions(current);
|
|
72
|
+
return isFlatConfig$1(normalized) ? normalized : void 0;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/nextjs.ts
|
|
76
|
+
const INSTALL_COMMAND$1 = "pnpm add -D @next/eslint-plugin-next";
|
|
77
|
+
function buildNextjsConfig(options = {}) {
|
|
78
|
+
if (options.enabled === false) return [];
|
|
79
|
+
const nextConfig = readFlatConfig(tryRequire("@next/eslint-plugin-next", options.requireFn), ["configs", "core-web-vitals"]);
|
|
80
|
+
if (options.enabled === true && nextConfig === void 0) throw new Error(`@exadev/eslint-config: Next.js support was explicitly requested but '@next/eslint-plugin-next' could not be resolved. Install it with: ${INSTALL_COMMAND$1}`);
|
|
81
|
+
return nextConfig === void 0 ? [] : [nextConfig];
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/react.ts
|
|
85
|
+
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
86
|
+
const INSTALL_COMMAND = "pnpm add -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y";
|
|
87
|
+
function isFlatConfig(value) {
|
|
88
|
+
return value !== void 0;
|
|
89
|
+
}
|
|
90
|
+
function buildReactConfig(options = {}) {
|
|
91
|
+
if (options.enabled === false) return [];
|
|
92
|
+
const reactConfig = readFlatConfig(tryRequire("eslint-plugin-react", options.requireFn), [
|
|
93
|
+
"configs",
|
|
94
|
+
"flat",
|
|
95
|
+
"recommended"
|
|
96
|
+
]);
|
|
97
|
+
if (options.enabled === true && reactConfig === void 0) throw new Error(`@exadev/eslint-config: React support was explicitly requested but 'eslint-plugin-react' could not be resolved. Install it with: ${INSTALL_COMMAND}`);
|
|
98
|
+
if (reactConfig === void 0) return [];
|
|
99
|
+
const hooksModule = tryRequire("eslint-plugin-react-hooks", options.requireFn);
|
|
100
|
+
return [
|
|
101
|
+
reactConfig,
|
|
102
|
+
readFlatConfig(hooksModule, [
|
|
103
|
+
"configs",
|
|
104
|
+
"flat",
|
|
105
|
+
"recommended-latest"
|
|
106
|
+
]) ?? readFlatConfig(hooksModule, ["configs", "recommended-latest"]),
|
|
107
|
+
readFlatConfig(tryRequire("eslint-plugin-jsx-a11y", options.requireFn), ["flatConfigs", "recommended"])
|
|
108
|
+
].filter(isFlatConfig).map((config) => ({
|
|
109
|
+
...config,
|
|
110
|
+
files: [...JSX_FILE_PATTERNS]
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
34
114
|
//#region package.json
|
|
35
|
-
var version = "2.
|
|
115
|
+
var version = "2.10.1";
|
|
36
116
|
//#endregion
|
|
37
117
|
//#region src/rules/barrel-helpers.ts
|
|
38
118
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -1248,6 +1328,12 @@ const plugin = {
|
|
|
1248
1328
|
plugins: { exadev: plugin },
|
|
1249
1329
|
rules: { "exadev/barrel-policy": ["error", { mode: "single" }] }
|
|
1250
1330
|
};
|
|
1331
|
+
},
|
|
1332
|
+
get react() {
|
|
1333
|
+
return buildReactConfig({ enabled: true });
|
|
1334
|
+
},
|
|
1335
|
+
get nextjs() {
|
|
1336
|
+
return buildNextjsConfig({ enabled: true });
|
|
1251
1337
|
}
|
|
1252
1338
|
}
|
|
1253
1339
|
};
|
|
@@ -1255,6 +1341,7 @@ const plugin = {
|
|
|
1255
1341
|
//#region src/recommended-type-checked.ts
|
|
1256
1342
|
const TEST_FILE_PATTERNS = "**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
1257
1343
|
const recommendedTypeChecked = [
|
|
1344
|
+
_eslint_js.default.configs.recommended,
|
|
1258
1345
|
...typescript_eslint.default.configs.strictTypeChecked,
|
|
1259
1346
|
...typescript_eslint.default.configs.stylisticTypeChecked,
|
|
1260
1347
|
{
|
|
@@ -1312,5 +1399,17 @@ const recommendedTypeChecked = [
|
|
|
1312
1399
|
}
|
|
1313
1400
|
];
|
|
1314
1401
|
//#endregion
|
|
1315
|
-
|
|
1402
|
+
//#region src/create-config.ts
|
|
1403
|
+
function exadevConfig(options = {}, ...userConfigs) {
|
|
1404
|
+
return [
|
|
1405
|
+
...recommendedTypeChecked,
|
|
1406
|
+
...buildReactConfig({ enabled: options.react }),
|
|
1407
|
+
...buildNextjsConfig({ enabled: options.nextjs }),
|
|
1408
|
+
...userConfigs
|
|
1409
|
+
];
|
|
1410
|
+
}
|
|
1411
|
+
const defaultConfig = exadevConfig();
|
|
1412
|
+
//#endregion
|
|
1413
|
+
exports.default = defaultConfig;
|
|
1414
|
+
exports.exadevConfig = exadevConfig;
|
|
1316
1415
|
exports.plugin = plugin;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
-
//#region src/
|
|
2
|
+
//#region src/config-types.d.ts
|
|
3
3
|
type ConfigValue = NonNullable<TSESLint.FlatConfig.Plugin['configs']>[string];
|
|
4
4
|
type ConfigArrayValue = Extract<ConfigValue, unknown[]>;
|
|
5
|
-
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/create-config.d.ts
|
|
7
|
+
interface ExadevConfigOptions {
|
|
8
|
+
readonly react?: boolean;
|
|
9
|
+
readonly nextjs?: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare function exadevConfig(options?: ExadevConfigOptions, ...userConfigs: readonly TSESLint.FlatConfig.Config[]): ConfigArrayValue;
|
|
12
|
+
declare const defaultConfig: ConfigArrayValue;
|
|
6
13
|
//#endregion
|
|
7
14
|
//#region src/plugin.d.ts
|
|
8
15
|
declare const plugin: TSESLint.FlatConfig.Plugin;
|
|
9
16
|
//#endregion
|
|
10
|
-
export {
|
|
17
|
+
export { defaultConfig as default, exadevConfig, plugin };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
-
//#region src/
|
|
2
|
+
//#region src/config-types.d.ts
|
|
3
3
|
type ConfigValue = NonNullable<TSESLint.FlatConfig.Plugin['configs']>[string];
|
|
4
4
|
type ConfigArrayValue = Extract<ConfigValue, unknown[]>;
|
|
5
|
-
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/create-config.d.ts
|
|
7
|
+
interface ExadevConfigOptions {
|
|
8
|
+
readonly react?: boolean;
|
|
9
|
+
readonly nextjs?: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare function exadevConfig(options?: ExadevConfigOptions, ...userConfigs: readonly TSESLint.FlatConfig.Config[]): ConfigArrayValue;
|
|
12
|
+
declare const defaultConfig: ConfigArrayValue;
|
|
6
13
|
//#endregion
|
|
7
14
|
//#region src/plugin.d.ts
|
|
8
15
|
declare const plugin: TSESLint.FlatConfig.Plugin;
|
|
9
16
|
//#endregion
|
|
10
|
-
export {
|
|
17
|
+
export { defaultConfig as default, exadevConfig, plugin };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,89 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import js from "@eslint/js";
|
|
1
3
|
import tseslint from "typescript-eslint";
|
|
2
4
|
import { posix } from "node:path";
|
|
3
5
|
import { AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
4
6
|
import * as ts from "typescript";
|
|
5
7
|
import { isPropertyReadonlyInType, isTypeReference } from "ts-api-utils";
|
|
8
|
+
//#region src/optional-plugin.ts
|
|
9
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
10
|
+
function tryRequire(specifier, requireFn = nodeRequire) {
|
|
11
|
+
try {
|
|
12
|
+
return requireFn(specifier);
|
|
13
|
+
} catch {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return typeof value === "object" && value !== null;
|
|
19
|
+
}
|
|
20
|
+
function isFlatConfig$1(value) {
|
|
21
|
+
return isRecord(value);
|
|
22
|
+
}
|
|
23
|
+
function normalizeLegacyParserOptions(record) {
|
|
24
|
+
if (!("parserOptions" in record)) return record;
|
|
25
|
+
const { parserOptions, languageOptions, ...rest } = record;
|
|
26
|
+
const existingLanguageOptions = isRecord(languageOptions) ? languageOptions : {};
|
|
27
|
+
return {
|
|
28
|
+
...rest,
|
|
29
|
+
languageOptions: {
|
|
30
|
+
...existingLanguageOptions,
|
|
31
|
+
parserOptions
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function readFlatConfig(module, path) {
|
|
36
|
+
let current = module;
|
|
37
|
+
for (const key of path) {
|
|
38
|
+
if (!isRecord(current)) return void 0;
|
|
39
|
+
current = current[key];
|
|
40
|
+
}
|
|
41
|
+
if (!isRecord(current)) return void 0;
|
|
42
|
+
const normalized = normalizeLegacyParserOptions(current);
|
|
43
|
+
return isFlatConfig$1(normalized) ? normalized : void 0;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/nextjs.ts
|
|
47
|
+
const INSTALL_COMMAND$1 = "pnpm add -D @next/eslint-plugin-next";
|
|
48
|
+
function buildNextjsConfig(options = {}) {
|
|
49
|
+
if (options.enabled === false) return [];
|
|
50
|
+
const nextConfig = readFlatConfig(tryRequire("@next/eslint-plugin-next", options.requireFn), ["configs", "core-web-vitals"]);
|
|
51
|
+
if (options.enabled === true && nextConfig === void 0) throw new Error(`@exadev/eslint-config: Next.js support was explicitly requested but '@next/eslint-plugin-next' could not be resolved. Install it with: ${INSTALL_COMMAND$1}`);
|
|
52
|
+
return nextConfig === void 0 ? [] : [nextConfig];
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/react.ts
|
|
56
|
+
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
57
|
+
const INSTALL_COMMAND = "pnpm add -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y";
|
|
58
|
+
function isFlatConfig(value) {
|
|
59
|
+
return value !== void 0;
|
|
60
|
+
}
|
|
61
|
+
function buildReactConfig(options = {}) {
|
|
62
|
+
if (options.enabled === false) return [];
|
|
63
|
+
const reactConfig = readFlatConfig(tryRequire("eslint-plugin-react", options.requireFn), [
|
|
64
|
+
"configs",
|
|
65
|
+
"flat",
|
|
66
|
+
"recommended"
|
|
67
|
+
]);
|
|
68
|
+
if (options.enabled === true && reactConfig === void 0) throw new Error(`@exadev/eslint-config: React support was explicitly requested but 'eslint-plugin-react' could not be resolved. Install it with: ${INSTALL_COMMAND}`);
|
|
69
|
+
if (reactConfig === void 0) return [];
|
|
70
|
+
const hooksModule = tryRequire("eslint-plugin-react-hooks", options.requireFn);
|
|
71
|
+
return [
|
|
72
|
+
reactConfig,
|
|
73
|
+
readFlatConfig(hooksModule, [
|
|
74
|
+
"configs",
|
|
75
|
+
"flat",
|
|
76
|
+
"recommended-latest"
|
|
77
|
+
]) ?? readFlatConfig(hooksModule, ["configs", "recommended-latest"]),
|
|
78
|
+
readFlatConfig(tryRequire("eslint-plugin-jsx-a11y", options.requireFn), ["flatConfigs", "recommended"])
|
|
79
|
+
].filter(isFlatConfig).map((config) => ({
|
|
80
|
+
...config,
|
|
81
|
+
files: [...JSX_FILE_PATTERNS]
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
6
85
|
//#region package.json
|
|
7
|
-
var version = "2.
|
|
86
|
+
var version = "2.10.1";
|
|
8
87
|
//#endregion
|
|
9
88
|
//#region src/rules/barrel-helpers.ts
|
|
10
89
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -1220,6 +1299,12 @@ const plugin = {
|
|
|
1220
1299
|
plugins: { exadev: plugin },
|
|
1221
1300
|
rules: { "exadev/barrel-policy": ["error", { mode: "single" }] }
|
|
1222
1301
|
};
|
|
1302
|
+
},
|
|
1303
|
+
get react() {
|
|
1304
|
+
return buildReactConfig({ enabled: true });
|
|
1305
|
+
},
|
|
1306
|
+
get nextjs() {
|
|
1307
|
+
return buildNextjsConfig({ enabled: true });
|
|
1223
1308
|
}
|
|
1224
1309
|
}
|
|
1225
1310
|
};
|
|
@@ -1227,6 +1312,7 @@ const plugin = {
|
|
|
1227
1312
|
//#region src/recommended-type-checked.ts
|
|
1228
1313
|
const TEST_FILE_PATTERNS = "**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
1229
1314
|
const recommendedTypeChecked = [
|
|
1315
|
+
js.configs.recommended,
|
|
1230
1316
|
...tseslint.configs.strictTypeChecked,
|
|
1231
1317
|
...tseslint.configs.stylisticTypeChecked,
|
|
1232
1318
|
{
|
|
@@ -1284,4 +1370,15 @@ const recommendedTypeChecked = [
|
|
|
1284
1370
|
}
|
|
1285
1371
|
];
|
|
1286
1372
|
//#endregion
|
|
1287
|
-
|
|
1373
|
+
//#region src/create-config.ts
|
|
1374
|
+
function exadevConfig(options = {}, ...userConfigs) {
|
|
1375
|
+
return [
|
|
1376
|
+
...recommendedTypeChecked,
|
|
1377
|
+
...buildReactConfig({ enabled: options.react }),
|
|
1378
|
+
...buildNextjsConfig({ enabled: options.nextjs }),
|
|
1379
|
+
...userConfigs
|
|
1380
|
+
];
|
|
1381
|
+
}
|
|
1382
|
+
const defaultConfig = exadevConfig();
|
|
1383
|
+
//#endregion
|
|
1384
|
+
export { defaultConfig as default, exadevConfig, plugin };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exadev/eslint-config",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.1",
|
|
4
4
|
"description": "Shared custom ESLint rules and plugin for ExaDev projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -38,21 +38,42 @@
|
|
|
38
38
|
"url": "git+https://github.com/ExaDev/eslint-config.git"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
+
"@next/eslint-plugin-next": "^16.3.2",
|
|
41
42
|
"eslint": ">=10.0.0",
|
|
43
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
44
|
+
"eslint-plugin-react": "^7.37.5",
|
|
45
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
42
46
|
"typescript": ">=4.8.4",
|
|
43
47
|
"typescript-eslint": ">=8.0.0"
|
|
44
48
|
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@next/eslint-plugin-next": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"eslint-plugin-jsx-a11y": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"eslint-plugin-react": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"eslint-plugin-react-hooks": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
45
63
|
"devDependencies": {
|
|
46
64
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
47
65
|
"@commitlint/cli": "^21.2.1",
|
|
48
66
|
"@commitlint/config-conventional": "^21.2.0",
|
|
49
|
-
"@eslint
|
|
67
|
+
"@next/eslint-plugin-next": "^16.3.2",
|
|
50
68
|
"@semantic-release/changelog": "^7.0.0",
|
|
51
69
|
"@semantic-release/git": "^11.0.1",
|
|
52
70
|
"@types/node": "^24.9.2",
|
|
53
71
|
"@typescript-eslint/rule-tester": "^8.67.0",
|
|
54
72
|
"@vitest/coverage-v8": "^4.1.10",
|
|
55
73
|
"eslint": "^10.8.0",
|
|
74
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
75
|
+
"eslint-plugin-react": "^7.37.5",
|
|
76
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
56
77
|
"husky": "^9.1.7",
|
|
57
78
|
"lint-staged": "^17.2.0",
|
|
58
79
|
"publint": "^0.3.21",
|
|
@@ -77,6 +98,7 @@
|
|
|
77
98
|
"release": "semantic-release"
|
|
78
99
|
},
|
|
79
100
|
"dependencies": {
|
|
101
|
+
"@eslint/js": "^10.0.1",
|
|
80
102
|
"@typescript-eslint/utils": "^8.67.0",
|
|
81
103
|
"ts-api-utils": "^2.5.0"
|
|
82
104
|
}
|