exadev-eslint-config 2.18.1 → 2.19.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 +9 -6
- package/dist/index.cjs +152 -101
- package/dist/index.js +153 -102
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Consumers need `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as required pee
|
|
|
16
16
|
pnpm add -D @exadev/eslint-config typescript-eslint eslint
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The default export is the full, type-checked ruleset: typescript-eslint's `strictTypeChecked` + `stylisticTypeChecked` presets (`strictTypeChecked` subsumes `recommendedTypeChecked`, so it already includes `no-deprecated`, `no-misused-spread`, `no-mixed-enums`, `no-unnecessary-condition`, `use-unknown-in-catch-callback-variable`, `return-await`, `related-getter-setter-pairs`, `no-unnecessary-type-parameters`, and more — those are no longer re-listed below), `exadev/barrel-policy` at
|
|
19
|
+
The default export is the full, type-checked ruleset: typescript-eslint's `strictTypeChecked` + `stylisticTypeChecked` presets (`strictTypeChecked` subsumes `recommendedTypeChecked`, so it already includes `no-deprecated`, `no-misused-spread`, `no-mixed-enums`, `no-unnecessary-condition`, `use-unknown-in-catch-callback-variable`, `return-await`, `related-getter-setter-pairs`, `no-unnecessary-type-parameters`, and more — those are no longer re-listed below), `exadev/barrel-policy` at its auto-detecting default (see [Barrel policy](#barrel-policy)), `exadev/no-object-assign`, `exadev/no-mutable-union-array-param`, `exadev/no-array-isarray-mutation`, `exadev/no-enum-number-widening`, `exadev/no-enum-reverse-lookup-widening`, `exadev/no-map-instanceof-mutation`, `exadev/no-set-instanceof-mutation`, `exadev/prefer-readonly-array-param`, `exadev/prefer-readonly-object-param`, `exadev/prefer-numeric-sort-compare`, `exadev/no-pointless-reassignment`, `exadev/test-file-kind`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, `@typescript-eslint/consistent-type-imports`, `@typescript-eslint/consistent-type-exports`, `@typescript-eslint/consistent-return` (a function that implicitly returns `undefined` on one path and a real value on another — a common real bug, not a deliberate design), `@typescript-eslint/no-non-null-assertion` banning the `!` operator (the same manual-override escape hatch as a type assertion, under a different spelling), `@typescript-eslint/no-redeclare`, `@typescript-eslint/no-shadow`, `@typescript-eslint/no-use-before-define` set to `{ functions: false }` (a genuine temporal-dead-zone crash risk for `let`/`const`/`class`/enum bindings, but exempting function declarations, which are fully hoisted and therefore runtime-safe to call before their point of textual declaration — this codebase's own rule files consistently define their helper functions after the logic that calls them), `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright, `@typescript-eslint/method-signature-style` set to `'property'` (method-shorthand signatures are checked bivariantly under `strictFunctionTypes`, which is unsound), `@typescript-eslint/prefer-readonly`, `@typescript-eslint/promise-function-async`, `@typescript-eslint/require-array-sort-compare`, `@typescript-eslint/strict-void-return` (not yet in any typescript-eslint preset — disallows passing a value-returning function where a void-returning one is expected, e.g. `arr.forEach(x => otherArray.push(x))`, which typechecks today under TS's own void-return contravariance leniency), `@typescript-eslint/switch-exhaustiveness-check`, `@typescript-eslint/strict-boolean-expressions` at the rule's own bare defaults (an unambiguous non-nullable truthy check stays allowed; an ambiguous nullable check does not), `@typescript-eslint/no-magic-numbers` tuned to exempt array indexes, enum members, readonly class properties, default parameter values, numeric literal types (e.g. `type Indent = 2 | 4`), and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`), `max-lines` set to `{ max: 800, skipBlankLines: true, skipComments: true }` (counting only real code, so a file isn't pushed over the limit by whitespace or its own WHY-explanation comments), and `no-warning-comments` banning any comment containing `Stryker disable` (Stryker's own mutation-testing suppression directive, invisible to ESLint's `noInlineConfig` above since it isn't an eslint-disable comment at all) — the type-assertion and ts-comment rules are relaxed in test files, and `no-magic-numbers`/`max-lines`/`no-warning-comments` are not (see below). Spread it directly into `tseslint.config(...)`:
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
// eslint.config.ts
|
|
@@ -34,11 +34,11 @@ export default tseslint.config(
|
|
|
34
34
|
);
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
**
|
|
37
|
+
**The default export's `barrel-policy` mode is `'auto'`, so a published package's `src/index.ts` is permitted as a barrel with no config change on your part** — it walks up from the linted file to the nearest ancestor `package.json` and checks for a real `exports` or `main`; find one and the file's package resolves to `single`, find neither and it resolves to `banned`, identical to this package's old hardcoded default. Override only when you want a fixed policy regardless of what your own `package.json` looks like — e.g. `siblings` mode, which `auto` never resolves to on its own (flat-config later blocks override earlier rule settings):
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
40
|
...exadev,
|
|
41
|
-
{ rules: { 'exadev/barrel-policy': ['error', { mode: '
|
|
41
|
+
{ rules: { 'exadev/barrel-policy': ['error', { mode: 'siblings' }] } }, // any index file may be a barrel, not just src/index.ts
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
`strictTypeChecked` subsumes both typescript-eslint's plain `recommended` and `recommendedTypeChecked` outright (every rule in each is also present in `strictTypeChecked`), and its base config registers the `@typescript-eslint` plugin and sets `languageOptions.parser` itself. That is why **you must remove your own `...tseslint.configs.recommended`/`recommendedTypeChecked`/`strictTypeChecked`/`stylisticTypeChecked` spreads** — flat config rejects two different plugin object instances registered under the same namespace. You still supply `languageOptions.parserOptions.project`/`projectService` pointing at your own tsconfig(s).
|
|
@@ -225,14 +225,17 @@ Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-d
|
|
|
225
225
|
|
|
226
226
|
## Barrel policy
|
|
227
227
|
|
|
228
|
-
`exadev/barrel-policy` is the convenience layer: one rule id, one `{ mode }` option selecting a complete index-file policy. Use EITHER this umbrella OR the individual rules (not both — they double-report).
|
|
228
|
+
`exadev/barrel-policy` is the convenience layer: one rule id, one `{ mode }` option selecting a complete index-file policy. Use EITHER this umbrella OR the individual rules (not both — they double-report). Omitting `mode` entirely, or passing an options object with no `mode` key, means `'auto'`.
|
|
229
229
|
|
|
230
230
|
| `mode` | Which files may be barrels | What a barrel may contain | Where re-exports may come from |
|
|
231
231
|
| --- | --- | --- | --- |
|
|
232
|
-
| `'
|
|
233
|
-
| `'
|
|
232
|
+
| `'auto'` (what an omitted `mode` means) | detected per file — walks up to the nearest ancestor `package.json`; a real `exports`/`main` there resolves to `single`, otherwise `banned` | only re-exports when detected as `single` | anywhere, when detected as `single` |
|
|
233
|
+
| `'banned'` (`plugin.configs.recommended`'s explicit choice) | none | — | — |
|
|
234
|
+
| `'single'` (`plugin.configs.barrel`'s explicit choice) | exactly `src/index.ts` | only re-exports | anywhere |
|
|
234
235
|
| `'siblings'` | any `index.ts` | only re-exports | a direct sibling only (`./module`) |
|
|
235
236
|
|
|
237
|
+
`'auto'` only ever resolves to `banned` or `single` — there is no single-signal auto-equivalent for `siblings` (which package.json field would suggest "any index file, not just the entry point"?), so a project wanting that policy still states it explicitly. `private: true` in `package.json` is not consulted by the detection: a pnpm workspace package is routinely both `private` and a genuine import target for sibling packages via `exports`, so `private` says nothing about whether a barrel is warranted.
|
|
238
|
+
|
|
236
239
|
In every mode, re-exports are banned outside a permitted barrel, and a permitted barrel may contain only re-export statements. The umbrella composes the identical predicates the standalone rules use (shared in `src/rules/barrel-helpers.ts`). It is non-fixable — the autofix lives on `no-non-barrel-reexport`.
|
|
237
240
|
|
|
238
241
|
## Build, test, and lint
|
package/dist/index.cjs
CHANGED
|
@@ -260,13 +260,13 @@ function tryRequire(specifier, requireFn = nodeRequire) {
|
|
|
260
260
|
return;
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
|
-
function isRecord$
|
|
263
|
+
function isRecord$2(value) {
|
|
264
264
|
return typeof value === "object" && value !== null;
|
|
265
265
|
}
|
|
266
266
|
function normalizeLegacyParserOptions(record) {
|
|
267
267
|
if (!("parserOptions" in record)) return record;
|
|
268
268
|
const { parserOptions, languageOptions, ...rest } = record;
|
|
269
|
-
const existingLanguageOptions = isRecord$
|
|
269
|
+
const existingLanguageOptions = isRecord$2(languageOptions) ? languageOptions : {};
|
|
270
270
|
return {
|
|
271
271
|
...rest,
|
|
272
272
|
languageOptions: {
|
|
@@ -278,10 +278,10 @@ function normalizeLegacyParserOptions(record) {
|
|
|
278
278
|
function readFlatConfig(module, path) {
|
|
279
279
|
let current = module;
|
|
280
280
|
for (const key of path) {
|
|
281
|
-
if (!isRecord$
|
|
281
|
+
if (!isRecord$2(current)) return void 0;
|
|
282
282
|
current = current[key];
|
|
283
283
|
}
|
|
284
|
-
if (!isRecord$
|
|
284
|
+
if (!isRecord$2(current)) return void 0;
|
|
285
285
|
return normalizeLegacyParserOptions(current);
|
|
286
286
|
}
|
|
287
287
|
//#endregion
|
|
@@ -295,7 +295,7 @@ function buildNextjsConfig(options = {}) {
|
|
|
295
295
|
}
|
|
296
296
|
//#endregion
|
|
297
297
|
//#region package.json
|
|
298
|
-
var version = "2.
|
|
298
|
+
var version = "2.19.0";
|
|
299
299
|
//#endregion
|
|
300
300
|
//#region src/react.ts
|
|
301
301
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
@@ -362,6 +362,9 @@ function isDirectSibling(specifier) {
|
|
|
362
362
|
function isBarrelMode(value) {
|
|
363
363
|
return value === "banned" || value === "single" || value === "siblings";
|
|
364
364
|
}
|
|
365
|
+
function isRawBarrelMode(value) {
|
|
366
|
+
return value === "auto" || isBarrelMode(value);
|
|
367
|
+
}
|
|
365
368
|
function isPermittedBarrel(filename, mode) {
|
|
366
369
|
if (mode === "banned") return false;
|
|
367
370
|
if (mode === "single") return isMainBarrel(filename);
|
|
@@ -468,79 +471,139 @@ const barrelDirectSiblingsOnly = {
|
|
|
468
471
|
}
|
|
469
472
|
};
|
|
470
473
|
//#endregion
|
|
474
|
+
//#region src/rules/barrel-auto-detect.ts
|
|
475
|
+
function isRecord$1(value) {
|
|
476
|
+
return typeof value === "object" && value !== null;
|
|
477
|
+
}
|
|
478
|
+
function hasRealExports(value) {
|
|
479
|
+
if (typeof value === "string") return value.length > 0;
|
|
480
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
481
|
+
return isRecord$1(value) && Object.keys(value).length > 0;
|
|
482
|
+
}
|
|
483
|
+
function decideAutoBarrelMode(packageJson) {
|
|
484
|
+
const hasExports = hasRealExports(packageJson["exports"]);
|
|
485
|
+
const main = packageJson["main"];
|
|
486
|
+
const hasMain = typeof main === "string" && main.length > 0;
|
|
487
|
+
return hasExports || hasMain ? "single" : "banned";
|
|
488
|
+
}
|
|
489
|
+
const packageJsonByStartDir = /* @__PURE__ */ new Map();
|
|
490
|
+
function findNearestPackageJson(startDir) {
|
|
491
|
+
const cached = packageJsonByStartDir.get(startDir);
|
|
492
|
+
if (packageJsonByStartDir.has(startDir)) return cached;
|
|
493
|
+
let dir = startDir;
|
|
494
|
+
for (;;) {
|
|
495
|
+
const candidate = (0, node_path.join)(dir, "package.json");
|
|
496
|
+
if ((0, node_fs.existsSync)(candidate)) {
|
|
497
|
+
const parsed = JSON.parse((0, node_fs.readFileSync)(candidate, "utf8"));
|
|
498
|
+
const result = isRecord$1(parsed) ? parsed : void 0;
|
|
499
|
+
packageJsonByStartDir.set(startDir, result);
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
const parent = (0, node_path.dirname)(dir);
|
|
503
|
+
if (parent === dir) {
|
|
504
|
+
packageJsonByStartDir.set(startDir, void 0);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
dir = parent;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function resolveAutoMode(filename, readPackageJsonFn = findNearestPackageJson) {
|
|
511
|
+
const packageJson = readPackageJsonFn((0, node_path.dirname)(filename));
|
|
512
|
+
return packageJson === void 0 ? "banned" : decideAutoBarrelMode(packageJson);
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
471
515
|
//#region src/rules/barrel-policy.ts
|
|
472
516
|
function readMode(options) {
|
|
473
|
-
if (
|
|
517
|
+
if (options === void 0) return "auto";
|
|
518
|
+
if (typeof options !== "object" || options === null) throw new Error("exadev/barrel-policy requires options: { mode: 'banned' | 'single' | 'siblings' | 'auto' }.");
|
|
519
|
+
if (!("mode" in options)) return "auto";
|
|
520
|
+
if (!isRawBarrelMode(options.mode)) throw new Error("exadev/barrel-policy requires options: { mode: 'banned' | 'single' | 'siblings' | 'auto' }.");
|
|
474
521
|
return options.mode;
|
|
475
522
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
});
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
if (mode === "single") {
|
|
514
|
-
if (isIndexFile(filename) && !isMainBarrel(filename)) {
|
|
515
|
-
context.report({
|
|
523
|
+
function createBarrelPolicyRule(readPackageJsonFn = findNearestPackageJson) {
|
|
524
|
+
return {
|
|
525
|
+
meta: {
|
|
526
|
+
type: "problem",
|
|
527
|
+
schema: [{
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: { mode: {
|
|
530
|
+
type: "string",
|
|
531
|
+
enum: [
|
|
532
|
+
"banned",
|
|
533
|
+
"single",
|
|
534
|
+
"siblings",
|
|
535
|
+
"auto"
|
|
536
|
+
]
|
|
537
|
+
} },
|
|
538
|
+
additionalProperties: false
|
|
539
|
+
}],
|
|
540
|
+
messages: {
|
|
541
|
+
indexFileBanned: "Index (barrel) files are banned in this project — import directly from the module that owns the export instead. Rename this file to something descriptive.",
|
|
542
|
+
nonMainIndexFile: "Only src/index.ts may be a barrel in this project — this index file is not it. Move its contents into the module that owns them or give the file a descriptive name.",
|
|
543
|
+
sideEffectInBarrel: "A barrel may contain only re-export statements ('export * from ...' / 'export { x } from ...' / 'export type { x } from ...') — nothing else, so it can never have a side effect at import time by construction. Found: {{ description }}.",
|
|
544
|
+
reexportOutsideBarrel: "Re-exports belong only in a barrel (index) file — import this value directly in the file that uses it instead of re-exporting it through this one.",
|
|
545
|
+
notADirectSibling: "A barrel may re-export only from a direct sibling file or folder ('./module' or './module.ts') — found '{{ source }}'. Move the source closer, or import it directly at the call site rather than re-exporting it through this barrel."
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
create(context) {
|
|
549
|
+
const filename = context.filename;
|
|
550
|
+
const rawMode = readMode(context.options[0]);
|
|
551
|
+
const mode = rawMode === "auto" ? resolveAutoMode(filename, readPackageJsonFn) : rawMode;
|
|
552
|
+
const detector = createSplitReexportDetector();
|
|
553
|
+
return {
|
|
554
|
+
Program(node) {
|
|
555
|
+
if (mode === "banned") {
|
|
556
|
+
if (isIndexFile(filename)) context.report({
|
|
516
557
|
node,
|
|
517
|
-
messageId: "
|
|
558
|
+
messageId: "indexFileBanned"
|
|
518
559
|
});
|
|
519
560
|
return;
|
|
520
561
|
}
|
|
521
|
-
if (
|
|
562
|
+
if (mode === "single") {
|
|
563
|
+
if (isIndexFile(filename) && !isMainBarrel(filename)) {
|
|
564
|
+
context.report({
|
|
565
|
+
node,
|
|
566
|
+
messageId: "nonMainIndexFile"
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (isMainBarrel(filename)) {
|
|
571
|
+
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
572
|
+
node: statement,
|
|
573
|
+
messageId: "sideEffectInBarrel",
|
|
574
|
+
data: { description: statement.type }
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (isIndexFile(filename)) {
|
|
522
580
|
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
523
581
|
node: statement,
|
|
524
582
|
messageId: "sideEffectInBarrel",
|
|
525
583
|
data: { description: statement.type }
|
|
526
584
|
});
|
|
527
585
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
586
|
+
},
|
|
587
|
+
ImportDeclaration: (node) => {
|
|
588
|
+
detector.visitImport(node);
|
|
589
|
+
},
|
|
590
|
+
ExportNamedDeclaration(node) {
|
|
591
|
+
detector.visitExportNamed(node);
|
|
592
|
+
if (hasSource(node) && !isInsideAmbientModuleDeclaration(node)) {
|
|
593
|
+
const source = moduleSpecifierValue(node.source);
|
|
594
|
+
if (!isPermittedBarrel(filename, mode)) context.report({
|
|
595
|
+
node,
|
|
596
|
+
messageId: "reexportOutsideBarrel"
|
|
597
|
+
});
|
|
598
|
+
else if (mode === "siblings" && !isDirectSibling(source)) context.report({
|
|
599
|
+
node,
|
|
600
|
+
messageId: "notADirectSibling",
|
|
601
|
+
data: { source }
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
ExportAllDeclaration(node) {
|
|
606
|
+
if (isInsideAmbientModuleDeclaration(node)) return;
|
|
544
607
|
const source = moduleSpecifierValue(node.source);
|
|
545
608
|
if (!isPermittedBarrel(filename, mode)) context.report({
|
|
546
609
|
node,
|
|
@@ -551,42 +614,30 @@ const barrelPolicy = {
|
|
|
551
614
|
messageId: "notADirectSibling",
|
|
552
615
|
data: { source }
|
|
553
616
|
});
|
|
617
|
+
},
|
|
618
|
+
ExportDefaultDeclaration: (node) => {
|
|
619
|
+
detector.visitExportDefault(node);
|
|
620
|
+
},
|
|
621
|
+
"Program:exit"() {
|
|
622
|
+
for (const violation of detector.violations()) if (isPermittedBarrel(filename, mode)) {
|
|
623
|
+
if (mode === "siblings") {
|
|
624
|
+
const importSource = moduleSpecifierValue(violation.trackedImport.declaration.source);
|
|
625
|
+
if (!isDirectSibling(importSource)) context.report({
|
|
626
|
+
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
627
|
+
messageId: "notADirectSibling",
|
|
628
|
+
data: { source: importSource }
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
} else context.report({
|
|
632
|
+
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
633
|
+
messageId: "reexportOutsideBarrel"
|
|
634
|
+
});
|
|
554
635
|
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
node,
|
|
561
|
-
messageId: "reexportOutsideBarrel"
|
|
562
|
-
});
|
|
563
|
-
else if (mode === "siblings" && !isDirectSibling(source)) context.report({
|
|
564
|
-
node,
|
|
565
|
-
messageId: "notADirectSibling",
|
|
566
|
-
data: { source }
|
|
567
|
-
});
|
|
568
|
-
},
|
|
569
|
-
ExportDefaultDeclaration: (node) => {
|
|
570
|
-
detector.visitExportDefault(node);
|
|
571
|
-
},
|
|
572
|
-
"Program:exit"() {
|
|
573
|
-
for (const violation of detector.violations()) if (isPermittedBarrel(filename, mode)) {
|
|
574
|
-
if (mode === "siblings") {
|
|
575
|
-
const importSource = moduleSpecifierValue(violation.trackedImport.declaration.source);
|
|
576
|
-
if (!isDirectSibling(importSource)) context.report({
|
|
577
|
-
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
578
|
-
messageId: "notADirectSibling",
|
|
579
|
-
data: { source: importSource }
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
} else context.report({
|
|
583
|
-
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
584
|
-
messageId: "reexportOutsideBarrel"
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
};
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
var barrel_policy_default = createBarrelPolicyRule();
|
|
590
641
|
//#endregion
|
|
591
642
|
//#region src/rules/scope-guards.ts
|
|
592
643
|
function asIdentifierName(name) {
|
|
@@ -1837,7 +1888,7 @@ const plugin = {
|
|
|
1837
1888
|
},
|
|
1838
1889
|
rules: {
|
|
1839
1890
|
"barrel-direct-siblings-only": barrelDirectSiblingsOnly,
|
|
1840
|
-
"barrel-policy":
|
|
1891
|
+
"barrel-policy": barrel_policy_default,
|
|
1841
1892
|
"no-array-isarray-mutation": noArrayIsArrayMutation,
|
|
1842
1893
|
"no-control-flow": noControlFlow,
|
|
1843
1894
|
"no-enum-number-widening": noEnumNumberWidening,
|
|
@@ -2017,7 +2068,7 @@ const recommendedTypeChecked = [
|
|
|
2017
2068
|
plugins: { exadev: plugin },
|
|
2018
2069
|
linterOptions: { noInlineConfig: true },
|
|
2019
2070
|
rules: {
|
|
2020
|
-
"exadev/barrel-policy":
|
|
2071
|
+
"exadev/barrel-policy": "error",
|
|
2021
2072
|
"exadev/no-array-isarray-mutation": "error",
|
|
2022
2073
|
"exadev/no-enum-number-widening": "error",
|
|
2023
2074
|
"exadev/no-enum-reverse-lookup-widening": "error",
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs, { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import path, { join, posix } from "node:path";
|
|
2
|
+
import path, { dirname, join, posix } from "node:path";
|
|
3
3
|
import jsdoc from "eslint-plugin-jsdoc";
|
|
4
4
|
import tsdoc from "eslint-plugin-tsdoc";
|
|
5
5
|
import jsonCanonical from "eslint-plugin-json-canonical";
|
|
@@ -226,13 +226,13 @@ function tryRequire(specifier, requireFn = nodeRequire) {
|
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
|
-
function isRecord$
|
|
229
|
+
function isRecord$2(value) {
|
|
230
230
|
return typeof value === "object" && value !== null;
|
|
231
231
|
}
|
|
232
232
|
function normalizeLegacyParserOptions(record) {
|
|
233
233
|
if (!("parserOptions" in record)) return record;
|
|
234
234
|
const { parserOptions, languageOptions, ...rest } = record;
|
|
235
|
-
const existingLanguageOptions = isRecord$
|
|
235
|
+
const existingLanguageOptions = isRecord$2(languageOptions) ? languageOptions : {};
|
|
236
236
|
return {
|
|
237
237
|
...rest,
|
|
238
238
|
languageOptions: {
|
|
@@ -244,10 +244,10 @@ function normalizeLegacyParserOptions(record) {
|
|
|
244
244
|
function readFlatConfig(module, path) {
|
|
245
245
|
let current = module;
|
|
246
246
|
for (const key of path) {
|
|
247
|
-
if (!isRecord$
|
|
247
|
+
if (!isRecord$2(current)) return void 0;
|
|
248
248
|
current = current[key];
|
|
249
249
|
}
|
|
250
|
-
if (!isRecord$
|
|
250
|
+
if (!isRecord$2(current)) return void 0;
|
|
251
251
|
return normalizeLegacyParserOptions(current);
|
|
252
252
|
}
|
|
253
253
|
//#endregion
|
|
@@ -261,7 +261,7 @@ function buildNextjsConfig(options = {}) {
|
|
|
261
261
|
}
|
|
262
262
|
//#endregion
|
|
263
263
|
//#region package.json
|
|
264
|
-
var version = "2.
|
|
264
|
+
var version = "2.19.0";
|
|
265
265
|
//#endregion
|
|
266
266
|
//#region src/react.ts
|
|
267
267
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
@@ -328,6 +328,9 @@ function isDirectSibling(specifier) {
|
|
|
328
328
|
function isBarrelMode(value) {
|
|
329
329
|
return value === "banned" || value === "single" || value === "siblings";
|
|
330
330
|
}
|
|
331
|
+
function isRawBarrelMode(value) {
|
|
332
|
+
return value === "auto" || isBarrelMode(value);
|
|
333
|
+
}
|
|
331
334
|
function isPermittedBarrel(filename, mode) {
|
|
332
335
|
if (mode === "banned") return false;
|
|
333
336
|
if (mode === "single") return isMainBarrel(filename);
|
|
@@ -434,79 +437,139 @@ const barrelDirectSiblingsOnly = {
|
|
|
434
437
|
}
|
|
435
438
|
};
|
|
436
439
|
//#endregion
|
|
440
|
+
//#region src/rules/barrel-auto-detect.ts
|
|
441
|
+
function isRecord$1(value) {
|
|
442
|
+
return typeof value === "object" && value !== null;
|
|
443
|
+
}
|
|
444
|
+
function hasRealExports(value) {
|
|
445
|
+
if (typeof value === "string") return value.length > 0;
|
|
446
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
447
|
+
return isRecord$1(value) && Object.keys(value).length > 0;
|
|
448
|
+
}
|
|
449
|
+
function decideAutoBarrelMode(packageJson) {
|
|
450
|
+
const hasExports = hasRealExports(packageJson["exports"]);
|
|
451
|
+
const main = packageJson["main"];
|
|
452
|
+
const hasMain = typeof main === "string" && main.length > 0;
|
|
453
|
+
return hasExports || hasMain ? "single" : "banned";
|
|
454
|
+
}
|
|
455
|
+
const packageJsonByStartDir = /* @__PURE__ */ new Map();
|
|
456
|
+
function findNearestPackageJson(startDir) {
|
|
457
|
+
const cached = packageJsonByStartDir.get(startDir);
|
|
458
|
+
if (packageJsonByStartDir.has(startDir)) return cached;
|
|
459
|
+
let dir = startDir;
|
|
460
|
+
for (;;) {
|
|
461
|
+
const candidate = join(dir, "package.json");
|
|
462
|
+
if (existsSync(candidate)) {
|
|
463
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
464
|
+
const result = isRecord$1(parsed) ? parsed : void 0;
|
|
465
|
+
packageJsonByStartDir.set(startDir, result);
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
const parent = dirname(dir);
|
|
469
|
+
if (parent === dir) {
|
|
470
|
+
packageJsonByStartDir.set(startDir, void 0);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
dir = parent;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function resolveAutoMode(filename, readPackageJsonFn = findNearestPackageJson) {
|
|
477
|
+
const packageJson = readPackageJsonFn(dirname(filename));
|
|
478
|
+
return packageJson === void 0 ? "banned" : decideAutoBarrelMode(packageJson);
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
437
481
|
//#region src/rules/barrel-policy.ts
|
|
438
482
|
function readMode(options) {
|
|
439
|
-
if (
|
|
483
|
+
if (options === void 0) return "auto";
|
|
484
|
+
if (typeof options !== "object" || options === null) throw new Error("exadev/barrel-policy requires options: { mode: 'banned' | 'single' | 'siblings' | 'auto' }.");
|
|
485
|
+
if (!("mode" in options)) return "auto";
|
|
486
|
+
if (!isRawBarrelMode(options.mode)) throw new Error("exadev/barrel-policy requires options: { mode: 'banned' | 'single' | 'siblings' | 'auto' }.");
|
|
440
487
|
return options.mode;
|
|
441
488
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
});
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
if (mode === "single") {
|
|
480
|
-
if (isIndexFile(filename) && !isMainBarrel(filename)) {
|
|
481
|
-
context.report({
|
|
489
|
+
function createBarrelPolicyRule(readPackageJsonFn = findNearestPackageJson) {
|
|
490
|
+
return {
|
|
491
|
+
meta: {
|
|
492
|
+
type: "problem",
|
|
493
|
+
schema: [{
|
|
494
|
+
type: "object",
|
|
495
|
+
properties: { mode: {
|
|
496
|
+
type: "string",
|
|
497
|
+
enum: [
|
|
498
|
+
"banned",
|
|
499
|
+
"single",
|
|
500
|
+
"siblings",
|
|
501
|
+
"auto"
|
|
502
|
+
]
|
|
503
|
+
} },
|
|
504
|
+
additionalProperties: false
|
|
505
|
+
}],
|
|
506
|
+
messages: {
|
|
507
|
+
indexFileBanned: "Index (barrel) files are banned in this project — import directly from the module that owns the export instead. Rename this file to something descriptive.",
|
|
508
|
+
nonMainIndexFile: "Only src/index.ts may be a barrel in this project — this index file is not it. Move its contents into the module that owns them or give the file a descriptive name.",
|
|
509
|
+
sideEffectInBarrel: "A barrel may contain only re-export statements ('export * from ...' / 'export { x } from ...' / 'export type { x } from ...') — nothing else, so it can never have a side effect at import time by construction. Found: {{ description }}.",
|
|
510
|
+
reexportOutsideBarrel: "Re-exports belong only in a barrel (index) file — import this value directly in the file that uses it instead of re-exporting it through this one.",
|
|
511
|
+
notADirectSibling: "A barrel may re-export only from a direct sibling file or folder ('./module' or './module.ts') — found '{{ source }}'. Move the source closer, or import it directly at the call site rather than re-exporting it through this barrel."
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
create(context) {
|
|
515
|
+
const filename = context.filename;
|
|
516
|
+
const rawMode = readMode(context.options[0]);
|
|
517
|
+
const mode = rawMode === "auto" ? resolveAutoMode(filename, readPackageJsonFn) : rawMode;
|
|
518
|
+
const detector = createSplitReexportDetector();
|
|
519
|
+
return {
|
|
520
|
+
Program(node) {
|
|
521
|
+
if (mode === "banned") {
|
|
522
|
+
if (isIndexFile(filename)) context.report({
|
|
482
523
|
node,
|
|
483
|
-
messageId: "
|
|
524
|
+
messageId: "indexFileBanned"
|
|
484
525
|
});
|
|
485
526
|
return;
|
|
486
527
|
}
|
|
487
|
-
if (
|
|
528
|
+
if (mode === "single") {
|
|
529
|
+
if (isIndexFile(filename) && !isMainBarrel(filename)) {
|
|
530
|
+
context.report({
|
|
531
|
+
node,
|
|
532
|
+
messageId: "nonMainIndexFile"
|
|
533
|
+
});
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (isMainBarrel(filename)) {
|
|
537
|
+
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
538
|
+
node: statement,
|
|
539
|
+
messageId: "sideEffectInBarrel",
|
|
540
|
+
data: { description: statement.type }
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (isIndexFile(filename)) {
|
|
488
546
|
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
489
547
|
node: statement,
|
|
490
548
|
messageId: "sideEffectInBarrel",
|
|
491
549
|
data: { description: statement.type }
|
|
492
550
|
});
|
|
493
551
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
552
|
+
},
|
|
553
|
+
ImportDeclaration: (node) => {
|
|
554
|
+
detector.visitImport(node);
|
|
555
|
+
},
|
|
556
|
+
ExportNamedDeclaration(node) {
|
|
557
|
+
detector.visitExportNamed(node);
|
|
558
|
+
if (hasSource(node) && !isInsideAmbientModuleDeclaration(node)) {
|
|
559
|
+
const source = moduleSpecifierValue(node.source);
|
|
560
|
+
if (!isPermittedBarrel(filename, mode)) context.report({
|
|
561
|
+
node,
|
|
562
|
+
messageId: "reexportOutsideBarrel"
|
|
563
|
+
});
|
|
564
|
+
else if (mode === "siblings" && !isDirectSibling(source)) context.report({
|
|
565
|
+
node,
|
|
566
|
+
messageId: "notADirectSibling",
|
|
567
|
+
data: { source }
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
ExportAllDeclaration(node) {
|
|
572
|
+
if (isInsideAmbientModuleDeclaration(node)) return;
|
|
510
573
|
const source = moduleSpecifierValue(node.source);
|
|
511
574
|
if (!isPermittedBarrel(filename, mode)) context.report({
|
|
512
575
|
node,
|
|
@@ -517,42 +580,30 @@ const barrelPolicy = {
|
|
|
517
580
|
messageId: "notADirectSibling",
|
|
518
581
|
data: { source }
|
|
519
582
|
});
|
|
583
|
+
},
|
|
584
|
+
ExportDefaultDeclaration: (node) => {
|
|
585
|
+
detector.visitExportDefault(node);
|
|
586
|
+
},
|
|
587
|
+
"Program:exit"() {
|
|
588
|
+
for (const violation of detector.violations()) if (isPermittedBarrel(filename, mode)) {
|
|
589
|
+
if (mode === "siblings") {
|
|
590
|
+
const importSource = moduleSpecifierValue(violation.trackedImport.declaration.source);
|
|
591
|
+
if (!isDirectSibling(importSource)) context.report({
|
|
592
|
+
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
593
|
+
messageId: "notADirectSibling",
|
|
594
|
+
data: { source: importSource }
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
} else context.report({
|
|
598
|
+
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
599
|
+
messageId: "reexportOutsideBarrel"
|
|
600
|
+
});
|
|
520
601
|
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
node,
|
|
527
|
-
messageId: "reexportOutsideBarrel"
|
|
528
|
-
});
|
|
529
|
-
else if (mode === "siblings" && !isDirectSibling(source)) context.report({
|
|
530
|
-
node,
|
|
531
|
-
messageId: "notADirectSibling",
|
|
532
|
-
data: { source }
|
|
533
|
-
});
|
|
534
|
-
},
|
|
535
|
-
ExportDefaultDeclaration: (node) => {
|
|
536
|
-
detector.visitExportDefault(node);
|
|
537
|
-
},
|
|
538
|
-
"Program:exit"() {
|
|
539
|
-
for (const violation of detector.violations()) if (isPermittedBarrel(filename, mode)) {
|
|
540
|
-
if (mode === "siblings") {
|
|
541
|
-
const importSource = moduleSpecifierValue(violation.trackedImport.declaration.source);
|
|
542
|
-
if (!isDirectSibling(importSource)) context.report({
|
|
543
|
-
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
544
|
-
messageId: "notADirectSibling",
|
|
545
|
-
data: { source: importSource }
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
} else context.report({
|
|
549
|
-
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
550
|
-
messageId: "reexportOutsideBarrel"
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
};
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
var barrel_policy_default = createBarrelPolicyRule();
|
|
556
607
|
//#endregion
|
|
557
608
|
//#region src/rules/scope-guards.ts
|
|
558
609
|
function asIdentifierName(name) {
|
|
@@ -1803,7 +1854,7 @@ const plugin = {
|
|
|
1803
1854
|
},
|
|
1804
1855
|
rules: {
|
|
1805
1856
|
"barrel-direct-siblings-only": barrelDirectSiblingsOnly,
|
|
1806
|
-
"barrel-policy":
|
|
1857
|
+
"barrel-policy": barrel_policy_default,
|
|
1807
1858
|
"no-array-isarray-mutation": noArrayIsArrayMutation,
|
|
1808
1859
|
"no-control-flow": noControlFlow,
|
|
1809
1860
|
"no-enum-number-widening": noEnumNumberWidening,
|
|
@@ -1983,7 +2034,7 @@ const recommendedTypeChecked = [
|
|
|
1983
2034
|
plugins: { exadev: plugin },
|
|
1984
2035
|
linterOptions: { noInlineConfig: true },
|
|
1985
2036
|
rules: {
|
|
1986
|
-
"exadev/barrel-policy":
|
|
2037
|
+
"exadev/barrel-policy": "error",
|
|
1987
2038
|
"exadev/no-array-isarray-mutation": "error",
|
|
1988
2039
|
"exadev/no-enum-number-widening": "error",
|
|
1989
2040
|
"exadev/no-enum-reverse-lookup-widening": "error",
|
package/package.json
CHANGED