exadev-eslint-config 2.22.1 → 2.23.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.
- package/README.md +42 -9
- package/dist/index.cjs +670 -89
- package/dist/index.d.cts +34 -15
- package/dist/index.d.ts +34 -15
- package/dist/index.js +669 -89
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ export default defineConfig(
|
|
|
38
38
|
|
|
39
39
|
**Remove your own `tseslint.configs.recommended`/`recommendedTypeChecked`/`strictTypeChecked`/`stylisticTypeChecked` spreads.** The default export already includes [`strictTypeChecked`](https://typescript-eslint.io/users/configs/#strict-type-checked) (which subsumes both plain [`recommended`](https://typescript-eslint.io/users/configs/#recommended) and [`recommendedTypeChecked`](https://typescript-eslint.io/users/configs/#recommended-type-checked)) plus [`stylisticTypeChecked`](https://typescript-eslint.io/users/configs/#stylistic-type-checked), and registers the `@typescript-eslint` plugin/parser itself — [flat config](https://eslint.org/docs/latest/use/configure/configuration-files) rejects two different plugin object instances registered under the same namespace. You still supply your own `languageOptions.parserOptions.project`/`projectService` pointing at your tsconfig(s).
|
|
40
40
|
|
|
41
|
+
**Remove your own `plugins: { '@stylistic': ... }` registration too.** The default export bundles [`@stylistic/eslint-plugin`](https://eslint.style) (see [Stylistic comment, class-member and JSX rules](#stylistic-comment-class-member-and-jsx-rules)) and registers it under the `@stylistic` namespace itself, so a consumer block registering its own copy of the plugin (a different version resolved from elsewhere in the lockfile, or a duplicated install) makes ESLint throw `Config: Key "plugins": Cannot redefine plugin "@stylistic".` at config load, the identical flat-config restriction as the typescript-eslint case above. Registering the very same instance is harmless, but the robust migration is to drop the registration entirely: configure any further `@stylistic/*` rules directly in your own `rules` block, since this package's block already registers the plugin for every JS/TS and JSX file.
|
|
42
|
+
|
|
41
43
|
### What the default export includes
|
|
42
44
|
|
|
43
45
|
**typescript-eslint presets:**
|
|
@@ -58,6 +60,7 @@ export default defineConfig(
|
|
|
58
60
|
- `exadev/prefer-readonly-object-param`
|
|
59
61
|
- `exadev/prefer-numeric-sort-compare`
|
|
60
62
|
- `exadev/prefer-options-object-param`
|
|
63
|
+
- `exadev/prefer-doc-comment`
|
|
61
64
|
- `exadev/no-pointless-reassignment`
|
|
62
65
|
- `exadev/test-file-kind`
|
|
63
66
|
|
|
@@ -213,6 +216,11 @@ React support pairs `eslint-plugin-react`'s `flat/recommended` with its own `fla
|
|
|
213
216
|
plugins: { exadev: plugin },
|
|
214
217
|
extends: [plugin.configs.react], // throws if eslint-plugin-react isn't installed
|
|
215
218
|
},
|
|
219
|
+
{
|
|
220
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
221
|
+
plugins: { exadev: plugin },
|
|
222
|
+
extends: [plugin.configs.nextjs], // throws if @next/eslint-plugin-next isn't installed
|
|
223
|
+
},
|
|
216
224
|
);
|
|
217
225
|
```
|
|
218
226
|
- **`exadevConfig({ react, nextjs })`** — see the tri-state table below.
|
|
@@ -247,6 +255,21 @@ The plugin's own full-canonicalization rule ([`no-insignificant-whitespace`](htt
|
|
|
247
255
|
|
|
248
256
|
`**/package.json` gets everything the plain-JSON config gives every other file — including pretty-printed layout — except [`json/sort-keys`](https://github.com/eslint/json/blob/main/docs/rules/sort-keys.md), turned back off in its own override block, since its key order is the separate, syncpack-aware concern the next section covers.
|
|
249
257
|
|
|
258
|
+
### Stylistic comment, class-member and JSX rules
|
|
259
|
+
|
|
260
|
+
A hand-picked subset of [`@stylistic/eslint-plugin`](https://eslint.style) is bundled unconditionally, the same way jsdoc/tsdoc and RFC 8785 JSON support are: `@stylistic/eslint-plugin` is a plain dependency of this package, so every consumer already has it, and every rule below matches purely on comment shape, statement/class-member placement, or JSX prop/tag conventions, never on formatting (indentation, quotes, semicolons, trailing commas, line wrapping, bracket spacing), which stays Prettier's job in every consumer of this package. None of these rules appears in [`eslint-config-prettier`](https://github.com/prettier/eslint-config-prettier)'s own disabled-rules list.
|
|
261
|
+
|
|
262
|
+
- **[`multiline-comment-style`](https://eslint.style/rules/multiline-comment-style)** is deliberately NOT enabled: the installed release's own directive-comment filter only recognises the `eslint`/`jshint`/`jslint`/`istanbul`/`globals`/`exported`/`jscs` family, so a run of `//` lines mixed with a `prettier-ignore`, `c8 ignore`/`v8 ignore`, `@ts-expect-error`/`@ts-ignore`/`@ts-nocheck`/`@ts-check`, or `TODO`/`FIXME` comment gets merged into one block and the directive lost (a lost `prettier-ignore` then lets Prettier reformat the code it protected), and two or more consecutive triple-slash reference/AMD directives break the same way regardless of file extension. This is a confirmed upstream defect, [issue #1249](https://github.com/eslint-stylistic/eslint-stylistic/issues/1249), fixed by the merged [PR #1251](https://github.com/eslint-stylistic/eslint-stylistic/pull/1251); the triple-slash gap is also tracked upstream in [issue #1285](https://github.com/eslint-stylistic/eslint-stylistic/issues/1285). This rule is re-enabled once a stable release contains PR #1251's fix, tracked at [ExaDev/eslint-config#45](https://github.com/ExaDev/eslint-config/issues/45).
|
|
263
|
+
- **[`spaced-comment`](https://eslint.style/rules/spaced-comment)**: requires a space after `//`/`/*`, without fighting a `/**`-opening doc comment or a shebang line. Adds `{ block: { markers: ['!'] } }` on top of the bare default so a `/*!` license/banner block is not flagged either. A SPACED triple-slash reference/AMD directive (`/// <reference types="..." />`, TypeScript's own compiler-emitted spelling) is exempted by the installed rule's own hard-coded check; a no-space directive (`///<reference types="..." />`, also valid TypeScript syntax) is not, and gets rewritten by `--fix` into a broken `// /<reference ...>` comment TypeScript no longer recognises. Confirmed directly against the installed rule with no config-level workaround available (`markers`/`exceptions` both still require real whitespace immediately after the marker, which the no-space form never has); tracked at [ExaDev/eslint-config#47](https://github.com/ExaDev/eslint-config/issues/47). Also adds `{ line: { markers: ['#', '#region', '#endregion'] } }`: without it, the bare default rewrites a `//# sourceMappingURL=...`/`//# sourceURL=...` source-map comment into `// # sourceMappingURL=...`, which browsers and Node no longer recognise, and a bare `line: { markers: ['#'] }` alone would instead rewrite an unspaced `//#region`/`//#endregion` editor fold marker into `//# region`, which VS Code's own folding regex no longer matches. Listing all three markers together, confirmed directly against the installed rule, leaves every one of those four shapes untouched.
|
|
264
|
+
- **[`lines-between-class-members`](https://eslint.style/rules/lines-between-class-members)** at its bare default: a blank line between class members, except after a `.d.ts`-style method overload signature.
|
|
265
|
+
- **[`line-comment-position`](https://eslint.style/rules/line-comment-position)** at `'above'`: a `//` comment belongs above the code it describes, never trailing beside it on the same line. Not autofixable; a flagged trailing comment needs moving by hand. A trailing `// cspell:disable-line` is exempt (`ignorePattern`): cspell honours that directive only on the line it suppresses, so moving it above, as the report demands, would silently disable spell-checking for the wrong line. Every other directive this package recognises applies to the line(s) below it and is correctly relocated.
|
|
266
|
+
- **[`padding-line-between-statements`](https://eslint.style/rules/padding-line-between-statements)**: a blank line is required after the last statement of a directive prologue (not between two consecutive directives), after the last CommonJS or ES import in a run (not between two consecutive imports), and before a `return` statement.
|
|
267
|
+
- **[`jsx-curly-brace-presence`](https://eslint.style/rules/jsx-curly-brace-presence)**, **[`jsx-pascal-case`](https://eslint.style/rules/jsx-pascal-case)**, and **[`jsx-self-closing-comp`](https://eslint.style/rules/jsx-self-closing-comp)**, each at its own bare default: none of the three has any dependency on `eslint-plugin-react` or React itself (confirmed directly against `@stylistic/eslint-plugin`'s own `package.json`, whose only `peerDependencies` entry is `eslint`), since each matches purely on a JSX AST node shape and is a structural no-op on every non-JSX file.
|
|
268
|
+
|
|
269
|
+
`@stylistic/eslint-plugin` has no rule covering JSX's shorthand-boolean-prop convention (`<Foo enabled />` vs `<Foo enabled={true} />`) or shorthand-fragment syntax (`<>...</>` vs `<React.Fragment>...</React.Fragment>`); those are `eslint-plugin-react`'s own `react/jsx-boolean-value` and `react/jsx-fragments`, which stay entirely within the optional [React support](#optional-react-and-nextjs-support) gate above, not this unconditional bundle.
|
|
270
|
+
|
|
271
|
+
This package's own **`exadev/prefer-doc-comment`** rule (see [Rules](#rules)) is wired alongside these, in the same unconditional bundle.
|
|
272
|
+
|
|
250
273
|
### Optional package.json key ordering
|
|
251
274
|
|
|
252
275
|
`exadevConfig({ packageJsonKeyOrder: true })` enables `exadev/package-json-key-order` for `**/package.json`, requiring the same key order [`syncpack format`](https://syncpack.dev/command/format) would produce — `sortFirst` fields (`name`, `description`, `version`, `author` by default) pinned to the top in that exact order, then every other top-level key alphabetically; and, inside each `sortAz`-listed field's own object or array value (`dependencies`, `devDependencies`, `scripts`, `keywords`, and the rest of syncpack's own default list), its members/elements sorted the same way. Confirmed directly against real `syncpack@15` output, not assumed from its docs — see this rule's own source comment for the exact reverse-engineering method (a symbol-before-digit-before-letter, case-insensitive comparison syncpack's docs don't specify precisely enough to derive from prose alone).
|
|
@@ -286,6 +309,7 @@ Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-d
|
|
|
286
309
|
| `no-enum-reverse-lookup-widening` | suggestion | **A numeric enum's reverse lookup can silently type as `string` for an out-of-range index.** Indexing a numeric enum's reverse mapping (`Direction[n]`) with a bare (non-literal) `number`, or with a different enum's member, types as plain `string` for any index, including one outside the enum's actual members, where it genuinely returns `undefined` at runtime — `tsc` does not range-check even a numeric literal index here. When the indexed expression is the init of a variable with an explicit `: string` annotation, a suggestion widens it to `: string \| undefined`, forcing later uses as a bare `string` to surface as real compile errors; every other syntactic position gets a plain report with no fix, and no case gets a full `--fix` autofix. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
287
310
|
| `prefer-numeric-sort-compare` | suggestion | **`.sort()` on a number array sorts lexicographically by default.** A deliberately narrow addition alongside [`@typescript-eslint/require-array-sort-compare`](https://typescript-eslint.io/rules/require-array-sort-compare/) (which already flags any bare `.sort()`/`.toSorted()` except on a plain string array, with no fix): when the array's element type is definitively `number`, a suggestion offers an ascending compare function (`(a, b) => a - b`), since the default comparator sorts lexicographically (`[1, 2, 10].sort()` becomes `[1, 10, 2]`). Not a full autofix — descending order is a real, if less common, alternative intent. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`, since it needs the checker to confirm the array's element type. |
|
|
288
311
|
| `prefer-options-object-param` | suggestion | **A run of 2+ trailing optional parameters (`?`-marked or default-valued) should be bundled into one destructured `options` parameter.** Without this, a caller needing only the last optional parameter must still pass `undefined` for every optional parameter before it (the motivating case: a 10-parameter constructor with 8 trailing optional ones). The threshold is configurable (`{ minTrailingOptional }`, default `2`). A suggestion (not a full autofix) rewrites the parameter list and inserts a `const { ... } = options ?? {};` destructure as the body's first statement; call sites are never rewritten, since the signature edit alone turns every stale positional call site into a real compile error. Still reported, but with no suggestion offered, whenever collapsing the run would not be safe/mechanical: a parameter property in the run, a parameter with no simple resolvable name and explicit type of its own (including a destructured parameter, or one typed only through a separately-declared function-type alias), a decorated parameter, a rest parameter anywhere in the full parameter list, no `BlockStatement` body at all (an arrow's expression body, or any declaration-only signature — including an ambient `.d.ts` function, an interface method/construct signature, or an abstract/ambient class method), a `@param` JSDoc tag naming a parameter in the run, or a parameter/function-scope-local variable already named `options`. Complements `max-params` (see "Individual rule tuning" above), which catches the different shape (excessive REQUIRED parameters) this rule is deliberately blind to. Requires no type information — registered in both `plugin.configs.recommended` and the default (type-checked) export, like `prefer-readonly-array-param` above; unlike its own `prefer-readonly-object-param`/`prefer-numeric-sort-compare` siblings, which genuinely need the checker and so cannot be offered outside the default export at all. |
|
|
312
|
+
| `prefer-doc-comment` | ✓ | **A substantial leading comment on an exported declaration should be a doc comment, not a plain `//`/`/* */` comment.** "Substantial" means 2 or more comment lines, or a single line whose text exceeds a configurable length (`{ maxLineLength }`, default `80`). Covers an exported function declaration, an exported function's own signature (a body-less `TSDeclareFunction`, an ordinary overload signature and a genuinely ambient one alike, `declare function foo(): void;` included, since TypeScript never shows either one's own implementation to callers, only the signature itself), an exported `const` declaration, whatever its initialiser (a function expression or arrow function, an ordinary value such as `export const TIMEOUT_MS = 5000;`, or none at all in an ambient `export declare const x: number;`; reported once even when the statement declares several, e.g. `export const h1 = () => {}, h2 = () => {};`), an exported class declaration, ambient (`export declare class`) or ordinary alike (TypeScript strips only an ambient class's own MEMBER bodies, never its declaration shape, so its leading comment documents the identical public contract an ordinary exported class's does), an exported class's public (non-`private`/`protected`/`#`-private) method (covering both a method's own overload signature and its trailing implementation) and public abstract method (`abstract m(): void;`, needing no equivalent overload-implementation exemption of its own, since TypeScript's own grammar never lets an abstract method carry a body at all), an exported class's public function/arrow-valued property (the class-member equivalent of the function-initialised `const` shape above), an exported interface/type-alias declaration, an exported enum declaration (`const enum` alike; never an individual member, the identical reason an interface's members are never individually checked), an exported namespace/module declaration (ambient or ordinary alike, at any nesting depth whose enclosing namespaces are all exported themselves), and a default-exported expression (`export default (): number => 1;`, `export default 42;`; a default-exported `function`/`class` declaration or body-less signature is covered by its own shape above and reported exactly once, never twice); a non-exported declaration is never reported, regardless of its comment. A TypeScript function/method overload set's own body-carrying IMPLEMENTATION signature is never reported either, however substantial its own comment: TypeScript never shows that signature to callers at all, only the separate overload signatures above it (a body-less `TSDeclareFunction` for a function, written without `declare`, or a `MethodDefinition` whose own `value` is `TSEmptyBodyFunctionExpression` for a class method) are ever checked against a call site, and both of those are themselves reported like any other declaration in this rule's scope, as the overload set's own real public contract; a comment directly above the implementation is instead internal reasoning about how the overloads are actually realised, never a public contract. The implementation is detected via ESLint's own scope analysis for a function (the overload signatures and the implementation all share one Variable) and, for a class method, via an EARLIER (source-order), same-`static`-ness, same-key signature sibling: a same-named `static`/instance pair is never one overload set to TypeScript itself, and a same-named signature declared AFTER the implementation belongs to a different, later member, never the one the implementation itself realises, so neither is ever mistaken for the implementation's own overload signature. A trailing comment on the previous line of code (`const x = 1; // note`) is never treated as the next declaration's own leading comment. A directive-shaped line (the full ESLint directive/config-comment family, `eslint-disable`/`eslint-disable-line`/`eslint-disable-next-line`/`eslint-enable`/`eslint`/`eslint-env`/`global`/`globals`/`exported`, matched case-sensitively, exactly as ESLint's own directive parser matches them, each requiring real whitespace or end-of-string immediately after the keyword rather than a bare word boundary, so an ordinary compound word such as `eslint-plugin-x` is never mistaken for one, and, on a `//` Line comment specifically, recognised only as `eslint-disable-line`/`eslint-disable-next-line`, the two labels ESLint itself honours there; every other member of the family is a directive only in Block form, matching what ESLint itself does; `@ts-expect-error`/`@ts-ignore`/`@ts-nocheck`/`@ts-check`, the `ts-*` markers matched with or without their own leading `@`; `TODO`/`FIXME`, matched case-insensitively when all-uppercase or immediately followed by `:`/`(` (`todo:`, `fixme(scope):`), never a `Todo`/`Fixme` opening ordinary prose; `prettier-ignore`; `c8 ignore`/`v8 ignore`/`istanbul ignore`/`node:coverage ignore`/`node:coverage disable`/`node:coverage enable`; `cspell:disable`/`cspell:disable-line`/`cspell:disable-next-line`/`cspell:enable` (never cspell's own `ignore`/`words` directives, which name dictionary words rather than suppressing a stretch of text); `biome-ignore`; and `#region`/`#endregion`, an editor folding marker) found anywhere in a `//` run is never itself merged into the fix: only the prose segment directly adjacent to the declaration, the run of lines after the LAST such directive, is ever reported and converted; every earlier segment, and every directive line itself, is left completely untouched, since an earlier segment sits on the far side of a directive from the declaration and documents whatever the directive itself demarcates, never the declaration below it; a directive seated directly above the declaration itself is transparent to this split rather than a boundary of it, since group assembly skips past it before the run is ever collected, so the prose above it is still reported and converted, with the directive preserved untouched in place, still functional, directly above the declaration; a directive found inside an already-consolidated Block comment leaves the whole comment alone, since a partial fix cannot safely be sliced out of one physical comment node. A `///` triple-slash reference/AMD directive (`/// <reference types="..." />`) is recognised separately, by its own un-stripped comment value rather than this same pattern, and excluded from the fix the same way. An existing `/**`-opening doc comment, or a `/*!` license/banner block, is left alone regardless of length. Autofix rewraps the comment's existing text into a `/** ... */` block, one line per original logical line as this rule's own fixer writes it, preserving each line's own indentation beyond the delimiter's single conventional space (never flattening a nested example or sub-list to its neighbours' level), and unwraps a hand-written starred block's (`/* ... */` with its own leading `* ` per line) marker rather than doubling it against the fixer's own. This describes this rule's own fixer alone, not a guarantee that `exadevConfig()`'s combined output preserves that text verbatim: a bundled sibling rule's own fixer can still rewrite it further in the same `--fix` run, which is exactly why the fix is withheld below for the two such collisions this rule already guards against. Handles the leading comment whether it is a run of `//` lines or already a bare block: a bare block may be hand-written, or, for a consumer who separately enables `@stylistic/eslint-plugin`'s own `multiline-comment-style` themselves, produced by its own bare-block fixer. Always reported when substantial, but the fix withheld, rather than silently skipping the violation, whenever the exact resulting text would contain a literal closing-comment delimiter or fails to parse as valid TSDoc (checked directly against `@microsoft/tsdoc`'s own parser, the same one `eslint-plugin-tsdoc`'s `tsdoc/syntax` validates against). Also withheld when any considered line itself begins with a literal `*` once its own leading whitespace is trimmed (a hand-written markdown bullet, `* item`, or a bare block comment whose own lines are only partially starred): spliced verbatim into this fixer's own template, that reads as a repeated delimiter on a middle line, which eslint-plugin-jsdoc's own `jsdoc/no-multi-asterisks` rule (bundled unconditionally alongside this rule, see [`jsdoc.ts`](src/jsdoc.ts)) would then strip in the very same `--fix` run, silently losing real content in a way neither this rule nor `@microsoft/tsdoc`'s own parser has any way to detect on its own. Also withheld when any considered line carries an unescaped tag-shaped `@word` mention, at the line's own start (a genuine TSDoc block or modifier tag, `@remarks`, `@param foo`, `@internal`, `@typeParam T`, `@virtual`, `@override`, ...) or immediately after whitespace anywhere further into the line (a bare mid-line mention, `tseslint.config() is @deprecated upstream`), the identical shape `jsdoc/escape-inline-tags` itself keys on: a line-start tag withholds because jsdoc.ts spreads `flat/recommended-tsdoc-error` with no `settings.jsdoc` override of its own, so eslint-plugin-jsdoc's own tag-vocabulary rules (bundled unconditionally alongside this rule too) validate a doc comment's tags against JSDoc/TypeScript's own vocabulary rather than TSDoc's, and confirmed directly (a real combined-config run, not this rule in isolation) to delete, rewrite or otherwise fight over a genuine TSDoc-only tag in the very same `--fix` run: `jsdoc/empty-tags` deletes an `@internal` tag's own description text, `jsdoc/check-tag-names` deletes `@public`/`@readonly` outright and rewrites `@typeParam` to `@template` (which then fails `tsdoc/syntax`, since `@template` is not itself valid TSDoc) and `@virtual` to `@abstract`, and `@override` triggers a live fight between `check-tag-names` and `empty-tags`. A bare mid-line mention withholds because it is worse than a sibling-fixer collision: it parses as perfectly valid TSDoc, but confirmed directly (linting a scratch file with this repo's own bundled config) that TypeScript's own JSDoc parser reads the converted mention as a REAL tag on the symbol, so a converted `is @deprecated upstream` makes every use of the symbol fail `@typescript-eslint/no-deprecated`, changing what the symbol means rather than merely its formatting. A mention wrapped in a backtick code span (`@deprecated` inside one) is exempt from both and converts safely, the backtick immediately before the `@` breaking the whitespace-preceded shape for TypeScript's tag scanning and `escape-inline-tags` alike (both confirmed directly). Withholding on every tag-shaped line, not merely a blank line adjacent to one, is deliberate: this rule has no way to enumerate in advance which further TSDoc-only tag eslint-plugin-jsdoc's own vocabulary will next disagree with, so it withholds structurally rather than adding a further one-off guard each time a new collision is found. `jsdoc.ts`'s own `check-tag-names`/TSDoc-vocabulary disagreement is itself a pre-existing gap on `main`, not caused by this rule, tracked separately. Requires no type information. Wired only through `stylisticCommentsConfig` into the default (type-checked) `exadevConfig()` output (see [Stylistic comment, class-member and JSX rules](#stylistic-comment-class-member-and-jsx-rules)), never registered in `plugin.configs.recommended`/`.barrel`, so a consumer using the lighter `extends: [plugin.configs.recommended]` option (see [The lighter option](#the-lighter-option-the-plugin-named-export)) never gets it. |
|
|
289
313
|
| `package-json-key-order` | ✓ | **Requires `package.json`'s keys to match `syncpack format`'s order.** See [Optional package.json key ordering](#optional-packagejson-key-ordering) — opt-in via `exadevConfig({ packageJsonKeyOrder: true })`, not part of `recommended`/`barrel`. A JSON-language rule (`@eslint/json`'s `json/json`), not a TSESLint one — needs no type information and doesn't apply to any `.ts`/`.js` file. |
|
|
290
314
|
| `no-uphill-dependency` | | **Enforces a configured workspace's rank, rank-skip, slice and group-isolation boundaries** on every `package.json`'s declared dependencies. See [Workspace architecture](#workspace-architecture). Opt-in via `exadevConfig({ workspaceArchitecture })` or the standalone `workspaceArchitectureConfig()`, not part of `recommended`/`barrel`. A JSON-language rule, needs no type information. |
|
|
291
315
|
| `no-dependency-cycle` | | **Disallows a workspace dependency that can reach back to the package declaring it.** A same-rank or same-slice dependency passes `no-uphill-dependency` while still forming a cycle, which this rule catches instead. See [Workspace architecture](#workspace-architecture). |
|
|
@@ -350,7 +374,7 @@ Both entry points need `@eslint/json` resolvable (`pnpm add -D @eslint/json`), t
|
|
|
350
374
|
| `root` | The workspace root directory. Defaults to the nearest ancestor of the linted file that owns a `pnpm-workspace.yaml`. |
|
|
351
375
|
| `packages` | Workspace package globs, matching pnpm's own `pnpm-workspace.yaml` glob support (`*`, `**`, `[...]` character classes, `{...}` brace expansion, `!`-prefixed excludes; a wildcard segment never matches a name starting with "."; a `./` prefix, a `.`/`..` segment, and a repeated slash normalise the same way `path.posix.normalize` would, so `./packages/*`, `packages//*` and `a/../packages/*` all select the same projects). Defaults to that file's own top-level `packages:` block sequence, quoted or bare. A positive pattern that currently matches no directory (an empty group awaiting its first member, say) resolves to no packages, the same as pnpm itself. |
|
|
352
376
|
| `dependencyFields` | `package.json` fields read as a package's declared dependencies. Defaults to `['dependencies']`. |
|
|
353
|
-
| `groups` | The workspace's own directory taxonomy: `{ name, path?, rank?, slice?, naming? }`. `path` defaults to `name` (a group rooted at a directory of the same name). A group's own `naming` (`'drop-group'` \| `'keep-group'` \| `'basename'`, default `'drop-group'`) selects which segments of a package's path (after the group's own root) `package-name-mirrors-path` expects its declared name to derive: `'drop-group'` keeps every remaining path segment
|
|
377
|
+
| `groups` | The workspace's own directory taxonomy: `{ name, path?, rank?, slice?, naming? }`. `path` defaults to `name` (a group rooted at a directory of the same name). A group's own `naming` (`'drop-group'` \| `'keep-group'` \| `'basename'`, default `'drop-group'`) selects which segments of a package's path (after the group's own root) `package-name-mirrors-path` expects its declared name to derive: `'drop-group'` keeps every remaining path segment, falling back to the group's own `name` when a package sits exactly at the group's own root (a standalone `docs` package under a `{ name: 'docs' }` group expects `docs`, never an empty or bare-scope name); `'keep-group'` additionally keeps the group's own `name` ahead of them, always, regardless of how deep its `path` nests (a group `{ name: 'test', path: 'packages/tests' }` expects `test-e2e` for `packages/tests/e2e`, never a path segment standing in for the name); `'basename'` keeps only the path's own final segment, dropping every intermediate directory. The joined segments are prefixed with `naming.scope` (when given) and separated with `naming.separator` (default `-`). |
|
|
354
378
|
| `nameRanks` | The name-role rank model: `{ pattern, rank }[]`, a package's declared name checked against each `pattern` in order, first match wins, ahead of its own group's `rank`. Omit entirely for a pure group-rank model. pnpm allows a workspace package to declare no `name` at all; such a package has nothing for a pattern to match, so `nameRanks` is skipped for it entirely and its rank falls straight through to its own group's `rank` or `defaultRank`. |
|
|
355
379
|
| `defaultRank` | The rank a package gets when neither `nameRanks` nor its own group resolves one. |
|
|
356
380
|
| `rankSkip` | `{ maxDistance, exemptRanks }`. A dependency more than `maxDistance` ranks below its dependant, and not itself one of `exemptRanks` (typically a foundational rank 0), is a `rankSkip` violation. Omitted entirely, that check never runs. |
|
|
@@ -361,18 +385,15 @@ A group's own `slice` (`{ segment: N }` or `{ namePrefix: true }`) partitions it
|
|
|
361
385
|
|
|
362
386
|
### Example: a group-ranked repo
|
|
363
387
|
|
|
364
|
-
Every group states its own `rank` directly; `test` keeps its own
|
|
388
|
+
Every group states its own `rank` directly; `test` keeps its own group name in the derived name (`@novus/test-database`, not `@novus/database`), which every other group drops:
|
|
365
389
|
|
|
366
390
|
```ts
|
|
367
391
|
// eslint.config.ts
|
|
368
392
|
import { defineConfig } from 'eslint/config';
|
|
369
|
-
import {
|
|
393
|
+
import { workspaceArchitectureConfig } from '@exadev/eslint-config';
|
|
370
394
|
|
|
371
395
|
export default defineConfig(
|
|
372
396
|
// ...your own config...
|
|
373
|
-
{
|
|
374
|
-
plugins: { exadev: plugin },
|
|
375
|
-
},
|
|
376
397
|
...workspaceArchitectureConfig({
|
|
377
398
|
groups: [
|
|
378
399
|
{ name: 'core', rank: 0 },
|
|
@@ -386,6 +407,8 @@ export default defineConfig(
|
|
|
386
407
|
);
|
|
387
408
|
```
|
|
388
409
|
|
|
410
|
+
`workspaceArchitectureConfig()`'s own returned config block already registers `plugins: { exadev: plugin }` itself (see its own source), so a consumer using only this standalone export never wires the plugin by hand; that manual line is needed only alongside the "lighter option" section above, where nothing else in the array registers the plugin.
|
|
411
|
+
|
|
389
412
|
### Example: a name-ranked repo
|
|
390
413
|
|
|
391
414
|
Rank comes from a package's own declared name, not its directory; `packages/targets` infers its slice from whichever feature/product vertical its own name is prefixed with, and `rankSkip` lets a target reach a rank-0 contract directly while still blocking it from skipping straight past `store-application-context` to an adapter:
|
|
@@ -446,6 +469,7 @@ pnpm install # requires Node >=20 and the pnpm version pinned in package.json
|
|
|
446
469
|
pnpm lint
|
|
447
470
|
pnpm typecheck
|
|
448
471
|
pnpm test
|
|
472
|
+
pnpm test:mutation # Stryker, 100% break threshold
|
|
449
473
|
pnpm build
|
|
450
474
|
```
|
|
451
475
|
|
|
@@ -468,6 +492,7 @@ pnpm build
|
|
|
468
492
|
- `recommended` ships `barrel-policy` at `mode: 'banned'`; `barrel` at `mode: 'single'`; `.react`/`.nextjs` call `buildReactConfig`/`buildNextjsConfig` with `enabled: true` (see below).
|
|
469
493
|
- [`src/config-types.ts`](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.
|
|
470
494
|
- *Why:* annotating a config array with the wider `ConfigValue` union directly broke `...exadev` with `TS2488` ("must have a Symbol.iterator method").
|
|
495
|
+
- It also holds `PublicConfigArray` (the default export's own `@eslint/core`-typed return shape) and `PublicPlugin` (the named `plugin` export's own boundary type, narrowing `configs` down to its four actual literal keys), the two public-boundary types [`src/to-public-config-array.ts`](src/to-public-config-array.ts)/[`src/to-public-plugin.ts`](src/to-public-plugin.ts) each cast into, isolated in their own single-cast modules for the reason each file's own comment gives.
|
|
471
496
|
- [`src/optional-plugin.ts`](src/optional-plugin.ts) is the lazy-resolution helper behind React/Next.js support.
|
|
472
497
|
- `tryRequire` wraps [`createRequire(import.meta.url)`](https://nodejs.org/api/module.html#modulecreaterequirefilename) in try/catch, returning `unknown` (never a cast) so every call site narrows explicitly before use.
|
|
473
498
|
- `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.
|
|
@@ -476,9 +501,17 @@ pnpm build
|
|
|
476
501
|
- `[]` if unresolvable and not explicitly forced on; a thrown `Error` if explicitly forced on (`enabled: true`) and still unresolvable.
|
|
477
502
|
- `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).
|
|
478
503
|
- [`src/create-config.ts`](src/create-config.ts) is config assembly's single source of truth.
|
|
479
|
-
- `exadevConfig(options, ...userConfigs)` concatenates `recommendedTypeChecked`
|
|
504
|
+
- `exadevConfig(options, ...userConfigs)` concatenates, in order: `buildGitignoreConfig`, `recommendedTypeChecked`, `jsdocAndTsdoc`, `jsonCanonicalConfig`, `stylisticCommentsConfig`, `buildReactConfig`, `buildNextjsConfig`, `buildPackageJsonKeyOrderConfig` (each tri-state builder fed its matching option), `buildWorkspaceArchitectureConfig` (only when `workspaceArchitecture` is given; there is no auto-detected default), and any trailing user configs.
|
|
480
505
|
- `defaultConfig` is `exadevConfig()` evaluated once, eagerly, at module load.
|
|
481
|
-
- [`src/
|
|
506
|
+
- [`src/stylistic-comments.ts`](src/stylistic-comments.ts) builds `stylisticCommentsConfig`: the hand-picked `@stylistic/eslint-plugin` rules plus this package's own `exadev/prefer-doc-comment`, in two blocks (one scoped to every JS/TS file, one scoped to JSX files only for the three JSX-specific rules). See [Stylistic comment, class-member and JSX rules](#stylistic-comment-class-member-and-jsx-rules).
|
|
507
|
+
- [`src/index.ts`](src/index.ts) is the entry point, still a pure re-export barrel:
|
|
508
|
+
```ts
|
|
509
|
+
export { defaultConfig as default, exadevConfig } from './create-config';
|
|
510
|
+
export { publicPlugin as plugin } from './plugin';
|
|
511
|
+
export { workspaceArchitectureConfig } from './workspace-architecture';
|
|
512
|
+
export type { GroupSpec, NamingOptions, RankRule, RankSkipOptions, SliceSpec, WorkspaceArchitectureOptions } from './rules/workspace-options';
|
|
513
|
+
```
|
|
514
|
+
- The named export is `publicPlugin`, not `plugin`'s own internal default export: `plugin` (src/plugin.ts) is typed against `@typescript-eslint/utils`' `TSESLint.FlatConfig.Plugin` for full rule-option checking while this package assembles it, then cast once, at the very end of that same file, to `PublicPlugin` (see `src/to-public-plugin.ts`) before re-export, mirroring how `defaultConfig` is built against the internal `ConfigArrayValue` and cast to `PublicConfigArray` at `exadevConfig`'s own boundary.
|
|
482
515
|
- Required by `no-side-effects-in-index`/`no-non-barrel-reexport`, both of which assume this file contains nothing but `export ... from ...`.
|
|
483
516
|
- 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).
|
|
484
517
|
- 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.
|
|
@@ -503,7 +536,7 @@ pnpm build
|
|
|
503
536
|
|
|
504
537
|
### Contributing
|
|
505
538
|
|
|
506
|
-
Conventional commits are enforced by a husky `commit-msg` hook and re-checked in CI. CI runs commitlint, lint,
|
|
539
|
+
Conventional commits are enforced by a husky `commit-msg` hook and re-checked in CI. CI runs commitlint, lint, typecheck+test+build+attw, and mutation testing (Stryker) on every pull request, on push to `main`, and on manual dispatch; the release job runs only on `main` (push or manual dispatch), after all four jobs pass.
|
|
507
540
|
|
|
508
541
|
### Release
|
|
509
542
|
|