exadev-eslint-config 2.22.0 → 2.22.2
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 +23 -10
- package/dist/index.cjs +76 -77
- package/dist/index.d.cts +9 -15
- package/dist/index.d.ts +9 -15
- package/dist/index.js +76 -77
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,6 +213,11 @@ React support pairs `eslint-plugin-react`'s `flat/recommended` with its own `fla
|
|
|
213
213
|
plugins: { exadev: plugin },
|
|
214
214
|
extends: [plugin.configs.react], // throws if eslint-plugin-react isn't installed
|
|
215
215
|
},
|
|
216
|
+
{
|
|
217
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
218
|
+
plugins: { exadev: plugin },
|
|
219
|
+
extends: [plugin.configs.nextjs], // throws if @next/eslint-plugin-next isn't installed
|
|
220
|
+
},
|
|
216
221
|
);
|
|
217
222
|
```
|
|
218
223
|
- **`exadevConfig({ react, nextjs })`** — see the tri-state table below.
|
|
@@ -350,7 +355,7 @@ Both entry points need `@eslint/json` resolvable (`pnpm add -D @eslint/json`), t
|
|
|
350
355
|
| `root` | The workspace root directory. Defaults to the nearest ancestor of the linted file that owns a `pnpm-workspace.yaml`. |
|
|
351
356
|
| `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
357
|
| `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
|
|
358
|
+
| `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
359
|
| `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
360
|
| `defaultRank` | The rank a package gets when neither `nameRanks` nor its own group resolves one. |
|
|
356
361
|
| `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 +366,15 @@ A group's own `slice` (`{ segment: N }` or `{ namePrefix: true }`) partitions it
|
|
|
361
366
|
|
|
362
367
|
### Example: a group-ranked repo
|
|
363
368
|
|
|
364
|
-
Every group states its own `rank` directly; `test` keeps its own
|
|
369
|
+
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
370
|
|
|
366
371
|
```ts
|
|
367
372
|
// eslint.config.ts
|
|
368
373
|
import { defineConfig } from 'eslint/config';
|
|
369
|
-
import {
|
|
374
|
+
import { workspaceArchitectureConfig } from '@exadev/eslint-config';
|
|
370
375
|
|
|
371
376
|
export default defineConfig(
|
|
372
377
|
// ...your own config...
|
|
373
|
-
{
|
|
374
|
-
plugins: { exadev: plugin },
|
|
375
|
-
},
|
|
376
378
|
...workspaceArchitectureConfig({
|
|
377
379
|
groups: [
|
|
378
380
|
{ name: 'core', rank: 0 },
|
|
@@ -386,6 +388,8 @@ export default defineConfig(
|
|
|
386
388
|
);
|
|
387
389
|
```
|
|
388
390
|
|
|
391
|
+
`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.
|
|
392
|
+
|
|
389
393
|
### Example: a name-ranked repo
|
|
390
394
|
|
|
391
395
|
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:
|
|
@@ -442,10 +446,11 @@ export default exadevConfig({
|
|
|
442
446
|
### Build, test, and lint
|
|
443
447
|
|
|
444
448
|
```sh
|
|
445
|
-
pnpm install # requires Node >=20 and pnpm
|
|
449
|
+
pnpm install # requires Node >=20 and the pnpm version pinned in package.json's packageManager field
|
|
446
450
|
pnpm lint
|
|
447
451
|
pnpm typecheck
|
|
448
452
|
pnpm test
|
|
453
|
+
pnpm test:mutation # Stryker, 100% break threshold
|
|
449
454
|
pnpm build
|
|
450
455
|
```
|
|
451
456
|
|
|
@@ -468,6 +473,7 @@ pnpm build
|
|
|
468
473
|
- `recommended` ships `barrel-policy` at `mode: 'banned'`; `barrel` at `mode: 'single'`; `.react`/`.nextjs` call `buildReactConfig`/`buildNextjsConfig` with `enabled: true` (see below).
|
|
469
474
|
- [`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
475
|
- *Why:* annotating a config array with the wider `ConfigValue` union directly broke `...exadev` with `TS2488` ("must have a Symbol.iterator method").
|
|
476
|
+
- 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
477
|
- [`src/optional-plugin.ts`](src/optional-plugin.ts) is the lazy-resolution helper behind React/Next.js support.
|
|
472
478
|
- `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
479
|
- `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 +482,16 @@ pnpm build
|
|
|
476
482
|
- `[]` if unresolvable and not explicitly forced on; a thrown `Error` if explicitly forced on (`enabled: true`) and still unresolvable.
|
|
477
483
|
- `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
484
|
- [`src/create-config.ts`](src/create-config.ts) is config assembly's single source of truth.
|
|
479
|
-
- `exadevConfig(options, ...userConfigs)` concatenates `recommendedTypeChecked`
|
|
485
|
+
- `exadevConfig(options, ...userConfigs)` concatenates, in order: `buildGitignoreConfig`, `recommendedTypeChecked`, `jsdocAndTsdoc`, `jsonCanonicalConfig`, `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
486
|
- `defaultConfig` is `exadevConfig()` evaluated once, eagerly, at module load.
|
|
481
|
-
- [`src/index.ts`](src/index.ts) is the entry point, still a pure re-export barrel:
|
|
487
|
+
- [`src/index.ts`](src/index.ts) is the entry point, still a pure re-export barrel:
|
|
488
|
+
```ts
|
|
489
|
+
export { defaultConfig as default, exadevConfig } from './create-config';
|
|
490
|
+
export { publicPlugin as plugin } from './plugin';
|
|
491
|
+
export { workspaceArchitectureConfig } from './workspace-architecture';
|
|
492
|
+
export type { GroupSpec, NamingOptions, RankRule, RankSkipOptions, SliceSpec, WorkspaceArchitectureOptions } from './rules/workspace-options';
|
|
493
|
+
```
|
|
494
|
+
- 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
495
|
- Required by `no-side-effects-in-index`/`no-non-barrel-reexport`, both of which assume this file contains nothing but `export ... from ...`.
|
|
483
496
|
- 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
497
|
- 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 +516,7 @@ pnpm build
|
|
|
503
516
|
|
|
504
517
|
### Contributing
|
|
505
518
|
|
|
506
|
-
Conventional commits are enforced by a husky `commit-msg` hook and re-checked in CI. CI runs commitlint, lint,
|
|
519
|
+
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
520
|
|
|
508
521
|
### Release
|
|
509
522
|
|
package/dist/index.cjs
CHANGED
|
@@ -251,6 +251,11 @@ const jsonCanonicalConfig = [
|
|
|
251
251
|
}
|
|
252
252
|
];
|
|
253
253
|
//#endregion
|
|
254
|
+
//#region src/is-record.ts
|
|
255
|
+
function isRecord(value) {
|
|
256
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
254
259
|
//#region src/optional-plugin.ts
|
|
255
260
|
const nodeRequire = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
256
261
|
function tryRequire(specifier, requireFn = nodeRequire) {
|
|
@@ -260,13 +265,10 @@ function tryRequire(specifier, requireFn = nodeRequire) {
|
|
|
260
265
|
return;
|
|
261
266
|
}
|
|
262
267
|
}
|
|
263
|
-
function isRecord$4(value) {
|
|
264
|
-
return typeof value === "object" && value !== null;
|
|
265
|
-
}
|
|
266
268
|
function normalizeLegacyParserOptions(record) {
|
|
267
269
|
if (!("parserOptions" in record)) return record;
|
|
268
270
|
const { parserOptions, languageOptions, ...rest } = record;
|
|
269
|
-
const existingLanguageOptions = isRecord
|
|
271
|
+
const existingLanguageOptions = isRecord(languageOptions) ? languageOptions : {};
|
|
270
272
|
return {
|
|
271
273
|
...rest,
|
|
272
274
|
languageOptions: {
|
|
@@ -278,10 +280,10 @@ function normalizeLegacyParserOptions(record) {
|
|
|
278
280
|
function readFlatConfig(module, path) {
|
|
279
281
|
let current = module;
|
|
280
282
|
for (const key of path) {
|
|
281
|
-
if (!isRecord
|
|
283
|
+
if (!isRecord(current)) return void 0;
|
|
282
284
|
current = current[key];
|
|
283
285
|
}
|
|
284
|
-
if (!isRecord
|
|
286
|
+
if (!isRecord(current)) return void 0;
|
|
285
287
|
return normalizeLegacyParserOptions(current);
|
|
286
288
|
}
|
|
287
289
|
//#endregion
|
|
@@ -295,21 +297,18 @@ function buildNextjsConfig(options = {}) {
|
|
|
295
297
|
}
|
|
296
298
|
//#endregion
|
|
297
299
|
//#region src/json-plugin.ts
|
|
298
|
-
function isRecord$3(value) {
|
|
299
|
-
return typeof value === "object" && value !== null;
|
|
300
|
-
}
|
|
301
300
|
function isJsonLanguagePlugin(value) {
|
|
302
|
-
if (!isRecord
|
|
301
|
+
if (!isRecord(value)) return false;
|
|
303
302
|
const languages = value["languages"];
|
|
304
|
-
return isRecord
|
|
303
|
+
return isRecord(languages) && "json" in languages;
|
|
305
304
|
}
|
|
306
305
|
function resolveJsonPlugin(value) {
|
|
307
|
-
if (isRecord
|
|
306
|
+
if (isRecord(value) && isJsonLanguagePlugin(value["default"])) return value["default"];
|
|
308
307
|
return isJsonLanguagePlugin(value) ? value : void 0;
|
|
309
308
|
}
|
|
310
309
|
//#endregion
|
|
311
310
|
//#region package.json
|
|
312
|
-
var version = "2.22.
|
|
311
|
+
var version = "2.22.2";
|
|
313
312
|
//#endregion
|
|
314
313
|
//#region src/react.ts
|
|
315
314
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
@@ -348,6 +347,11 @@ function buildReactConfig(options = {}) {
|
|
|
348
347
|
}));
|
|
349
348
|
}
|
|
350
349
|
//#endregion
|
|
350
|
+
//#region src/to-public-plugin.ts
|
|
351
|
+
function toPublicPlugin(built) {
|
|
352
|
+
return built;
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
351
355
|
//#region src/rules/barrel-helpers.ts
|
|
352
356
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
353
357
|
function basenameOf(filename) {
|
|
@@ -486,13 +490,10 @@ const barrelDirectSiblingsOnly = {
|
|
|
486
490
|
};
|
|
487
491
|
//#endregion
|
|
488
492
|
//#region src/rules/barrel-auto-detect.ts
|
|
489
|
-
function isRecord$2(value) {
|
|
490
|
-
return typeof value === "object" && value !== null;
|
|
491
|
-
}
|
|
492
493
|
function hasRealExports(value) {
|
|
493
494
|
if (typeof value === "string") return value.length > 0;
|
|
494
495
|
if (Array.isArray(value)) return value.length > 0;
|
|
495
|
-
return isRecord
|
|
496
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
496
497
|
}
|
|
497
498
|
function decideAutoBarrelMode(packageJson) {
|
|
498
499
|
const hasExports = hasRealExports(packageJson["exports"]);
|
|
@@ -509,7 +510,7 @@ function findNearestPackageJson(startDir) {
|
|
|
509
510
|
const candidate = (0, node_path.join)(dir, "package.json");
|
|
510
511
|
if ((0, node_fs.existsSync)(candidate)) {
|
|
511
512
|
const parsed = JSON.parse((0, node_fs.readFileSync)(candidate, "utf8"));
|
|
512
|
-
const result = isRecord
|
|
513
|
+
const result = isRecord(parsed) ? parsed : void 0;
|
|
513
514
|
packageJsonByStartDir.set(startDir, result);
|
|
514
515
|
return result;
|
|
515
516
|
}
|
|
@@ -838,20 +839,20 @@ function listSubdirectories(fs, dir) {
|
|
|
838
839
|
function splitPathSegments(path) {
|
|
839
840
|
return path.split("/").filter((segment) => segment.length > 0);
|
|
840
841
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
/** Reads `text[index]`, throwing rather than reading past the end: every call site in this file only ever derives `index` from a `for` loop whose own condition (`index < text.length`) already guarantees it in range, so an out-of-bounds read here would mean that loop's own invariant broke, not a case to handle quietly. Exported so this throw (unreachable through every real call site) can be tested directly, the same "Unreachable, tested directly rather than trusted on a comment" shape workspace-yaml.ts's own `requireLine` establishes. */
|
|
844
|
-
function requireChar$1(text, index) {
|
|
842
|
+
/** Reads `text[index]`, throwing rather than reading past the end: every real call site in workspace-yaml.ts and workspace-glob.ts derives `index` from a `for` loop whose own condition already guarantees it in range, so an out-of-bounds read here would mean that loop's own invariant broke, not a case to handle quietly. Shared by both modules. Exported so this throw (unreachable through every real call site) can be tested directly, the same "Unreachable, tested directly rather than trusted on a comment" shape package-json-key-order.ts's own `at()` helper establishes. */
|
|
843
|
+
function requireChar(text, index) {
|
|
845
844
|
const char = text[index];
|
|
846
845
|
if (char === void 0) throw new Error(`Unreachable: index ${String(index)} is out of bounds for a string of length ${String(text.length)}.`);
|
|
847
846
|
return char;
|
|
848
847
|
}
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/rules/workspace-glob.ts
|
|
849
850
|
const REGEXP_SPECIAL_CHARS = /[.+^${}()|\]\\]/gu;
|
|
850
851
|
const ESCAPE_ANY_REGEXP_CHAR = /[.*+?^${}()|[\]\\]/gu;
|
|
851
852
|
function segmentToRegExp(segment) {
|
|
852
853
|
let source = "";
|
|
853
854
|
for (let index = 0; index < segment.length;) {
|
|
854
|
-
const char = requireChar
|
|
855
|
+
const char = requireChar(segment, index);
|
|
855
856
|
if (char === "\\") {
|
|
856
857
|
const next = segment[index + 1];
|
|
857
858
|
if (next === void 0) {
|
|
@@ -919,7 +920,7 @@ function walkPattern(fs, root, segments, matchedSoFar) {
|
|
|
919
920
|
function findMatchingBrace(pattern, openIndex) {
|
|
920
921
|
let depth = 0;
|
|
921
922
|
for (let index = openIndex; index < pattern.length; index += 1) {
|
|
922
|
-
const char = requireChar
|
|
923
|
+
const char = requireChar(pattern, index);
|
|
923
924
|
if (char === "{") depth += 1;
|
|
924
925
|
else if (char === "}") {
|
|
925
926
|
depth -= 1;
|
|
@@ -933,7 +934,7 @@ function splitTopLevelAlternatives(text) {
|
|
|
933
934
|
let depth = 0;
|
|
934
935
|
let start = 0;
|
|
935
936
|
for (let index = 0; index < text.length; index += 1) {
|
|
936
|
-
const char = requireChar
|
|
937
|
+
const char = requireChar(text, index);
|
|
937
938
|
if (char === "{") depth += 1;
|
|
938
939
|
else if (char === "}") depth -= 1;
|
|
939
940
|
else if (char === "," && depth === 0) {
|
|
@@ -1020,12 +1021,6 @@ function requireMatch(pattern, text) {
|
|
|
1020
1021
|
if (match === null) throw new Error(`Unreachable: /${pattern.source}/ was already confirmed to match "${text}".`);
|
|
1021
1022
|
return match;
|
|
1022
1023
|
}
|
|
1023
|
-
/** Reads `text[index]`, throwing rather than reading past the end: both real call sites below derive `index` from a `for` loop whose own condition (`index < text.length`) already guarantees it in range, so an out-of-bounds read here would mean that loop's own invariant broke, not a case to handle quietly. A genuinely maybe-out-of-bounds neighbour check (this file's own "the character right before/after this one, if any" reads) stays a plain `charAt`, deliberately: those are never loop-bound-guaranteed and a missing neighbour is a real, legitimate outcome, not a bug. Exported for the same direct-test reason as requireLine above. */
|
|
1024
|
-
function requireChar(text, index) {
|
|
1025
|
-
const char = text[index];
|
|
1026
|
-
if (char === void 0) throw new Error(`Unreachable: index ${String(index)} is out of bounds for a string of length ${String(text.length)}.`);
|
|
1027
|
-
return char;
|
|
1028
|
-
}
|
|
1029
1024
|
/** Whether `value[index]` is a quote character, returning that exact character (not merely `true`) so a caller can go on to search for the SAME quote style's own matching close. Exported for direct testing of its own per-index decision, independent of commentSearchStart's own real-string scenarios below. */
|
|
1030
1025
|
function quoteCharAt(value, index) {
|
|
1031
1026
|
const char = value.charAt(index);
|
|
@@ -1074,7 +1069,7 @@ function readWorkspacePackages(yamlText) {
|
|
|
1074
1069
|
if (keyLineIndex === -1) return [];
|
|
1075
1070
|
const keyLine = requireLine(lines, keyLineIndex);
|
|
1076
1071
|
const inlineWithoutComment = stripComment(requireCapture(requireMatch(PACKAGES_KEY_PATTERN, keyLine), 1));
|
|
1077
|
-
if (inlineWithoutComment.length > 0) throw new Error(
|
|
1072
|
+
if (inlineWithoutComment.length > 0) throw new Error(`@exadev/eslint-config: pnpm-workspace.yaml's "packages:" key is written in flow style ("${inlineWithoutComment}"), which this package's minimal reader does not support. Rewrite it as a block sequence (one "- 'glob'" per line), or pass the "packages" rule option explicitly.`);
|
|
1078
1073
|
const items = [];
|
|
1079
1074
|
let sequenceIndent;
|
|
1080
1075
|
for (let index = keyLineIndex + 1; index < lines.length; index += 1) {
|
|
@@ -1082,7 +1077,7 @@ function readWorkspacePackages(yamlText) {
|
|
|
1082
1077
|
if (line.trim().length === 0 || line.trim().startsWith("#")) continue;
|
|
1083
1078
|
const itemMatch = SEQUENCE_ITEM_PATTERN.exec(line);
|
|
1084
1079
|
if (itemMatch === null) {
|
|
1085
|
-
if (items.length === 0) throw new Error(
|
|
1080
|
+
if (items.length === 0) throw new Error(`@exadev/eslint-config: pnpm-workspace.yaml's "packages:" key is present but its next line ("${line.trim()}") is not a block-sequence item ("- 'glob'"). This package's minimal reader only supports a block sequence; rewrite it, or pass the "packages" rule option explicitly.`);
|
|
1086
1081
|
break;
|
|
1087
1082
|
}
|
|
1088
1083
|
const indent = requireCapture(itemMatch, 1);
|
|
@@ -1214,9 +1209,6 @@ const MISCONFIGURATION_MESSAGE = "exadev workspace architecture rules require op
|
|
|
1214
1209
|
function fail() {
|
|
1215
1210
|
throw new Error(MISCONFIGURATION_MESSAGE);
|
|
1216
1211
|
}
|
|
1217
|
-
function isRecord$1(value) {
|
|
1218
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1219
|
-
}
|
|
1220
1212
|
const TOP_LEVEL_KEYS = [
|
|
1221
1213
|
"root",
|
|
1222
1214
|
"packages",
|
|
@@ -1265,7 +1257,7 @@ function isNonNegativeInteger(value) {
|
|
|
1265
1257
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
1266
1258
|
}
|
|
1267
1259
|
function isSliceSpec(value) {
|
|
1268
|
-
if (!isRecord
|
|
1260
|
+
if (!isRecord(value)) return false;
|
|
1269
1261
|
if ("segment" in value) return hasOnlyKeys(value, SLICE_BY_SEGMENT_KEYS) && isNonNegativeInteger(value["segment"]);
|
|
1270
1262
|
return hasOnlyKeys(value, SLICE_BY_NAME_PREFIX_KEYS) && value["namePrefix"] === true;
|
|
1271
1263
|
}
|
|
@@ -1273,7 +1265,7 @@ function isNamingStrategy(value) {
|
|
|
1273
1265
|
return value === "drop-group" || value === "keep-group" || value === "basename";
|
|
1274
1266
|
}
|
|
1275
1267
|
function isGroupSpec(value) {
|
|
1276
|
-
if (!isRecord
|
|
1268
|
+
if (!isRecord(value)) return false;
|
|
1277
1269
|
if (!hasOnlyKeys(value, GROUP_KEYS)) return false;
|
|
1278
1270
|
if (typeof value["name"] !== "string") return false;
|
|
1279
1271
|
const { path, rank, slice, naming } = value;
|
|
@@ -1298,7 +1290,7 @@ function findDuplicateGroupName(groups) {
|
|
|
1298
1290
|
}
|
|
1299
1291
|
}
|
|
1300
1292
|
function isRankRule(value) {
|
|
1301
|
-
return isRecord
|
|
1293
|
+
return isRecord(value) && hasOnlyKeys(value, RANK_RULE_KEYS) && typeof value["pattern"] === "string" && isInteger(value["rank"]);
|
|
1302
1294
|
}
|
|
1303
1295
|
function validateRankRulePatterns(rankRules) {
|
|
1304
1296
|
for (const rule of rankRules) try {
|
|
@@ -1315,7 +1307,7 @@ function asOptionalRankRuleArray(value) {
|
|
|
1315
1307
|
return value;
|
|
1316
1308
|
}
|
|
1317
1309
|
function isRankSkipOptions(value) {
|
|
1318
|
-
if (!isRecord
|
|
1310
|
+
if (!isRecord(value)) return false;
|
|
1319
1311
|
if (!hasOnlyKeys(value, RANK_SKIP_KEYS)) return false;
|
|
1320
1312
|
if (!isNonNegativeInteger(value["maxDistance"])) return false;
|
|
1321
1313
|
const { exemptRanks } = value;
|
|
@@ -1335,7 +1327,7 @@ function asOptionalIsolatedGroups(value) {
|
|
|
1335
1327
|
return value;
|
|
1336
1328
|
}
|
|
1337
1329
|
function isNamingOptions(value) {
|
|
1338
|
-
if (!isRecord
|
|
1330
|
+
if (!isRecord(value)) return false;
|
|
1339
1331
|
if (!hasOnlyKeys(value, NAMING_KEYS)) return false;
|
|
1340
1332
|
const { scope, separator } = value;
|
|
1341
1333
|
if (scope !== void 0 && typeof scope !== "string") return false;
|
|
@@ -1350,7 +1342,7 @@ function asOptionalNaming(value) {
|
|
|
1350
1342
|
* The runtime safety net behind workspaceArchitectureOptionsSchema above: ESLint's own schema validation does run whenever the config is used in a real lint (rejecting malformed rule options there too, the same division of labour barrel-policy.ts's readMode establishes for its own, much smaller options shape), but workspaceArchitectureConfig() (src/workspace-architecture.ts) builds its rule options by calling this reader on the caller's raw object BEFORE that validation ever inspects it, and previously reconstructed a whitelisted object that silently dropped any unknown or misspelled top-level key rather than rejecting it, leaving ESLint's own schema nothing left to catch. This reader now rejects an unknown key at every level it validates (top level, group, slice, rankSkip, naming, nameRanks entries) itself, so a genuinely malformed options object still fails loudly and specifically here, whichever entry point it arrives through, rather than crashing later with a confusing TypeError deep inside graph construction or being silently ignored.
|
|
1351
1343
|
*/
|
|
1352
1344
|
function readWorkspaceArchitectureOptions(options) {
|
|
1353
|
-
if (!isRecord
|
|
1345
|
+
if (!isRecord(options)) fail();
|
|
1354
1346
|
if (!hasOnlyKeys(options, TOP_LEVEL_KEYS)) fail();
|
|
1355
1347
|
const groups = asGroupSpecArray(options["groups"]);
|
|
1356
1348
|
const duplicateGroupName = findDuplicateGroupName(groups);
|
|
@@ -1437,9 +1429,6 @@ function sliceByNamePrefix(declaredName, knownSlices) {
|
|
|
1437
1429
|
const unscoped = stripScope(declaredName);
|
|
1438
1430
|
return [...knownSlices].filter((candidate) => unscoped === candidate || unscoped.startsWith(`${candidate}-`)).sort((a, b) => b.length - a.length)[0];
|
|
1439
1431
|
}
|
|
1440
|
-
function isRecord(value) {
|
|
1441
|
-
return typeof value === "object" && value !== null;
|
|
1442
|
-
}
|
|
1443
1432
|
/**
|
|
1444
1433
|
* Reads a candidate package directory's own package.json directly (plain JSON.parse, not momoa): this is data collection for the graph, not a file being linted, so no AST/location information is needed. No existsSync guard here: every relativeDir this is called with came from resolveWorkspacePackageDirs, which already only returns directories that own a real package.json, so its absence here would mean that guarantee broke, not a case to handle quietly. A manifest that is not a usable JSON object at all (parses to an array, a string, null) returns undefined: nothing this graph can identify a package by, regardless of any "name" field. A manifest that IS a usable object but declares no "name" (or a non-string one) is different: pnpm allows a workspace package with no declared name at all, so its own `name` comes back as undefined rather than dropping the whole manifest, letting collectCandidates below key such a package by its directory instead of silently skipping it (and every dependency it declares along with it).
|
|
1445
1434
|
*
|
|
@@ -1507,6 +1496,7 @@ function collectCandidates(fs, root, options) {
|
|
|
1507
1496
|
const relativeDirs = resolveWorkspacePackageDirs(fs, root, resolveWorkspacePackagePatterns(fs, root, options.packages));
|
|
1508
1497
|
const candidates = [];
|
|
1509
1498
|
for (const relativeDir of relativeDirs) {
|
|
1499
|
+
if (relativeDir === "") continue;
|
|
1510
1500
|
const group = findOwningGroup(relativeDir, options.groups);
|
|
1511
1501
|
if (group === void 0) throw new Error(`@exadev/eslint-config: workspace package directory "${relativeDir}" (matched by the "packages" globs) is not covered by any configured group's own "path". Add a "groups" entry whose path prefixes it, or narrow "packages" to exclude it: an uncovered package would otherwise be silently skipped by every workspace-architecture check.`);
|
|
1512
1502
|
const manifest = readDeclaredManifest(fs, (0, node_path.join)(root, relativeDir), resolveDependencyFields(options));
|
|
@@ -1684,14 +1674,14 @@ function last(array) {
|
|
|
1684
1674
|
/**
|
|
1685
1675
|
* The three per-group naming strategies' own segment selection, keyed by NamingStrategy's own three literal members rather than a chain of ternaries: `Record<NamingStrategy, ...>` requires every member to have its own entry, which is what makes 'drop-group' a genuine, independently-typed branch, not merely "whatever the ternary chain falls through to when nothing else matched". Stryker's own typescript checker rejects a mutant that replaces the `?? 'drop-group'` fallback with some other string, since indexing this record with a value outside NamingStrategy is a type error, caught before any test even runs.
|
|
1686
1676
|
*
|
|
1687
|
-
* - **'drop-group'**: drop the group's own root path segments, keep what remains.
|
|
1688
|
-
* - **'keep-group'**: keep the group's OWN name
|
|
1677
|
+
* - **'drop-group'**: drop the group's own root path segments, keep what remains, falling back to the group's own `name` when nothing remains: a package sitting exactly at its group's own root (`relativeDir` equals the group's `path`, a group that IS a single package rather than a container of them, a standalone `docs` package under a `{ name: 'docs' }` group, say) has no path segments left of its own for `rest` to keep, so the group's declared identity stands in for them, exactly as it always does for 'keep-group'. Without this, such a group could never have a satisfiable expected name at all: every package in it would derive an empty (or bare-scope) name no real `package.json` can declare.
|
|
1678
|
+
* - **'keep-group'**: keep the group's OWN `name` ahead of `rest`, always, regardless of how deep its own `path` nests: a group declared `{ name: 'test', path: 'tests' }` derives "test-e2e" for `tests/e2e`, and one declared `{ name: 'test', path: 'packages/tests' }` derives "test-e2e" for `packages/tests/e2e` too, never "tests-e2e" (the group's declared identity, not whatever path segment sits above it). A test group whose packages are named "test-<feature>", mirroring the feature they test, needs its own "test" name kept, regardless of how many container directories its `path` nests under.
|
|
1689
1679
|
* - **'basename'**: use only `relativeDir`'s own final segment, ignoring every intermediate directory, for a group whose intermediate structure exists purely for filesystem organisation and carries no naming intent of its own.
|
|
1690
1680
|
*/
|
|
1691
1681
|
const NAME_SEGMENTS_BY_STRATEGY = {
|
|
1692
1682
|
basename: ({ segments }) => [last(segments)],
|
|
1693
|
-
"keep-group": ({ rest,
|
|
1694
|
-
"drop-group": ({ rest }) => rest
|
|
1683
|
+
"keep-group": ({ rest, group }) => [group.name, ...rest],
|
|
1684
|
+
"drop-group": ({ rest, group }) => rest.length === 0 ? [group.name] : rest
|
|
1695
1685
|
};
|
|
1696
1686
|
/**
|
|
1697
1687
|
* The package name package-name-mirrors-path expects a package at `relativeDir` (workspace-root-relative, forward-slash-joined) to declare, given its own matched group and the workspace's global naming options. See NAME_SEGMENTS_BY_STRATEGY above for the three per-group strategies' own segment selection. The joined segments are prefixed with `naming.scope` (when given) and joined to it with "/"; segments themselves join with `naming.separator` (default "-").
|
|
@@ -1701,15 +1691,27 @@ function expectedPackageName(relativeDir, group, naming) {
|
|
|
1701
1691
|
const segments = splitPathSegments(relativeDir);
|
|
1702
1692
|
const groupPathSegments = splitPathSegments(group.path ?? group.name);
|
|
1703
1693
|
const rest = segments.slice(groupPathSegments.length);
|
|
1704
|
-
const groupNameSegment = last(groupPathSegments);
|
|
1705
1694
|
const joined = NAME_SEGMENTS_BY_STRATEGY[group.naming ?? "drop-group"]({
|
|
1706
1695
|
segments,
|
|
1707
1696
|
rest,
|
|
1708
|
-
|
|
1697
|
+
group
|
|
1709
1698
|
}).join(separator);
|
|
1710
1699
|
return naming.scope === void 0 ? joined : `${naming.scope}/${joined}`;
|
|
1711
1700
|
}
|
|
1712
1701
|
//#endregion
|
|
1702
|
+
//#region src/rules/json-member-key.ts
|
|
1703
|
+
function isStringNamed(name) {
|
|
1704
|
+
return name.type === "String";
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Shared by package-json-key-order.ts and workspace-json-helpers.ts: momoa's own MemberNode.name is typed StringNode | IdentifierNode because momoa's grammar also covers JSON5 (unquoted identifier keys), but every rule that calls this only ever sets meta.languages to json/json or json/jsonc, where an unquoted key is a genuine parse error, never a value this function is asked to name. Exported so that guarantee is checked directly against a deliberately IdentifierNode-shaped input, rather than trusted on the strength of this comment alone.
|
|
1708
|
+
*/
|
|
1709
|
+
function getMemberKeyName(member) {
|
|
1710
|
+
const { name } = member;
|
|
1711
|
+
if (!isStringNamed(name)) throw new Error(`Unreachable: JSON member names are only read under json/json or json/jsonc, where an unquoted (Identifier) member name is a parse error, got a "${name.type}" name instead.`);
|
|
1712
|
+
return name.value;
|
|
1713
|
+
}
|
|
1714
|
+
//#endregion
|
|
1713
1715
|
//#region src/rules/workspace-json-helpers.ts
|
|
1714
1716
|
function isStringNode$1(node) {
|
|
1715
1717
|
return node.type === "String";
|
|
@@ -1717,7 +1719,7 @@ function isStringNode$1(node) {
|
|
|
1717
1719
|
/** package.json's own declared "name" field, read directly off a confirmed-top-level Object node. Returns undefined for a manifest with no "name" at all, or one whose value is not a plain string (a shape other JSON tooling would already flag elsewhere, not this rule's concern). */
|
|
1718
1720
|
function readDeclaredName(rootObject) {
|
|
1719
1721
|
for (const member of rootObject.members) {
|
|
1720
|
-
if (
|
|
1722
|
+
if (getMemberKeyName(member) !== "name") continue;
|
|
1721
1723
|
if (!isStringNode$1(member.value)) return void 0;
|
|
1722
1724
|
return {
|
|
1723
1725
|
name: member.value.value,
|
|
@@ -1732,15 +1734,12 @@ function collectTopLevelDependencies(rootObject, dependencyFields) {
|
|
|
1732
1734
|
const fields = new Set(dependencyFields);
|
|
1733
1735
|
const results = [];
|
|
1734
1736
|
for (const member of rootObject.members) {
|
|
1735
|
-
if (!
|
|
1737
|
+
if (!fields.has(getMemberKeyName(member))) continue;
|
|
1736
1738
|
if (member.value.type !== "Object") continue;
|
|
1737
|
-
for (const dependency of member.value.members) {
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
node: dependency
|
|
1742
|
-
});
|
|
1743
|
-
}
|
|
1739
|
+
for (const dependency of member.value.members) results.push({
|
|
1740
|
+
name: getMemberKeyName(dependency),
|
|
1741
|
+
node: dependency
|
|
1742
|
+
});
|
|
1744
1743
|
}
|
|
1745
1744
|
return results;
|
|
1746
1745
|
}
|
|
@@ -1763,7 +1762,7 @@ function createNoDependencyCycleRule(deps = {}) {
|
|
|
1763
1762
|
description: "Disallow a cyclic workspace dependency.",
|
|
1764
1763
|
url: "https://github.com/ExaDev/eslint-config/blob/main/src/rules/no-dependency-cycle.ts"
|
|
1765
1764
|
},
|
|
1766
|
-
messages: { cycle: "\"{{from}}\" depends on \"{{to}}\", which
|
|
1765
|
+
messages: { cycle: "\"{{from}}\" depends on \"{{to}}\", which can reach back to \"{{from}}\": a workspace cycle. Move the shared code into a package both can depend on, or invert one edge behind a contract." }
|
|
1767
1766
|
},
|
|
1768
1767
|
create(context) {
|
|
1769
1768
|
const options = readWorkspaceArchitectureOptions(context.options[0]);
|
|
@@ -2595,14 +2594,6 @@ function isIdentityPermutation(permutation) {
|
|
|
2595
2594
|
function isStringNode(node) {
|
|
2596
2595
|
return node.type === "String";
|
|
2597
2596
|
}
|
|
2598
|
-
function isStringNamed(name) {
|
|
2599
|
-
return name.type === "String";
|
|
2600
|
-
}
|
|
2601
|
-
function getMemberKeyName(member) {
|
|
2602
|
-
const { name } = member;
|
|
2603
|
-
if (!isStringNamed(name)) throw new Error(`Unreachable: package-json-key-order only supports json/json and json/jsonc, where an unquoted (Identifier) member name is a parse error — got a "${name.type}" name instead.`);
|
|
2604
|
-
return name.value;
|
|
2605
|
-
}
|
|
2606
2597
|
function rangeOf(node) {
|
|
2607
2598
|
if (node.range === void 0) throw new Error("Unreachable: every node reaching this function was produced by a JSON language that always requests range tracking.");
|
|
2608
2599
|
return node.range;
|
|
@@ -2854,11 +2845,11 @@ const FUNCTION_LIKE_SELECTOR$2 = [
|
|
|
2854
2845
|
function isOptionalParam(param) {
|
|
2855
2846
|
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.TSParameterProperty) return isOptionalParam(param.parameter);
|
|
2856
2847
|
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.AssignmentPattern) return true;
|
|
2857
|
-
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return param.optional;
|
|
2848
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier || param.type === _typescript_eslint_utils.AST_NODE_TYPES.ObjectPattern || param.type === _typescript_eslint_utils.AST_NODE_TYPES.ArrayPattern) return param.optional;
|
|
2858
2849
|
return false;
|
|
2859
2850
|
}
|
|
2860
2851
|
function getTrailingOptionalRun(params) {
|
|
2861
|
-
const hasTrailingRest = params
|
|
2852
|
+
const hasTrailingRest = params[params.length - 1]?.type === _typescript_eslint_utils.AST_NODE_TYPES.RestElement;
|
|
2862
2853
|
const run = [];
|
|
2863
2854
|
for (let i = params.length - (hasTrailingRest ? 2 : 1); i >= 0; i--) {
|
|
2864
2855
|
const param = params[i];
|
|
@@ -2868,7 +2859,6 @@ function getTrailingOptionalRun(params) {
|
|
|
2868
2859
|
return run;
|
|
2869
2860
|
}
|
|
2870
2861
|
function resolveFixableParam(param) {
|
|
2871
|
-
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.TSParameterProperty) return void 0;
|
|
2872
2862
|
const identifierNode = param.type === _typescript_eslint_utils.AST_NODE_TYPES.AssignmentPattern ? param.left.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier ? param.left : void 0 : param.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier ? param : void 0;
|
|
2873
2863
|
if (identifierNode === void 0) return void 0;
|
|
2874
2864
|
const typeNode = identifierNode.typeAnnotation?.typeAnnotation;
|
|
@@ -2962,7 +2952,7 @@ const preferOptionsObjectParam = createRule$2({
|
|
|
2962
2952
|
const allResolvable = resolvedParams.length === run.length;
|
|
2963
2953
|
const hasRestParam = node.params.some((param) => param.type === _typescript_eslint_utils.AST_NODE_TYPES.RestElement);
|
|
2964
2954
|
const body = getBlockBody(node);
|
|
2965
|
-
let isFixable = allResolvable && !hasRestParam
|
|
2955
|
+
let isFixable = allResolvable && !hasRestParam;
|
|
2966
2956
|
if (isFixable) {
|
|
2967
2957
|
const jsdocComment = getLeadingJSDocComment(sourceCode, node);
|
|
2968
2958
|
const jsDocBail = jsdocComment !== void 0 && resolvedParams.some((info) => jsDocMentionsParam(jsdocComment.value, info.identifierNode.name));
|
|
@@ -3305,6 +3295,7 @@ const plugin = {
|
|
|
3305
3295
|
}
|
|
3306
3296
|
}
|
|
3307
3297
|
};
|
|
3298
|
+
const publicPlugin = toPublicPlugin(plugin);
|
|
3308
3299
|
//#endregion
|
|
3309
3300
|
//#region src/package-json-key-order.ts
|
|
3310
3301
|
const SYNCPACK_CONFIG_FILENAMES = [
|
|
@@ -3448,11 +3439,13 @@ function toPublicConfigArray(built) {
|
|
|
3448
3439
|
//#endregion
|
|
3449
3440
|
//#region src/workspace-architecture.ts
|
|
3450
3441
|
/**
|
|
3451
|
-
* Wires the three workspace-architecture rules (no-uphill-dependency, no-dependency-cycle, and, when the shared `naming` option is given, package-name-mirrors-path) onto `**\/package.json`, all three sharing the one options object given here.
|
|
3442
|
+
* Wires the three workspace-architecture rules (no-uphill-dependency, no-dependency-cycle, and, when the shared `naming` option is given, package-name-mirrors-path) onto `**\/package.json`, all three sharing the one options object given here. Internal: consumed by create-config.ts as one more `ConfigArrayValue` entry among the rest of `exadevConfig`'s own internally-typed build (see recommendedTypeChecked, buildReactConfig, buildNextjsConfig et al. for the same shape), before that whole array is converted once at `exadevConfig`'s own outer boundary. Never exported directly; a standalone consumer wants `workspaceArchitectureConfig` below instead.
|
|
3452
3443
|
*
|
|
3453
3444
|
* Unlike buildPackageJsonKeyOrderConfig, this has no auto-detecting tri-state: workspace architecture rules require real configuration (`groups` has no sensible default), so this is off unless a consumer calls it at all, and, once called, always requires `@eslint/json` to be resolvable (there is no "silently do nothing" case to fall back to the way an unconfigured, purely auto-detected feature can).
|
|
3445
|
+
*
|
|
3446
|
+
* Takes `WorkspaceArchitectureConfigOptions` (the `requireFn` test seam included) rather than the public `workspaceArchitectureConfig` export's own `WorkspaceArchitectureOptions`, so this internal builder's own tests (workspace-architecture.unit.test.ts) can still simulate an unresolvable `@eslint/json` directly, without the seam ever reaching the public function's parameter type or dist/index.d.ts.
|
|
3454
3447
|
*/
|
|
3455
|
-
function
|
|
3448
|
+
function buildWorkspaceArchitectureConfig(options) {
|
|
3456
3449
|
const { requireFn, ...ruleOptions } = options;
|
|
3457
3450
|
const validated = readWorkspaceArchitectureOptions(ruleOptions);
|
|
3458
3451
|
const jsonPlugin = resolveJsonPlugin(tryRequire("@eslint/json", requireFn));
|
|
@@ -3471,6 +3464,12 @@ function workspaceArchitectureConfig(options) {
|
|
|
3471
3464
|
}
|
|
3472
3465
|
}];
|
|
3473
3466
|
}
|
|
3467
|
+
/**
|
|
3468
|
+
* Wires the three workspace-architecture rules (`no-uphill-dependency`, `no-dependency-cycle`, and, when the shared `naming` option is given, `package-name-mirrors-path`) onto `**\/package.json` from one `WorkspaceArchitectureOptions` object, returning ESLint core's own `Config[]` so the result spreads directly into `defineConfig(...)` (`...workspaceArchitectureConfig(options)`, the README's own standalone usage) alongside a consumer's own configuration.
|
|
3469
|
+
*/
|
|
3470
|
+
function workspaceArchitectureConfig(options) {
|
|
3471
|
+
return toPublicConfigArray(buildWorkspaceArchitectureConfig(options));
|
|
3472
|
+
}
|
|
3474
3473
|
//#endregion
|
|
3475
3474
|
//#region src/create-config.ts
|
|
3476
3475
|
function exadevConfig(options = {}, ...userConfigs) {
|
|
@@ -3482,7 +3481,7 @@ function exadevConfig(options = {}, ...userConfigs) {
|
|
|
3482
3481
|
...buildReactConfig({ enabled: options.react }),
|
|
3483
3482
|
...buildNextjsConfig({ enabled: options.nextjs }),
|
|
3484
3483
|
...buildPackageJsonKeyOrderConfig({ enabled: options.packageJsonKeyOrder }),
|
|
3485
|
-
...options.workspaceArchitecture !== void 0 ?
|
|
3484
|
+
...options.workspaceArchitecture !== void 0 ? buildWorkspaceArchitectureConfig(options.workspaceArchitecture) : [],
|
|
3486
3485
|
...userConfigs
|
|
3487
3486
|
]);
|
|
3488
3487
|
}
|
|
@@ -3490,5 +3489,5 @@ const defaultConfig = exadevConfig();
|
|
|
3490
3489
|
//#endregion
|
|
3491
3490
|
exports.default = defaultConfig;
|
|
3492
3491
|
exports.exadevConfig = exadevConfig;
|
|
3493
|
-
exports.plugin =
|
|
3492
|
+
exports.plugin = publicPlugin;
|
|
3494
3493
|
exports.workspaceArchitectureConfig = workspaceArchitectureConfig;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
-
import { Config } from "eslint/config";
|
|
2
|
+
import { Config, ConfigObject } from "eslint/config";
|
|
3
3
|
//#region src/config-types.d.ts
|
|
4
|
-
type ConfigValue = NonNullable<TSESLint.FlatConfig.Plugin['configs']>[string];
|
|
5
|
-
type ConfigArrayValue = Extract<ConfigValue, unknown[]>;
|
|
6
4
|
type PublicConfigArray = Config[];
|
|
5
|
+
type PublicConfigObject = ConfigObject | ConfigObject[];
|
|
6
|
+
type PublicPlugin = Omit<NonNullable<ConfigObject['plugins']>[string], 'configs'> & {
|
|
7
|
+
readonly configs: Readonly<Record<'recommended' | 'barrel' | 'react' | 'nextjs', PublicConfigObject>>;
|
|
8
|
+
};
|
|
7
9
|
//#endregion
|
|
8
10
|
//#region src/rules/workspace-options.d.ts
|
|
9
11
|
interface SliceBySegment {
|
|
@@ -57,20 +59,12 @@ declare function exadevConfig(options?: ExadevConfigOptions, ...userConfigs: rea
|
|
|
57
59
|
declare const defaultConfig: PublicConfigArray;
|
|
58
60
|
//#endregion
|
|
59
61
|
//#region src/plugin.d.ts
|
|
60
|
-
declare const
|
|
61
|
-
//#endregion
|
|
62
|
-
//#region src/optional-plugin.d.ts
|
|
63
|
-
type RequireFn = (specifier: string) => unknown;
|
|
62
|
+
declare const publicPlugin: PublicPlugin;
|
|
64
63
|
//#endregion
|
|
65
64
|
//#region src/workspace-architecture.d.ts
|
|
66
|
-
interface WorkspaceArchitectureConfigOptions extends WorkspaceArchitectureOptions {
|
|
67
|
-
readonly requireFn?: RequireFn;
|
|
68
|
-
}
|
|
69
65
|
/**
|
|
70
|
-
* Wires the three workspace-architecture rules (no-uphill-dependency
|
|
71
|
-
*
|
|
72
|
-
* Unlike buildPackageJsonKeyOrderConfig, this has no auto-detecting tri-state: workspace architecture rules require real configuration (`groups` has no sensible default), so this is off unless a consumer calls it at all, and, once called, always requires `@eslint/json` to be resolvable (there is no "silently do nothing" case to fall back to the way an unconfigured, purely auto-detected feature can).
|
|
66
|
+
* Wires the three workspace-architecture rules (`no-uphill-dependency`, `no-dependency-cycle`, and, when the shared `naming` option is given, `package-name-mirrors-path`) onto `**\/package.json` from one `WorkspaceArchitectureOptions` object, returning ESLint core's own `Config[]` so the result spreads directly into `defineConfig(...)` (`...workspaceArchitectureConfig(options)`, the README's own standalone usage) alongside a consumer's own configuration.
|
|
73
67
|
*/
|
|
74
|
-
declare function workspaceArchitectureConfig(options:
|
|
68
|
+
declare function workspaceArchitectureConfig(options: WorkspaceArchitectureOptions): PublicConfigArray;
|
|
75
69
|
//#endregion
|
|
76
|
-
export { type GroupSpec, type NamingOptions, type RankRule, type RankSkipOptions, type SliceSpec, type WorkspaceArchitectureOptions, defaultConfig as default, exadevConfig, plugin, workspaceArchitectureConfig };
|
|
70
|
+
export { type GroupSpec, type NamingOptions, type RankRule, type RankSkipOptions, type SliceSpec, type WorkspaceArchitectureOptions, defaultConfig as default, exadevConfig, publicPlugin as plugin, workspaceArchitectureConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
-
import { Config } from "eslint/config";
|
|
2
|
+
import { Config, ConfigObject } from "eslint/config";
|
|
3
3
|
//#region src/config-types.d.ts
|
|
4
|
-
type ConfigValue = NonNullable<TSESLint.FlatConfig.Plugin['configs']>[string];
|
|
5
|
-
type ConfigArrayValue = Extract<ConfigValue, unknown[]>;
|
|
6
4
|
type PublicConfigArray = Config[];
|
|
5
|
+
type PublicConfigObject = ConfigObject | ConfigObject[];
|
|
6
|
+
type PublicPlugin = Omit<NonNullable<ConfigObject['plugins']>[string], 'configs'> & {
|
|
7
|
+
readonly configs: Readonly<Record<'recommended' | 'barrel' | 'react' | 'nextjs', PublicConfigObject>>;
|
|
8
|
+
};
|
|
7
9
|
//#endregion
|
|
8
10
|
//#region src/rules/workspace-options.d.ts
|
|
9
11
|
interface SliceBySegment {
|
|
@@ -57,20 +59,12 @@ declare function exadevConfig(options?: ExadevConfigOptions, ...userConfigs: rea
|
|
|
57
59
|
declare const defaultConfig: PublicConfigArray;
|
|
58
60
|
//#endregion
|
|
59
61
|
//#region src/plugin.d.ts
|
|
60
|
-
declare const
|
|
61
|
-
//#endregion
|
|
62
|
-
//#region src/optional-plugin.d.ts
|
|
63
|
-
type RequireFn = (specifier: string) => unknown;
|
|
62
|
+
declare const publicPlugin: PublicPlugin;
|
|
64
63
|
//#endregion
|
|
65
64
|
//#region src/workspace-architecture.d.ts
|
|
66
|
-
interface WorkspaceArchitectureConfigOptions extends WorkspaceArchitectureOptions {
|
|
67
|
-
readonly requireFn?: RequireFn;
|
|
68
|
-
}
|
|
69
65
|
/**
|
|
70
|
-
* Wires the three workspace-architecture rules (no-uphill-dependency
|
|
71
|
-
*
|
|
72
|
-
* Unlike buildPackageJsonKeyOrderConfig, this has no auto-detecting tri-state: workspace architecture rules require real configuration (`groups` has no sensible default), so this is off unless a consumer calls it at all, and, once called, always requires `@eslint/json` to be resolvable (there is no "silently do nothing" case to fall back to the way an unconfigured, purely auto-detected feature can).
|
|
66
|
+
* Wires the three workspace-architecture rules (`no-uphill-dependency`, `no-dependency-cycle`, and, when the shared `naming` option is given, `package-name-mirrors-path`) onto `**\/package.json` from one `WorkspaceArchitectureOptions` object, returning ESLint core's own `Config[]` so the result spreads directly into `defineConfig(...)` (`...workspaceArchitectureConfig(options)`, the README's own standalone usage) alongside a consumer's own configuration.
|
|
73
67
|
*/
|
|
74
|
-
declare function workspaceArchitectureConfig(options:
|
|
68
|
+
declare function workspaceArchitectureConfig(options: WorkspaceArchitectureOptions): PublicConfigArray;
|
|
75
69
|
//#endregion
|
|
76
|
-
export { type GroupSpec, type NamingOptions, type RankRule, type RankSkipOptions, type SliceSpec, type WorkspaceArchitectureOptions, defaultConfig as default, exadevConfig, plugin, workspaceArchitectureConfig };
|
|
70
|
+
export { type GroupSpec, type NamingOptions, type RankRule, type RankSkipOptions, type SliceSpec, type WorkspaceArchitectureOptions, defaultConfig as default, exadevConfig, publicPlugin as plugin, workspaceArchitectureConfig };
|
package/dist/index.js
CHANGED
|
@@ -217,6 +217,11 @@ const jsonCanonicalConfig = [
|
|
|
217
217
|
}
|
|
218
218
|
];
|
|
219
219
|
//#endregion
|
|
220
|
+
//#region src/is-record.ts
|
|
221
|
+
function isRecord(value) {
|
|
222
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
220
225
|
//#region src/optional-plugin.ts
|
|
221
226
|
const nodeRequire = createRequire(import.meta.url);
|
|
222
227
|
function tryRequire(specifier, requireFn = nodeRequire) {
|
|
@@ -226,13 +231,10 @@ function tryRequire(specifier, requireFn = nodeRequire) {
|
|
|
226
231
|
return;
|
|
227
232
|
}
|
|
228
233
|
}
|
|
229
|
-
function isRecord$4(value) {
|
|
230
|
-
return typeof value === "object" && value !== null;
|
|
231
|
-
}
|
|
232
234
|
function normalizeLegacyParserOptions(record) {
|
|
233
235
|
if (!("parserOptions" in record)) return record;
|
|
234
236
|
const { parserOptions, languageOptions, ...rest } = record;
|
|
235
|
-
const existingLanguageOptions = isRecord
|
|
237
|
+
const existingLanguageOptions = isRecord(languageOptions) ? languageOptions : {};
|
|
236
238
|
return {
|
|
237
239
|
...rest,
|
|
238
240
|
languageOptions: {
|
|
@@ -244,10 +246,10 @@ function normalizeLegacyParserOptions(record) {
|
|
|
244
246
|
function readFlatConfig(module, path) {
|
|
245
247
|
let current = module;
|
|
246
248
|
for (const key of path) {
|
|
247
|
-
if (!isRecord
|
|
249
|
+
if (!isRecord(current)) return void 0;
|
|
248
250
|
current = current[key];
|
|
249
251
|
}
|
|
250
|
-
if (!isRecord
|
|
252
|
+
if (!isRecord(current)) return void 0;
|
|
251
253
|
return normalizeLegacyParserOptions(current);
|
|
252
254
|
}
|
|
253
255
|
//#endregion
|
|
@@ -261,21 +263,18 @@ function buildNextjsConfig(options = {}) {
|
|
|
261
263
|
}
|
|
262
264
|
//#endregion
|
|
263
265
|
//#region src/json-plugin.ts
|
|
264
|
-
function isRecord$3(value) {
|
|
265
|
-
return typeof value === "object" && value !== null;
|
|
266
|
-
}
|
|
267
266
|
function isJsonLanguagePlugin(value) {
|
|
268
|
-
if (!isRecord
|
|
267
|
+
if (!isRecord(value)) return false;
|
|
269
268
|
const languages = value["languages"];
|
|
270
|
-
return isRecord
|
|
269
|
+
return isRecord(languages) && "json" in languages;
|
|
271
270
|
}
|
|
272
271
|
function resolveJsonPlugin(value) {
|
|
273
|
-
if (isRecord
|
|
272
|
+
if (isRecord(value) && isJsonLanguagePlugin(value["default"])) return value["default"];
|
|
274
273
|
return isJsonLanguagePlugin(value) ? value : void 0;
|
|
275
274
|
}
|
|
276
275
|
//#endregion
|
|
277
276
|
//#region package.json
|
|
278
|
-
var version = "2.22.
|
|
277
|
+
var version = "2.22.2";
|
|
279
278
|
//#endregion
|
|
280
279
|
//#region src/react.ts
|
|
281
280
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
@@ -314,6 +313,11 @@ function buildReactConfig(options = {}) {
|
|
|
314
313
|
}));
|
|
315
314
|
}
|
|
316
315
|
//#endregion
|
|
316
|
+
//#region src/to-public-plugin.ts
|
|
317
|
+
function toPublicPlugin(built) {
|
|
318
|
+
return built;
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
317
321
|
//#region src/rules/barrel-helpers.ts
|
|
318
322
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
319
323
|
function basenameOf(filename) {
|
|
@@ -452,13 +456,10 @@ const barrelDirectSiblingsOnly = {
|
|
|
452
456
|
};
|
|
453
457
|
//#endregion
|
|
454
458
|
//#region src/rules/barrel-auto-detect.ts
|
|
455
|
-
function isRecord$2(value) {
|
|
456
|
-
return typeof value === "object" && value !== null;
|
|
457
|
-
}
|
|
458
459
|
function hasRealExports(value) {
|
|
459
460
|
if (typeof value === "string") return value.length > 0;
|
|
460
461
|
if (Array.isArray(value)) return value.length > 0;
|
|
461
|
-
return isRecord
|
|
462
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
462
463
|
}
|
|
463
464
|
function decideAutoBarrelMode(packageJson) {
|
|
464
465
|
const hasExports = hasRealExports(packageJson["exports"]);
|
|
@@ -475,7 +476,7 @@ function findNearestPackageJson(startDir) {
|
|
|
475
476
|
const candidate = join(dir, "package.json");
|
|
476
477
|
if (existsSync(candidate)) {
|
|
477
478
|
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
478
|
-
const result = isRecord
|
|
479
|
+
const result = isRecord(parsed) ? parsed : void 0;
|
|
479
480
|
packageJsonByStartDir.set(startDir, result);
|
|
480
481
|
return result;
|
|
481
482
|
}
|
|
@@ -804,20 +805,20 @@ function listSubdirectories(fs, dir) {
|
|
|
804
805
|
function splitPathSegments(path) {
|
|
805
806
|
return path.split("/").filter((segment) => segment.length > 0);
|
|
806
807
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
/** Reads `text[index]`, throwing rather than reading past the end: every call site in this file only ever derives `index` from a `for` loop whose own condition (`index < text.length`) already guarantees it in range, so an out-of-bounds read here would mean that loop's own invariant broke, not a case to handle quietly. Exported so this throw (unreachable through every real call site) can be tested directly, the same "Unreachable, tested directly rather than trusted on a comment" shape workspace-yaml.ts's own `requireLine` establishes. */
|
|
810
|
-
function requireChar$1(text, index) {
|
|
808
|
+
/** Reads `text[index]`, throwing rather than reading past the end: every real call site in workspace-yaml.ts and workspace-glob.ts derives `index` from a `for` loop whose own condition already guarantees it in range, so an out-of-bounds read here would mean that loop's own invariant broke, not a case to handle quietly. Shared by both modules. Exported so this throw (unreachable through every real call site) can be tested directly, the same "Unreachable, tested directly rather than trusted on a comment" shape package-json-key-order.ts's own `at()` helper establishes. */
|
|
809
|
+
function requireChar(text, index) {
|
|
811
810
|
const char = text[index];
|
|
812
811
|
if (char === void 0) throw new Error(`Unreachable: index ${String(index)} is out of bounds for a string of length ${String(text.length)}.`);
|
|
813
812
|
return char;
|
|
814
813
|
}
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/rules/workspace-glob.ts
|
|
815
816
|
const REGEXP_SPECIAL_CHARS = /[.+^${}()|\]\\]/gu;
|
|
816
817
|
const ESCAPE_ANY_REGEXP_CHAR = /[.*+?^${}()|[\]\\]/gu;
|
|
817
818
|
function segmentToRegExp(segment) {
|
|
818
819
|
let source = "";
|
|
819
820
|
for (let index = 0; index < segment.length;) {
|
|
820
|
-
const char = requireChar
|
|
821
|
+
const char = requireChar(segment, index);
|
|
821
822
|
if (char === "\\") {
|
|
822
823
|
const next = segment[index + 1];
|
|
823
824
|
if (next === void 0) {
|
|
@@ -885,7 +886,7 @@ function walkPattern(fs, root, segments, matchedSoFar) {
|
|
|
885
886
|
function findMatchingBrace(pattern, openIndex) {
|
|
886
887
|
let depth = 0;
|
|
887
888
|
for (let index = openIndex; index < pattern.length; index += 1) {
|
|
888
|
-
const char = requireChar
|
|
889
|
+
const char = requireChar(pattern, index);
|
|
889
890
|
if (char === "{") depth += 1;
|
|
890
891
|
else if (char === "}") {
|
|
891
892
|
depth -= 1;
|
|
@@ -899,7 +900,7 @@ function splitTopLevelAlternatives(text) {
|
|
|
899
900
|
let depth = 0;
|
|
900
901
|
let start = 0;
|
|
901
902
|
for (let index = 0; index < text.length; index += 1) {
|
|
902
|
-
const char = requireChar
|
|
903
|
+
const char = requireChar(text, index);
|
|
903
904
|
if (char === "{") depth += 1;
|
|
904
905
|
else if (char === "}") depth -= 1;
|
|
905
906
|
else if (char === "," && depth === 0) {
|
|
@@ -986,12 +987,6 @@ function requireMatch(pattern, text) {
|
|
|
986
987
|
if (match === null) throw new Error(`Unreachable: /${pattern.source}/ was already confirmed to match "${text}".`);
|
|
987
988
|
return match;
|
|
988
989
|
}
|
|
989
|
-
/** Reads `text[index]`, throwing rather than reading past the end: both real call sites below derive `index` from a `for` loop whose own condition (`index < text.length`) already guarantees it in range, so an out-of-bounds read here would mean that loop's own invariant broke, not a case to handle quietly. A genuinely maybe-out-of-bounds neighbour check (this file's own "the character right before/after this one, if any" reads) stays a plain `charAt`, deliberately: those are never loop-bound-guaranteed and a missing neighbour is a real, legitimate outcome, not a bug. Exported for the same direct-test reason as requireLine above. */
|
|
990
|
-
function requireChar(text, index) {
|
|
991
|
-
const char = text[index];
|
|
992
|
-
if (char === void 0) throw new Error(`Unreachable: index ${String(index)} is out of bounds for a string of length ${String(text.length)}.`);
|
|
993
|
-
return char;
|
|
994
|
-
}
|
|
995
990
|
/** Whether `value[index]` is a quote character, returning that exact character (not merely `true`) so a caller can go on to search for the SAME quote style's own matching close. Exported for direct testing of its own per-index decision, independent of commentSearchStart's own real-string scenarios below. */
|
|
996
991
|
function quoteCharAt(value, index) {
|
|
997
992
|
const char = value.charAt(index);
|
|
@@ -1040,7 +1035,7 @@ function readWorkspacePackages(yamlText) {
|
|
|
1040
1035
|
if (keyLineIndex === -1) return [];
|
|
1041
1036
|
const keyLine = requireLine(lines, keyLineIndex);
|
|
1042
1037
|
const inlineWithoutComment = stripComment(requireCapture(requireMatch(PACKAGES_KEY_PATTERN, keyLine), 1));
|
|
1043
|
-
if (inlineWithoutComment.length > 0) throw new Error(
|
|
1038
|
+
if (inlineWithoutComment.length > 0) throw new Error(`@exadev/eslint-config: pnpm-workspace.yaml's "packages:" key is written in flow style ("${inlineWithoutComment}"), which this package's minimal reader does not support. Rewrite it as a block sequence (one "- 'glob'" per line), or pass the "packages" rule option explicitly.`);
|
|
1044
1039
|
const items = [];
|
|
1045
1040
|
let sequenceIndent;
|
|
1046
1041
|
for (let index = keyLineIndex + 1; index < lines.length; index += 1) {
|
|
@@ -1048,7 +1043,7 @@ function readWorkspacePackages(yamlText) {
|
|
|
1048
1043
|
if (line.trim().length === 0 || line.trim().startsWith("#")) continue;
|
|
1049
1044
|
const itemMatch = SEQUENCE_ITEM_PATTERN.exec(line);
|
|
1050
1045
|
if (itemMatch === null) {
|
|
1051
|
-
if (items.length === 0) throw new Error(
|
|
1046
|
+
if (items.length === 0) throw new Error(`@exadev/eslint-config: pnpm-workspace.yaml's "packages:" key is present but its next line ("${line.trim()}") is not a block-sequence item ("- 'glob'"). This package's minimal reader only supports a block sequence; rewrite it, or pass the "packages" rule option explicitly.`);
|
|
1052
1047
|
break;
|
|
1053
1048
|
}
|
|
1054
1049
|
const indent = requireCapture(itemMatch, 1);
|
|
@@ -1180,9 +1175,6 @@ const MISCONFIGURATION_MESSAGE = "exadev workspace architecture rules require op
|
|
|
1180
1175
|
function fail() {
|
|
1181
1176
|
throw new Error(MISCONFIGURATION_MESSAGE);
|
|
1182
1177
|
}
|
|
1183
|
-
function isRecord$1(value) {
|
|
1184
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1185
|
-
}
|
|
1186
1178
|
const TOP_LEVEL_KEYS = [
|
|
1187
1179
|
"root",
|
|
1188
1180
|
"packages",
|
|
@@ -1231,7 +1223,7 @@ function isNonNegativeInteger(value) {
|
|
|
1231
1223
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
1232
1224
|
}
|
|
1233
1225
|
function isSliceSpec(value) {
|
|
1234
|
-
if (!isRecord
|
|
1226
|
+
if (!isRecord(value)) return false;
|
|
1235
1227
|
if ("segment" in value) return hasOnlyKeys(value, SLICE_BY_SEGMENT_KEYS) && isNonNegativeInteger(value["segment"]);
|
|
1236
1228
|
return hasOnlyKeys(value, SLICE_BY_NAME_PREFIX_KEYS) && value["namePrefix"] === true;
|
|
1237
1229
|
}
|
|
@@ -1239,7 +1231,7 @@ function isNamingStrategy(value) {
|
|
|
1239
1231
|
return value === "drop-group" || value === "keep-group" || value === "basename";
|
|
1240
1232
|
}
|
|
1241
1233
|
function isGroupSpec(value) {
|
|
1242
|
-
if (!isRecord
|
|
1234
|
+
if (!isRecord(value)) return false;
|
|
1243
1235
|
if (!hasOnlyKeys(value, GROUP_KEYS)) return false;
|
|
1244
1236
|
if (typeof value["name"] !== "string") return false;
|
|
1245
1237
|
const { path, rank, slice, naming } = value;
|
|
@@ -1264,7 +1256,7 @@ function findDuplicateGroupName(groups) {
|
|
|
1264
1256
|
}
|
|
1265
1257
|
}
|
|
1266
1258
|
function isRankRule(value) {
|
|
1267
|
-
return isRecord
|
|
1259
|
+
return isRecord(value) && hasOnlyKeys(value, RANK_RULE_KEYS) && typeof value["pattern"] === "string" && isInteger(value["rank"]);
|
|
1268
1260
|
}
|
|
1269
1261
|
function validateRankRulePatterns(rankRules) {
|
|
1270
1262
|
for (const rule of rankRules) try {
|
|
@@ -1281,7 +1273,7 @@ function asOptionalRankRuleArray(value) {
|
|
|
1281
1273
|
return value;
|
|
1282
1274
|
}
|
|
1283
1275
|
function isRankSkipOptions(value) {
|
|
1284
|
-
if (!isRecord
|
|
1276
|
+
if (!isRecord(value)) return false;
|
|
1285
1277
|
if (!hasOnlyKeys(value, RANK_SKIP_KEYS)) return false;
|
|
1286
1278
|
if (!isNonNegativeInteger(value["maxDistance"])) return false;
|
|
1287
1279
|
const { exemptRanks } = value;
|
|
@@ -1301,7 +1293,7 @@ function asOptionalIsolatedGroups(value) {
|
|
|
1301
1293
|
return value;
|
|
1302
1294
|
}
|
|
1303
1295
|
function isNamingOptions(value) {
|
|
1304
|
-
if (!isRecord
|
|
1296
|
+
if (!isRecord(value)) return false;
|
|
1305
1297
|
if (!hasOnlyKeys(value, NAMING_KEYS)) return false;
|
|
1306
1298
|
const { scope, separator } = value;
|
|
1307
1299
|
if (scope !== void 0 && typeof scope !== "string") return false;
|
|
@@ -1316,7 +1308,7 @@ function asOptionalNaming(value) {
|
|
|
1316
1308
|
* The runtime safety net behind workspaceArchitectureOptionsSchema above: ESLint's own schema validation does run whenever the config is used in a real lint (rejecting malformed rule options there too, the same division of labour barrel-policy.ts's readMode establishes for its own, much smaller options shape), but workspaceArchitectureConfig() (src/workspace-architecture.ts) builds its rule options by calling this reader on the caller's raw object BEFORE that validation ever inspects it, and previously reconstructed a whitelisted object that silently dropped any unknown or misspelled top-level key rather than rejecting it, leaving ESLint's own schema nothing left to catch. This reader now rejects an unknown key at every level it validates (top level, group, slice, rankSkip, naming, nameRanks entries) itself, so a genuinely malformed options object still fails loudly and specifically here, whichever entry point it arrives through, rather than crashing later with a confusing TypeError deep inside graph construction or being silently ignored.
|
|
1317
1309
|
*/
|
|
1318
1310
|
function readWorkspaceArchitectureOptions(options) {
|
|
1319
|
-
if (!isRecord
|
|
1311
|
+
if (!isRecord(options)) fail();
|
|
1320
1312
|
if (!hasOnlyKeys(options, TOP_LEVEL_KEYS)) fail();
|
|
1321
1313
|
const groups = asGroupSpecArray(options["groups"]);
|
|
1322
1314
|
const duplicateGroupName = findDuplicateGroupName(groups);
|
|
@@ -1403,9 +1395,6 @@ function sliceByNamePrefix(declaredName, knownSlices) {
|
|
|
1403
1395
|
const unscoped = stripScope(declaredName);
|
|
1404
1396
|
return [...knownSlices].filter((candidate) => unscoped === candidate || unscoped.startsWith(`${candidate}-`)).sort((a, b) => b.length - a.length)[0];
|
|
1405
1397
|
}
|
|
1406
|
-
function isRecord(value) {
|
|
1407
|
-
return typeof value === "object" && value !== null;
|
|
1408
|
-
}
|
|
1409
1398
|
/**
|
|
1410
1399
|
* Reads a candidate package directory's own package.json directly (plain JSON.parse, not momoa): this is data collection for the graph, not a file being linted, so no AST/location information is needed. No existsSync guard here: every relativeDir this is called with came from resolveWorkspacePackageDirs, which already only returns directories that own a real package.json, so its absence here would mean that guarantee broke, not a case to handle quietly. A manifest that is not a usable JSON object at all (parses to an array, a string, null) returns undefined: nothing this graph can identify a package by, regardless of any "name" field. A manifest that IS a usable object but declares no "name" (or a non-string one) is different: pnpm allows a workspace package with no declared name at all, so its own `name` comes back as undefined rather than dropping the whole manifest, letting collectCandidates below key such a package by its directory instead of silently skipping it (and every dependency it declares along with it).
|
|
1411
1400
|
*
|
|
@@ -1473,6 +1462,7 @@ function collectCandidates(fs, root, options) {
|
|
|
1473
1462
|
const relativeDirs = resolveWorkspacePackageDirs(fs, root, resolveWorkspacePackagePatterns(fs, root, options.packages));
|
|
1474
1463
|
const candidates = [];
|
|
1475
1464
|
for (const relativeDir of relativeDirs) {
|
|
1465
|
+
if (relativeDir === "") continue;
|
|
1476
1466
|
const group = findOwningGroup(relativeDir, options.groups);
|
|
1477
1467
|
if (group === void 0) throw new Error(`@exadev/eslint-config: workspace package directory "${relativeDir}" (matched by the "packages" globs) is not covered by any configured group's own "path". Add a "groups" entry whose path prefixes it, or narrow "packages" to exclude it: an uncovered package would otherwise be silently skipped by every workspace-architecture check.`);
|
|
1478
1468
|
const manifest = readDeclaredManifest(fs, join(root, relativeDir), resolveDependencyFields(options));
|
|
@@ -1650,14 +1640,14 @@ function last(array) {
|
|
|
1650
1640
|
/**
|
|
1651
1641
|
* The three per-group naming strategies' own segment selection, keyed by NamingStrategy's own three literal members rather than a chain of ternaries: `Record<NamingStrategy, ...>` requires every member to have its own entry, which is what makes 'drop-group' a genuine, independently-typed branch, not merely "whatever the ternary chain falls through to when nothing else matched". Stryker's own typescript checker rejects a mutant that replaces the `?? 'drop-group'` fallback with some other string, since indexing this record with a value outside NamingStrategy is a type error, caught before any test even runs.
|
|
1652
1642
|
*
|
|
1653
|
-
* - **'drop-group'**: drop the group's own root path segments, keep what remains.
|
|
1654
|
-
* - **'keep-group'**: keep the group's OWN name
|
|
1643
|
+
* - **'drop-group'**: drop the group's own root path segments, keep what remains, falling back to the group's own `name` when nothing remains: a package sitting exactly at its group's own root (`relativeDir` equals the group's `path`, a group that IS a single package rather than a container of them, a standalone `docs` package under a `{ name: 'docs' }` group, say) has no path segments left of its own for `rest` to keep, so the group's declared identity stands in for them, exactly as it always does for 'keep-group'. Without this, such a group could never have a satisfiable expected name at all: every package in it would derive an empty (or bare-scope) name no real `package.json` can declare.
|
|
1644
|
+
* - **'keep-group'**: keep the group's OWN `name` ahead of `rest`, always, regardless of how deep its own `path` nests: a group declared `{ name: 'test', path: 'tests' }` derives "test-e2e" for `tests/e2e`, and one declared `{ name: 'test', path: 'packages/tests' }` derives "test-e2e" for `packages/tests/e2e` too, never "tests-e2e" (the group's declared identity, not whatever path segment sits above it). A test group whose packages are named "test-<feature>", mirroring the feature they test, needs its own "test" name kept, regardless of how many container directories its `path` nests under.
|
|
1655
1645
|
* - **'basename'**: use only `relativeDir`'s own final segment, ignoring every intermediate directory, for a group whose intermediate structure exists purely for filesystem organisation and carries no naming intent of its own.
|
|
1656
1646
|
*/
|
|
1657
1647
|
const NAME_SEGMENTS_BY_STRATEGY = {
|
|
1658
1648
|
basename: ({ segments }) => [last(segments)],
|
|
1659
|
-
"keep-group": ({ rest,
|
|
1660
|
-
"drop-group": ({ rest }) => rest
|
|
1649
|
+
"keep-group": ({ rest, group }) => [group.name, ...rest],
|
|
1650
|
+
"drop-group": ({ rest, group }) => rest.length === 0 ? [group.name] : rest
|
|
1661
1651
|
};
|
|
1662
1652
|
/**
|
|
1663
1653
|
* The package name package-name-mirrors-path expects a package at `relativeDir` (workspace-root-relative, forward-slash-joined) to declare, given its own matched group and the workspace's global naming options. See NAME_SEGMENTS_BY_STRATEGY above for the three per-group strategies' own segment selection. The joined segments are prefixed with `naming.scope` (when given) and joined to it with "/"; segments themselves join with `naming.separator` (default "-").
|
|
@@ -1667,15 +1657,27 @@ function expectedPackageName(relativeDir, group, naming) {
|
|
|
1667
1657
|
const segments = splitPathSegments(relativeDir);
|
|
1668
1658
|
const groupPathSegments = splitPathSegments(group.path ?? group.name);
|
|
1669
1659
|
const rest = segments.slice(groupPathSegments.length);
|
|
1670
|
-
const groupNameSegment = last(groupPathSegments);
|
|
1671
1660
|
const joined = NAME_SEGMENTS_BY_STRATEGY[group.naming ?? "drop-group"]({
|
|
1672
1661
|
segments,
|
|
1673
1662
|
rest,
|
|
1674
|
-
|
|
1663
|
+
group
|
|
1675
1664
|
}).join(separator);
|
|
1676
1665
|
return naming.scope === void 0 ? joined : `${naming.scope}/${joined}`;
|
|
1677
1666
|
}
|
|
1678
1667
|
//#endregion
|
|
1668
|
+
//#region src/rules/json-member-key.ts
|
|
1669
|
+
function isStringNamed(name) {
|
|
1670
|
+
return name.type === "String";
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Shared by package-json-key-order.ts and workspace-json-helpers.ts: momoa's own MemberNode.name is typed StringNode | IdentifierNode because momoa's grammar also covers JSON5 (unquoted identifier keys), but every rule that calls this only ever sets meta.languages to json/json or json/jsonc, where an unquoted key is a genuine parse error, never a value this function is asked to name. Exported so that guarantee is checked directly against a deliberately IdentifierNode-shaped input, rather than trusted on the strength of this comment alone.
|
|
1674
|
+
*/
|
|
1675
|
+
function getMemberKeyName(member) {
|
|
1676
|
+
const { name } = member;
|
|
1677
|
+
if (!isStringNamed(name)) throw new Error(`Unreachable: JSON member names are only read under json/json or json/jsonc, where an unquoted (Identifier) member name is a parse error, got a "${name.type}" name instead.`);
|
|
1678
|
+
return name.value;
|
|
1679
|
+
}
|
|
1680
|
+
//#endregion
|
|
1679
1681
|
//#region src/rules/workspace-json-helpers.ts
|
|
1680
1682
|
function isStringNode$1(node) {
|
|
1681
1683
|
return node.type === "String";
|
|
@@ -1683,7 +1685,7 @@ function isStringNode$1(node) {
|
|
|
1683
1685
|
/** package.json's own declared "name" field, read directly off a confirmed-top-level Object node. Returns undefined for a manifest with no "name" at all, or one whose value is not a plain string (a shape other JSON tooling would already flag elsewhere, not this rule's concern). */
|
|
1684
1686
|
function readDeclaredName(rootObject) {
|
|
1685
1687
|
for (const member of rootObject.members) {
|
|
1686
|
-
if (
|
|
1688
|
+
if (getMemberKeyName(member) !== "name") continue;
|
|
1687
1689
|
if (!isStringNode$1(member.value)) return void 0;
|
|
1688
1690
|
return {
|
|
1689
1691
|
name: member.value.value,
|
|
@@ -1698,15 +1700,12 @@ function collectTopLevelDependencies(rootObject, dependencyFields) {
|
|
|
1698
1700
|
const fields = new Set(dependencyFields);
|
|
1699
1701
|
const results = [];
|
|
1700
1702
|
for (const member of rootObject.members) {
|
|
1701
|
-
if (!
|
|
1703
|
+
if (!fields.has(getMemberKeyName(member))) continue;
|
|
1702
1704
|
if (member.value.type !== "Object") continue;
|
|
1703
|
-
for (const dependency of member.value.members) {
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
node: dependency
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
1705
|
+
for (const dependency of member.value.members) results.push({
|
|
1706
|
+
name: getMemberKeyName(dependency),
|
|
1707
|
+
node: dependency
|
|
1708
|
+
});
|
|
1710
1709
|
}
|
|
1711
1710
|
return results;
|
|
1712
1711
|
}
|
|
@@ -1729,7 +1728,7 @@ function createNoDependencyCycleRule(deps = {}) {
|
|
|
1729
1728
|
description: "Disallow a cyclic workspace dependency.",
|
|
1730
1729
|
url: "https://github.com/ExaDev/eslint-config/blob/main/src/rules/no-dependency-cycle.ts"
|
|
1731
1730
|
},
|
|
1732
|
-
messages: { cycle: "\"{{from}}\" depends on \"{{to}}\", which
|
|
1731
|
+
messages: { cycle: "\"{{from}}\" depends on \"{{to}}\", which can reach back to \"{{from}}\": a workspace cycle. Move the shared code into a package both can depend on, or invert one edge behind a contract." }
|
|
1733
1732
|
},
|
|
1734
1733
|
create(context) {
|
|
1735
1734
|
const options = readWorkspaceArchitectureOptions(context.options[0]);
|
|
@@ -2561,14 +2560,6 @@ function isIdentityPermutation(permutation) {
|
|
|
2561
2560
|
function isStringNode(node) {
|
|
2562
2561
|
return node.type === "String";
|
|
2563
2562
|
}
|
|
2564
|
-
function isStringNamed(name) {
|
|
2565
|
-
return name.type === "String";
|
|
2566
|
-
}
|
|
2567
|
-
function getMemberKeyName(member) {
|
|
2568
|
-
const { name } = member;
|
|
2569
|
-
if (!isStringNamed(name)) throw new Error(`Unreachable: package-json-key-order only supports json/json and json/jsonc, where an unquoted (Identifier) member name is a parse error — got a "${name.type}" name instead.`);
|
|
2570
|
-
return name.value;
|
|
2571
|
-
}
|
|
2572
2563
|
function rangeOf(node) {
|
|
2573
2564
|
if (node.range === void 0) throw new Error("Unreachable: every node reaching this function was produced by a JSON language that always requests range tracking.");
|
|
2574
2565
|
return node.range;
|
|
@@ -2820,11 +2811,11 @@ const FUNCTION_LIKE_SELECTOR$2 = [
|
|
|
2820
2811
|
function isOptionalParam(param) {
|
|
2821
2812
|
if (param.type === AST_NODE_TYPES.TSParameterProperty) return isOptionalParam(param.parameter);
|
|
2822
2813
|
if (param.type === AST_NODE_TYPES.AssignmentPattern) return true;
|
|
2823
|
-
if (param.type === AST_NODE_TYPES.Identifier) return param.optional;
|
|
2814
|
+
if (param.type === AST_NODE_TYPES.Identifier || param.type === AST_NODE_TYPES.ObjectPattern || param.type === AST_NODE_TYPES.ArrayPattern) return param.optional;
|
|
2824
2815
|
return false;
|
|
2825
2816
|
}
|
|
2826
2817
|
function getTrailingOptionalRun(params) {
|
|
2827
|
-
const hasTrailingRest = params
|
|
2818
|
+
const hasTrailingRest = params[params.length - 1]?.type === AST_NODE_TYPES.RestElement;
|
|
2828
2819
|
const run = [];
|
|
2829
2820
|
for (let i = params.length - (hasTrailingRest ? 2 : 1); i >= 0; i--) {
|
|
2830
2821
|
const param = params[i];
|
|
@@ -2834,7 +2825,6 @@ function getTrailingOptionalRun(params) {
|
|
|
2834
2825
|
return run;
|
|
2835
2826
|
}
|
|
2836
2827
|
function resolveFixableParam(param) {
|
|
2837
|
-
if (param.type === AST_NODE_TYPES.TSParameterProperty) return void 0;
|
|
2838
2828
|
const identifierNode = param.type === AST_NODE_TYPES.AssignmentPattern ? param.left.type === AST_NODE_TYPES.Identifier ? param.left : void 0 : param.type === AST_NODE_TYPES.Identifier ? param : void 0;
|
|
2839
2829
|
if (identifierNode === void 0) return void 0;
|
|
2840
2830
|
const typeNode = identifierNode.typeAnnotation?.typeAnnotation;
|
|
@@ -2928,7 +2918,7 @@ const preferOptionsObjectParam = createRule$2({
|
|
|
2928
2918
|
const allResolvable = resolvedParams.length === run.length;
|
|
2929
2919
|
const hasRestParam = node.params.some((param) => param.type === AST_NODE_TYPES.RestElement);
|
|
2930
2920
|
const body = getBlockBody(node);
|
|
2931
|
-
let isFixable = allResolvable && !hasRestParam
|
|
2921
|
+
let isFixable = allResolvable && !hasRestParam;
|
|
2932
2922
|
if (isFixable) {
|
|
2933
2923
|
const jsdocComment = getLeadingJSDocComment(sourceCode, node);
|
|
2934
2924
|
const jsDocBail = jsdocComment !== void 0 && resolvedParams.some((info) => jsDocMentionsParam(jsdocComment.value, info.identifierNode.name));
|
|
@@ -3271,6 +3261,7 @@ const plugin = {
|
|
|
3271
3261
|
}
|
|
3272
3262
|
}
|
|
3273
3263
|
};
|
|
3264
|
+
const publicPlugin = toPublicPlugin(plugin);
|
|
3274
3265
|
//#endregion
|
|
3275
3266
|
//#region src/package-json-key-order.ts
|
|
3276
3267
|
const SYNCPACK_CONFIG_FILENAMES = [
|
|
@@ -3414,11 +3405,13 @@ function toPublicConfigArray(built) {
|
|
|
3414
3405
|
//#endregion
|
|
3415
3406
|
//#region src/workspace-architecture.ts
|
|
3416
3407
|
/**
|
|
3417
|
-
* Wires the three workspace-architecture rules (no-uphill-dependency, no-dependency-cycle, and, when the shared `naming` option is given, package-name-mirrors-path) onto `**\/package.json`, all three sharing the one options object given here.
|
|
3408
|
+
* Wires the three workspace-architecture rules (no-uphill-dependency, no-dependency-cycle, and, when the shared `naming` option is given, package-name-mirrors-path) onto `**\/package.json`, all three sharing the one options object given here. Internal: consumed by create-config.ts as one more `ConfigArrayValue` entry among the rest of `exadevConfig`'s own internally-typed build (see recommendedTypeChecked, buildReactConfig, buildNextjsConfig et al. for the same shape), before that whole array is converted once at `exadevConfig`'s own outer boundary. Never exported directly; a standalone consumer wants `workspaceArchitectureConfig` below instead.
|
|
3418
3409
|
*
|
|
3419
3410
|
* Unlike buildPackageJsonKeyOrderConfig, this has no auto-detecting tri-state: workspace architecture rules require real configuration (`groups` has no sensible default), so this is off unless a consumer calls it at all, and, once called, always requires `@eslint/json` to be resolvable (there is no "silently do nothing" case to fall back to the way an unconfigured, purely auto-detected feature can).
|
|
3411
|
+
*
|
|
3412
|
+
* Takes `WorkspaceArchitectureConfigOptions` (the `requireFn` test seam included) rather than the public `workspaceArchitectureConfig` export's own `WorkspaceArchitectureOptions`, so this internal builder's own tests (workspace-architecture.unit.test.ts) can still simulate an unresolvable `@eslint/json` directly, without the seam ever reaching the public function's parameter type or dist/index.d.ts.
|
|
3420
3413
|
*/
|
|
3421
|
-
function
|
|
3414
|
+
function buildWorkspaceArchitectureConfig(options) {
|
|
3422
3415
|
const { requireFn, ...ruleOptions } = options;
|
|
3423
3416
|
const validated = readWorkspaceArchitectureOptions(ruleOptions);
|
|
3424
3417
|
const jsonPlugin = resolveJsonPlugin(tryRequire("@eslint/json", requireFn));
|
|
@@ -3437,6 +3430,12 @@ function workspaceArchitectureConfig(options) {
|
|
|
3437
3430
|
}
|
|
3438
3431
|
}];
|
|
3439
3432
|
}
|
|
3433
|
+
/**
|
|
3434
|
+
* Wires the three workspace-architecture rules (`no-uphill-dependency`, `no-dependency-cycle`, and, when the shared `naming` option is given, `package-name-mirrors-path`) onto `**\/package.json` from one `WorkspaceArchitectureOptions` object, returning ESLint core's own `Config[]` so the result spreads directly into `defineConfig(...)` (`...workspaceArchitectureConfig(options)`, the README's own standalone usage) alongside a consumer's own configuration.
|
|
3435
|
+
*/
|
|
3436
|
+
function workspaceArchitectureConfig(options) {
|
|
3437
|
+
return toPublicConfigArray(buildWorkspaceArchitectureConfig(options));
|
|
3438
|
+
}
|
|
3440
3439
|
//#endregion
|
|
3441
3440
|
//#region src/create-config.ts
|
|
3442
3441
|
function exadevConfig(options = {}, ...userConfigs) {
|
|
@@ -3448,10 +3447,10 @@ function exadevConfig(options = {}, ...userConfigs) {
|
|
|
3448
3447
|
...buildReactConfig({ enabled: options.react }),
|
|
3449
3448
|
...buildNextjsConfig({ enabled: options.nextjs }),
|
|
3450
3449
|
...buildPackageJsonKeyOrderConfig({ enabled: options.packageJsonKeyOrder }),
|
|
3451
|
-
...options.workspaceArchitecture !== void 0 ?
|
|
3450
|
+
...options.workspaceArchitecture !== void 0 ? buildWorkspaceArchitectureConfig(options.workspaceArchitecture) : [],
|
|
3452
3451
|
...userConfigs
|
|
3453
3452
|
]);
|
|
3454
3453
|
}
|
|
3455
3454
|
const defaultConfig = exadevConfig();
|
|
3456
3455
|
//#endregion
|
|
3457
|
-
export { defaultConfig as default, exadevConfig, plugin, workspaceArchitectureConfig };
|
|
3456
|
+
export { defaultConfig as default, exadevConfig, publicPlugin as plugin, workspaceArchitectureConfig };
|
package/package.json
CHANGED