@exadev/eslint-config 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -34
- package/dist/index.cjs +328 -109
- package/dist/index.js +328 -109
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,19 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
## Why
|
|
8
8
|
|
|
9
|
-
Multiple ExaDev repos
|
|
10
|
-
|
|
11
|
-
Only the *rules* are centralized here, not a consumer's whole `eslint.config.ts`. A repo's own file-scoping (`files`/`ignores`), tsconfig wiring, and any runtime-isomorphism import bans are genuinely project-specific -- forcing those into one shared config would mean either losing real per-project distinctions or building a heavily-parameterised config just to route around them. Each consumer keeps its own `eslint.config.ts`, importing rule implementations from here instead of a local copy.
|
|
9
|
+
Multiple ExaDev repos carried identical copies of a handful of custom ESLint rules (barrel/index discipline, re-export placement, pointless-alias detection). This package is the single source of truth for those rules. Only the *rules* are centralized -- not a consumer's whole `eslint.config.ts`, since file-scoping, tsconfig wiring, and runtime-isomorphism import bans are genuinely project-specific. Each consumer keeps its own `eslint.config.ts`, importing rule implementations from here.
|
|
12
10
|
|
|
13
11
|
## Getting started
|
|
14
12
|
|
|
15
|
-
Consumers need `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as peer dependencies
|
|
13
|
+
Consumers need `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as required peer dependencies. Importing anything from this package resolves `typescript-eslint`, since both the default export and `plugin` share the same root module -- ESM/CJS module evaluation runs a module's entire top-level import graph regardless of which export the caller reads (see [Architecture](#architecture)).
|
|
16
14
|
|
|
17
15
|
```sh
|
|
18
16
|
pnpm add -D @exadev/eslint-config typescript-eslint eslint
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
The default export is the full, type-checked ruleset: typescript-eslint's
|
|
19
|
+
The default export is the full, type-checked ruleset: typescript-eslint's `recommendedTypeChecked` + `stylisticTypeChecked` presets, `exadev/barrel-policy` at `mode: 'banned'` (see [Barrel policy](#barrel-policy)), `exadev/no-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, and `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright -- the last two relaxed in test files (see below). Spread it directly into `tseslint.config(...)`:
|
|
22
20
|
|
|
23
21
|
```ts
|
|
24
22
|
// eslint.config.ts
|
|
@@ -36,13 +34,20 @@ export default tseslint.config(
|
|
|
36
34
|
);
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
**A published package whose `src/index.ts` is its package entry point overrides `banned` to `single` in one line** (flat-config later blocks override earlier rule settings), since deleting its barrel would break every downstream importer:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
...exadev,
|
|
41
|
+
{ rules: { 'exadev/barrel-policy': ['error', { mode: 'single' }] } }, // this package keeps its barrel
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`recommendedTypeChecked` subsumes typescript-eslint's plain `recommended` outright (all 46 of its rules are a subset of `recommendedTypeChecked`'s 73), 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`/`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).
|
|
40
45
|
|
|
41
|
-
**Test files (`**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`) get two narrow relaxations of this package's own additions
|
|
46
|
+
**Test files (`**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`) get two narrow relaxations of this package's own additions, and only those two.** `@ts-expect-error` reverts to `allow-with-description` (a compile-time-only assertion of a type failure is a legitimate test pattern; `@ts-ignore`/`@ts-nocheck` stay banned since `@ts-expect-error` is strictly better). `consistent-type-assertions` relaxes to `assertionStyle: 'as'` (the legacy `<Type>value` form stays banned everywhere). Nothing inherited from the presets is relaxed.
|
|
42
47
|
|
|
43
48
|
### The lighter option: the `plugin` named export
|
|
44
49
|
|
|
45
|
-
For a project that wants only this package's own
|
|
50
|
+
For a project that wants only this package's own rules without the full type-checked bundle, import the named `plugin` export and wire rules individually:
|
|
46
51
|
|
|
47
52
|
```ts
|
|
48
53
|
// eslint.config.ts
|
|
@@ -78,7 +83,7 @@ export default defineConfig([
|
|
|
78
83
|
]);
|
|
79
84
|
```
|
|
80
85
|
|
|
81
|
-
`
|
|
86
|
+
`tseslint.config()` does **not** accept string `extends` (only `defineConfig()` does); pass the config value directly instead:
|
|
82
87
|
|
|
83
88
|
```ts
|
|
84
89
|
import { plugin } from '@exadev/eslint-config';
|
|
@@ -94,20 +99,31 @@ export default tseslint.config(
|
|
|
94
99
|
);
|
|
95
100
|
```
|
|
96
101
|
|
|
97
|
-
**`plugin.configs.recommended`/`plugin.configs.barrel` carry no `files`/`ignores`
|
|
98
|
-
|
|
99
|
-
The one thing self-scoping can't know on your behalf is a barrel that lives somewhere other than `src/index.ts`, or a project-specific exception beyond the barrel (an extra file you want exempt from the re-export ban). For either of those, layer an additional override on top of `recommended`/`barrel` -- e.g. `{ files: ['lib/other-legacy-reexport.ts'], rules: { 'exadev/no-non-barrel-reexport': 'off' } }` -- rather than falling back to wiring all four rules individually, which is still fine but no longer required for the common case.
|
|
100
|
-
|
|
101
|
-
`plugin.configs.recommended`/`plugin.configs.barrel` are not usable without `typescript-eslint` installed, even though neither config itself references it: `plugin` is a named export sharing its root module with the default export, so `typescript-eslint` resolves the moment anything is imported from `@exadev/eslint-config` at all -- see [Architecture](#architecture) for the trade-off this reflects.
|
|
102
|
+
**`plugin.configs.recommended`/`plugin.configs.barrel` carry no `files`/`ignores` and are safe unscoped** -- `no-side-effects-in-index` and `no-non-barrel-reexport` each check `context.filename` themselves (self-scoping). For a barrel not at `src/index.ts`, or a project-specific exception, layer an override on top (e.g. `{ files: ['lib/other.ts'], rules: { 'exadev/no-non-barrel-reexport': 'off' } }`) rather than wiring all four rules individually.
|
|
102
103
|
|
|
103
104
|
## Rules
|
|
104
105
|
|
|
105
106
|
| Rule | Fixable | Description |
|
|
106
107
|
| --- | --- | --- |
|
|
107
|
-
| `
|
|
108
|
-
| `no-
|
|
108
|
+
| `barrel-policy` | | Umbrella over the four barrel rules below: one `{ mode }` option selecting a whole index-file policy. See [Barrel policy](#barrel-policy). |
|
|
109
|
+
| `no-index-files` | | Bans any `index.*` file outright (mode 1). The strictest policy. |
|
|
110
|
+
| `no-non-barrel-index` | | Only `src/index.ts` may be named `index.*` -- any other `index.ts`/`.js`/etc would be silently selected by a consumer's bare directory import. |
|
|
111
|
+
| `no-non-barrel-reexport` | ✓ | Re-exports belong only in a barrel. Catches the split form across two statements (`import { x } from './y'; export { x };` or `export default x;`) which no AST selector alone can match. Autofix deletes the export and the now-pointless import when it was the import's only use. Self-scopes away from any index file. |
|
|
112
|
+
| `no-side-effects-in-index` | | A barrel file may contain only re-export statements -- nothing that could execute at import time. Self-scopes to any index file. |
|
|
113
|
+
| `barrel-direct-siblings-only` | | A barrel may re-export only from a direct sibling (`./module`), never a nested path, parent, or bare package specifier (mode 3). |
|
|
109
114
|
| `no-pointless-reassignment` | ✓ | `const foo = bar` where both sides are plain identifiers and the alias adds no transformation. |
|
|
110
|
-
|
|
115
|
+
|
|
116
|
+
## Barrel policy
|
|
117
|
+
|
|
118
|
+
`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).
|
|
119
|
+
|
|
120
|
+
| `mode` | Which files may be barrels | What a barrel may contain | Where re-exports may come from |
|
|
121
|
+
| --- | --- | --- | --- |
|
|
122
|
+
| `'banned'` (default/recommended) | none | — | — |
|
|
123
|
+
| `'single'` | exactly `src/index.ts` | only re-exports | anywhere |
|
|
124
|
+
| `'siblings'` | any `index.ts` | only re-exports | a direct sibling only (`./module`) |
|
|
125
|
+
|
|
126
|
+
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`.
|
|
111
127
|
|
|
112
128
|
## Build, test, and lint
|
|
113
129
|
|
|
@@ -119,46 +135,46 @@ pnpm test
|
|
|
119
135
|
pnpm build
|
|
120
136
|
```
|
|
121
137
|
|
|
122
|
-
Each rule has a co-located `*.test.ts`
|
|
138
|
+
Each rule has a co-located `*.test.ts` exercising it with ESLint's `RuleTester` under Vitest. `vitest.setup.ts` wires `RuleTester.describe`/`.it`/`.itOnly` to Vitest's `describe`/`it` explicitly (no `test.globals`). Each test uses typescript-eslint's parser for TypeScript-only fixtures; none need type information.
|
|
123
139
|
|
|
124
|
-
`pnpm test` always measures coverage (
|
|
140
|
+
`pnpm test` always measures coverage (`@vitest/coverage-v8`), scoped to `src/**/*.ts` excluding `*.test.ts`. Text output in terminal; `html`/`lcov` in `coverage/` (gitignored alongside `.eslintcache` and `dist/`).
|
|
125
141
|
|
|
126
|
-
The `lint`/`typecheck`/`test`/`build` npm scripts
|
|
142
|
+
The `lint`/`typecheck`/`test`/`build` npm scripts wrap turbo tasks named `_lint`/`_typecheck`/`_test`/`_build` -- run `pnpm build`, not `turbo run build`.
|
|
127
143
|
|
|
128
|
-
`pnpm build` runs `tsdown` from
|
|
144
|
+
`pnpm build` runs `tsdown` from `src/index.ts`, bundling the whole module graph into ESM + CJS + declarations. `prepublishOnly` re-runs lint, typecheck, `test`, `tsdown`, `publint`, and `attw --pack`.
|
|
129
145
|
|
|
130
146
|
## Architecture
|
|
131
147
|
|
|
132
|
-
`src/plugin.ts` builds an `ESLint.Plugin`
|
|
148
|
+
`src/plugin.ts` builds an `ESLint.Plugin` (ESLint's own type) combining `src/rules/` into a flat `rules` map. `configs.recommended` and `configs.barrel` are getters in the object literal -- each references the fully-built `plugin` (`plugins: { exadev: plugin }`), which a plain property initializer can't do mid-construction. `recommended` ships `barrel-policy` at `mode: 'banned'`; `barrel` at `mode: 'single'`.
|
|
133
149
|
|
|
134
|
-
`src/recommended-type-checked.ts` bundles typescript-eslint's
|
|
150
|
+
`src/recommended-type-checked.ts` bundles typescript-eslint's `recommendedTypeChecked` + `stylisticTypeChecked` alongside this plugin's rules into a flat config array. Its value is typed as `ConfigArrayValue = Extract<ConfigValue, unknown[]>` (the array-only member of ESLint's own config-value union), because annotating with the wider union broke `...exadev` with `TS2488`.
|
|
135
151
|
|
|
136
|
-
`src/index.ts` is the
|
|
152
|
+
`src/index.ts` is the entry point: `export { default } from './recommended-type-checked'; export { default as plugin } from './plugin';`. Both 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).
|
|
137
153
|
|
|
138
|
-
`pnpm-workspace.yaml`
|
|
154
|
+
`pnpm-workspace.yaml` declares an empty `packages: []` -- not a real workspace, just giving turbo a root for local task caching.
|
|
139
155
|
|
|
140
156
|
## Conventions
|
|
141
157
|
|
|
142
|
-
`eslint.config.ts` dogfoods
|
|
158
|
+
`eslint.config.ts` dogfoods the default export on itself (`import exadev from './src/index'`), spreading it exactly as a real consumer would. `no-side-effects-in-index` and `no-non-barrel-reexport` self-scope to `src/index.ts` internally, so no `files`/`ignores` wiring is needed here. Plugin construction lives in `src/plugin.ts` specifically so `src/index.ts` stays a pure re-export point.
|
|
143
159
|
|
|
144
|
-
`tsconfig.json` enables `verbatimModuleSyntax` (
|
|
160
|
+
`tsconfig.json` enables `verbatimModuleSyntax` (`import type`/`export type` required for type-only imports -- also enforced by `consistent-type-imports`) and `noUncheckedIndexedAccess` (narrow indexed access before use rather than asserting).
|
|
145
161
|
|
|
146
|
-
Conventional commits are enforced by commitlint, restricted to the type-enum defined once in `release.config.ts`'s `commitTypes` -- both commitlint
|
|
162
|
+
Conventional commits are enforced by commitlint, restricted to the type-enum defined once in `release.config.ts`'s `commitTypes` -- both commitlint and semantic-release derive from that single list.
|
|
147
163
|
|
|
148
164
|
## Gotchas and quirks
|
|
149
165
|
|
|
150
|
-
- `.attw.json` ignores
|
|
151
|
-
- `src/index.ts` mixing a default export with a named one
|
|
152
|
-
- Husky hooks: `pre-commit` runs lint-staged (`eslint --fix` on staged `*.ts`), `commit-msg` runs commitlint
|
|
153
|
-
- The CI release job sets `HUSKY=0` (
|
|
166
|
+
- `.attw.json` ignores `false-export-default`: tsdown/rolldown's CJS output for this plugin's sole default export doesn't emit the `export =` form `arethetypeswrong` wants under legacy `node10` resolution. The modes ESLint flat config uses (`node16`, `bundler`) are unaffected, so the rule is suppressed rather than changing the default-export shape.
|
|
167
|
+
- `src/index.ts` mixing a default export with a named one triggers rolldown's `MIXED_EXPORTS` warning: a raw CommonJS `require()` would see the raw exports object instead of the default. ESM `import` (the actual consumer path) resolves both correctly; `attw --pack` and `publint` report no problems, so the warning is accepted (see `tsdown.config.ts`).
|
|
168
|
+
- Husky hooks: `pre-commit` runs lint-staged (`eslint --fix` on staged `*.ts`), `commit-msg` runs commitlint, `pre-push` runs typecheck + test + build.
|
|
169
|
+
- The CI release job sets `HUSKY=0` (commit-msg hook skips the automated release commit) and blanks `NPM_TOKEN`/`NODE_AUTH_TOKEN` explicitly so an inherited token can't win over OIDC trusted publishing.
|
|
154
170
|
|
|
155
171
|
## Contributing
|
|
156
172
|
|
|
157
|
-
Conventional commits are enforced by
|
|
173
|
+
Conventional commits are enforced by a husky `commit-msg` hook and re-checked in CI. CI runs commitlint, lint, and typecheck+test+build+attw on every push and pull request; the release job runs only on push to `main`, after all pass.
|
|
158
174
|
|
|
159
175
|
## Release
|
|
160
176
|
|
|
161
|
-
Conventional commits drive [semantic-release](https://semantic-release.gitbook.io/semantic-release) on every push to `main`: version bump, `CHANGELOG.md`, GitHub Release, and
|
|
177
|
+
Conventional commits drive [semantic-release](https://semantic-release.gitbook.io/semantic-release) on every push to `main`: version bump, `CHANGELOG.md`, GitHub Release, and npm publish via OIDC (no stored token). A second CI job republishes the identical build under the unscoped alias `exadev-eslint-config`.
|
|
162
178
|
|
|
163
179
|
## License
|
|
164
180
|
|
package/dist/index.cjs
CHANGED
|
@@ -26,8 +26,261 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
//#endregion
|
|
27
27
|
let typescript_eslint = require("typescript-eslint");
|
|
28
28
|
typescript_eslint = __toESM(typescript_eslint, 1);
|
|
29
|
+
let node_path = require("node:path");
|
|
29
30
|
//#region package.json
|
|
30
|
-
var version = "2.
|
|
31
|
+
var version = "2.1.1";
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/rules/barrel-helpers.ts
|
|
34
|
+
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
35
|
+
function basenameOf(filename) {
|
|
36
|
+
const slash = filename.lastIndexOf("/");
|
|
37
|
+
return slash === -1 ? filename : filename.slice(slash + 1);
|
|
38
|
+
}
|
|
39
|
+
function isIndexFile(filename) {
|
|
40
|
+
return INDEX_BASENAME$1.test(basenameOf(filename));
|
|
41
|
+
}
|
|
42
|
+
function isMainBarrel(filename) {
|
|
43
|
+
return filename.endsWith("/src/index.ts");
|
|
44
|
+
}
|
|
45
|
+
function isPureReexport(statement) {
|
|
46
|
+
if (statement.type === "ExportAllDeclaration") return true;
|
|
47
|
+
return statement.type === "ExportNamedDeclaration" && statement.source !== null && statement.source !== void 0;
|
|
48
|
+
}
|
|
49
|
+
function isDirectSibling(specifier) {
|
|
50
|
+
if (!specifier.startsWith("./")) return false;
|
|
51
|
+
let rest = node_path.posix.normalize(specifier.slice(2));
|
|
52
|
+
if (rest.endsWith("/")) rest = rest.slice(0, -1);
|
|
53
|
+
return rest !== "." && rest !== ".." && rest !== "" && !rest.includes("/");
|
|
54
|
+
}
|
|
55
|
+
function isBarrelMode(value) {
|
|
56
|
+
return value === "banned" || value === "single" || value === "siblings";
|
|
57
|
+
}
|
|
58
|
+
function isPermittedBarrel(filename, mode) {
|
|
59
|
+
if (mode === "banned") return false;
|
|
60
|
+
if (mode === "single") return isMainBarrel(filename);
|
|
61
|
+
return isIndexFile(filename);
|
|
62
|
+
}
|
|
63
|
+
function createSplitReexportDetector() {
|
|
64
|
+
const importsByName = /* @__PURE__ */ new Map();
|
|
65
|
+
const bareExportSpecifiers = [];
|
|
66
|
+
const defaultExportDeclarations = [];
|
|
67
|
+
return {
|
|
68
|
+
visitImport(node) {
|
|
69
|
+
for (const specifier of node.specifiers) importsByName.set(specifier.local.name, {
|
|
70
|
+
declaration: node,
|
|
71
|
+
specifier
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
visitExportNamed(node) {
|
|
75
|
+
if (node.source !== null && node.source !== void 0) return;
|
|
76
|
+
for (const specifier of node.specifiers) bareExportSpecifiers.push({
|
|
77
|
+
declaration: node,
|
|
78
|
+
specifier
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
visitExportDefault(node) {
|
|
82
|
+
defaultExportDeclarations.push(node);
|
|
83
|
+
},
|
|
84
|
+
violations() {
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const { declaration, specifier } of bareExportSpecifiers) {
|
|
87
|
+
const name = specifier.local.type === "Identifier" ? specifier.local.name : void 0;
|
|
88
|
+
if (name === void 0) continue;
|
|
89
|
+
const trackedImport = importsByName.get(name);
|
|
90
|
+
if (trackedImport === void 0) continue;
|
|
91
|
+
out.push({
|
|
92
|
+
kind: "named",
|
|
93
|
+
specifier,
|
|
94
|
+
declaration,
|
|
95
|
+
name,
|
|
96
|
+
trackedImport
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (const declarationNode of defaultExportDeclarations) {
|
|
100
|
+
const name = declarationNode.declaration.type === "Identifier" ? declarationNode.declaration.name : void 0;
|
|
101
|
+
if (name === void 0) continue;
|
|
102
|
+
const trackedImport = importsByName.get(name);
|
|
103
|
+
if (trackedImport === void 0) continue;
|
|
104
|
+
out.push({
|
|
105
|
+
kind: "default",
|
|
106
|
+
declaration: declarationNode,
|
|
107
|
+
name,
|
|
108
|
+
trackedImport
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/rules/barrel-direct-siblings-only.ts
|
|
117
|
+
const barrelDirectSiblingsOnly = {
|
|
118
|
+
meta: {
|
|
119
|
+
type: "problem",
|
|
120
|
+
schema: [],
|
|
121
|
+
messages: { 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." }
|
|
122
|
+
},
|
|
123
|
+
create(context) {
|
|
124
|
+
if (!isIndexFile(context.filename)) return {};
|
|
125
|
+
return {
|
|
126
|
+
ExportNamedDeclaration(node) {
|
|
127
|
+
if (node.source === null || node.source === void 0) return;
|
|
128
|
+
const source = node.source.value;
|
|
129
|
+
if (typeof source !== "string") return;
|
|
130
|
+
if (!isDirectSibling(source)) context.report({
|
|
131
|
+
node,
|
|
132
|
+
messageId: "notADirectSibling",
|
|
133
|
+
data: { source }
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
ExportAllDeclaration(node) {
|
|
137
|
+
const source = node.source.value;
|
|
138
|
+
if (typeof source !== "string") return;
|
|
139
|
+
if (!isDirectSibling(source)) context.report({
|
|
140
|
+
node,
|
|
141
|
+
messageId: "notADirectSibling",
|
|
142
|
+
data: { source }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/rules/barrel-policy.ts
|
|
150
|
+
function readMode(options) {
|
|
151
|
+
if (options === void 0 || typeof options !== "object" || options === null || !("mode" in options) || !isBarrelMode(options.mode)) throw new Error("exadev/barrel-policy requires options: { mode: 'banned' | 'single' | 'siblings' }.");
|
|
152
|
+
return options.mode;
|
|
153
|
+
}
|
|
154
|
+
const barrelPolicy = {
|
|
155
|
+
meta: {
|
|
156
|
+
type: "problem",
|
|
157
|
+
schema: [{
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: { mode: {
|
|
160
|
+
type: "string",
|
|
161
|
+
enum: [
|
|
162
|
+
"banned",
|
|
163
|
+
"single",
|
|
164
|
+
"siblings"
|
|
165
|
+
]
|
|
166
|
+
} },
|
|
167
|
+
required: ["mode"],
|
|
168
|
+
additionalProperties: false
|
|
169
|
+
}],
|
|
170
|
+
messages: {
|
|
171
|
+
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.",
|
|
172
|
+
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.",
|
|
173
|
+
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 }}.",
|
|
174
|
+
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.",
|
|
175
|
+
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."
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
create(context) {
|
|
179
|
+
const mode = readMode(context.options[0]);
|
|
180
|
+
const filename = context.filename;
|
|
181
|
+
const detector = createSplitReexportDetector();
|
|
182
|
+
function hasSource(node) {
|
|
183
|
+
return node.source !== null && node.source !== void 0;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
Program(node) {
|
|
187
|
+
if (mode === "banned") {
|
|
188
|
+
if (isIndexFile(filename)) context.report({
|
|
189
|
+
node,
|
|
190
|
+
messageId: "indexFileBanned"
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (mode === "single") {
|
|
195
|
+
if (isIndexFile(filename) && !isMainBarrel(filename)) {
|
|
196
|
+
context.report({
|
|
197
|
+
node,
|
|
198
|
+
messageId: "nonMainIndexFile"
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (isMainBarrel(filename)) {
|
|
203
|
+
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
204
|
+
node: statement,
|
|
205
|
+
messageId: "sideEffectInBarrel",
|
|
206
|
+
data: { description: statement.type }
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (isIndexFile(filename)) {
|
|
212
|
+
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
213
|
+
node: statement,
|
|
214
|
+
messageId: "sideEffectInBarrel",
|
|
215
|
+
data: { description: statement.type }
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
ImportDeclaration: (node) => detector.visitImport(node),
|
|
220
|
+
ExportNamedDeclaration(node) {
|
|
221
|
+
detector.visitExportNamed(node);
|
|
222
|
+
if (hasSource(node)) {
|
|
223
|
+
const source = node.source === null || node.source === void 0 ? void 0 : node.source.value;
|
|
224
|
+
if (!isPermittedBarrel(filename, mode)) context.report({
|
|
225
|
+
node,
|
|
226
|
+
messageId: "reexportOutsideBarrel"
|
|
227
|
+
});
|
|
228
|
+
else if (mode === "siblings" && typeof source === "string" && !isDirectSibling(source)) context.report({
|
|
229
|
+
node,
|
|
230
|
+
messageId: "notADirectSibling",
|
|
231
|
+
data: { source }
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
ExportAllDeclaration(node) {
|
|
236
|
+
const source = node.source.value;
|
|
237
|
+
if (!isPermittedBarrel(filename, mode)) context.report({
|
|
238
|
+
node,
|
|
239
|
+
messageId: "reexportOutsideBarrel"
|
|
240
|
+
});
|
|
241
|
+
else if (mode === "siblings" && typeof source === "string" && !isDirectSibling(source)) context.report({
|
|
242
|
+
node,
|
|
243
|
+
messageId: "notADirectSibling",
|
|
244
|
+
data: { source }
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
ExportDefaultDeclaration: (node) => detector.visitExportDefault(node),
|
|
248
|
+
"Program:exit"() {
|
|
249
|
+
for (const violation of detector.violations()) if (isPermittedBarrel(filename, mode)) {
|
|
250
|
+
if (mode === "siblings") {
|
|
251
|
+
const importSource = violation.trackedImport.declaration.source.value;
|
|
252
|
+
if (typeof importSource === "string" && !isDirectSibling(importSource)) context.report({
|
|
253
|
+
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
254
|
+
messageId: "notADirectSibling",
|
|
255
|
+
data: { source: importSource }
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
} else context.report({
|
|
259
|
+
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
260
|
+
messageId: "reexportOutsideBarrel"
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/rules/no-index-files.ts
|
|
268
|
+
const noIndexFiles = {
|
|
269
|
+
meta: {
|
|
270
|
+
type: "problem",
|
|
271
|
+
schema: [],
|
|
272
|
+
messages: { indexFileBanned: "Index (barrel) files are banned -- import directly from the module that owns the export instead. Rename this file to something descriptive." }
|
|
273
|
+
},
|
|
274
|
+
create(context) {
|
|
275
|
+
if (!isIndexFile(context.filename)) return {};
|
|
276
|
+
return { Program(node) {
|
|
277
|
+
context.report({
|
|
278
|
+
node,
|
|
279
|
+
messageId: "indexFileBanned"
|
|
280
|
+
});
|
|
281
|
+
} };
|
|
282
|
+
}
|
|
283
|
+
};
|
|
31
284
|
//#endregion
|
|
32
285
|
//#region src/rules/no-non-barrel-index.ts
|
|
33
286
|
const INDEX_BASENAME = /^index\.[cm]?[tj]s$/;
|
|
@@ -75,39 +328,21 @@ const noNonBarrelReexport = {
|
|
|
75
328
|
fixable: "code",
|
|
76
329
|
schema: [],
|
|
77
330
|
messages: {
|
|
78
|
-
splitStatementReexport: "'{{ name }}' is imported here and handed straight back out via a bare export -- the identical re-export 'export { {{ name }} } from ...' would be, just split across two statements. Re-exports belong only in
|
|
79
|
-
splitStatementDefaultReexport: "'{{ name }}' is imported here and handed straight back out via `export default` -- the identical re-export 'export { {{ name }} as default } from ...' would be, just split across two statements. Re-exports belong only in
|
|
331
|
+
splitStatementReexport: "'{{ name }}' is imported here and handed straight back out via a bare export -- the identical re-export 'export { {{ name }} } from ...' would be, just split across two statements. Re-exports belong only in the public barrel.",
|
|
332
|
+
splitStatementDefaultReexport: "'{{ name }}' is imported here and handed straight back out via `export default` -- the identical re-export 'export { {{ name }} as default } from ...' would be, just split across two statements. Re-exports belong only in the public barrel."
|
|
80
333
|
}
|
|
81
334
|
},
|
|
82
335
|
create(context) {
|
|
83
|
-
if (context.filename
|
|
84
|
-
const
|
|
85
|
-
const bareExportSpecifiers = [];
|
|
86
|
-
const defaultExportDeclarations = [];
|
|
336
|
+
if (isIndexFile(context.filename)) return {};
|
|
337
|
+
const detector = createSplitReexportDetector();
|
|
87
338
|
return {
|
|
88
|
-
ImportDeclaration(node)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
specifier
|
|
92
|
-
});
|
|
93
|
-
},
|
|
94
|
-
ExportNamedDeclaration(node) {
|
|
95
|
-
if (node.source !== null && node.source !== void 0) return;
|
|
96
|
-
for (const specifier of node.specifiers) bareExportSpecifiers.push({
|
|
97
|
-
declaration: node,
|
|
98
|
-
specifier
|
|
99
|
-
});
|
|
100
|
-
},
|
|
101
|
-
ExportDefaultDeclaration(node) {
|
|
102
|
-
defaultExportDeclarations.push(node);
|
|
103
|
-
},
|
|
339
|
+
ImportDeclaration: (node) => detector.visitImport(node),
|
|
340
|
+
ExportNamedDeclaration: (node) => detector.visitExportNamed(node),
|
|
341
|
+
ExportDefaultDeclaration: (node) => detector.visitExportDefault(node),
|
|
104
342
|
"Program:exit"() {
|
|
105
343
|
const { sourceCode } = context;
|
|
106
|
-
for (const
|
|
107
|
-
const
|
|
108
|
-
if (name === void 0) continue;
|
|
109
|
-
const trackedImport = importsByName.get(name);
|
|
110
|
-
if (trackedImport === void 0) continue;
|
|
344
|
+
for (const violation of detector.violations()) if (violation.kind === "named") {
|
|
345
|
+
const { specifier, declaration, name, trackedImport } = violation;
|
|
111
346
|
context.report({
|
|
112
347
|
node: specifier,
|
|
113
348
|
messageId: "splitStatementReexport",
|
|
@@ -118,19 +353,15 @@ const noNonBarrelReexport = {
|
|
|
118
353
|
return fixes;
|
|
119
354
|
}
|
|
120
355
|
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const name = declarationNode.declaration.type === "Identifier" ? declarationNode.declaration.name : void 0;
|
|
124
|
-
if (name === void 0) continue;
|
|
125
|
-
const trackedImport = importsByName.get(name);
|
|
126
|
-
if (trackedImport === void 0) continue;
|
|
356
|
+
} else {
|
|
357
|
+
const { declaration, name, trackedImport } = violation;
|
|
127
358
|
context.report({
|
|
128
|
-
node:
|
|
359
|
+
node: declaration,
|
|
129
360
|
messageId: "splitStatementDefaultReexport",
|
|
130
361
|
data: { name },
|
|
131
362
|
fix(fixer) {
|
|
132
|
-
const fixes = [fixer.remove(
|
|
133
|
-
if (
|
|
363
|
+
const fixes = [fixer.remove(declaration)];
|
|
364
|
+
if (declaration.declaration.type === "Identifier" && importIsOnlyUsedByThisExport(sourceCode, trackedImport, declaration.declaration)) fixes.push(removeListMember(fixer, sourceCode, trackedImport.declaration, trackedImport.declaration.specifiers, trackedImport.specifier));
|
|
134
365
|
return fixes;
|
|
135
366
|
}
|
|
136
367
|
});
|
|
@@ -144,63 +375,6 @@ const noNonBarrelReexport = {
|
|
|
144
375
|
function isIdentifierReference(reference) {
|
|
145
376
|
return reference.identifier.type === "Identifier";
|
|
146
377
|
}
|
|
147
|
-
const noPointlessReassignment = {
|
|
148
|
-
meta: {
|
|
149
|
-
type: "problem",
|
|
150
|
-
fixable: "code",
|
|
151
|
-
schema: [],
|
|
152
|
-
messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
|
|
153
|
-
},
|
|
154
|
-
create(context) {
|
|
155
|
-
return { VariableDeclarator(node) {
|
|
156
|
-
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
157
|
-
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
158
|
-
const scope = context.sourceCode.getScope(node);
|
|
159
|
-
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
160
|
-
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
161
|
-
const aliasName = node.id.name;
|
|
162
|
-
const originalName = node.init.name;
|
|
163
|
-
context.report({
|
|
164
|
-
node,
|
|
165
|
-
messageId: "pointlessReassignment",
|
|
166
|
-
data: {
|
|
167
|
-
name: aliasName,
|
|
168
|
-
value: originalName
|
|
169
|
-
},
|
|
170
|
-
fix(fixer) {
|
|
171
|
-
const variable = scope.set.get(aliasName);
|
|
172
|
-
if (!variable) return null;
|
|
173
|
-
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
174
|
-
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
175
|
-
if (readRefs.some((reference) => {
|
|
176
|
-
const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
|
|
177
|
-
if (afterToken?.value === ":") return false;
|
|
178
|
-
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
179
|
-
let token = context.sourceCode.getTokenBefore(reference.identifier);
|
|
180
|
-
while (token) {
|
|
181
|
-
if (token.value === "{") return true;
|
|
182
|
-
if (token.value === "[" || token.value === "(") return false;
|
|
183
|
-
if (token.value === ":") return false;
|
|
184
|
-
token = context.sourceCode.getTokenBefore(token);
|
|
185
|
-
}
|
|
186
|
-
return false;
|
|
187
|
-
})) return null;
|
|
188
|
-
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
189
|
-
const declaration = node.parent;
|
|
190
|
-
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
191
|
-
fixes.push(fixer.remove(declaration));
|
|
192
|
-
return fixes;
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
} };
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
//#endregion
|
|
199
|
-
//#region src/rules/no-side-effects-in-index.ts
|
|
200
|
-
function isPureReexport(statement) {
|
|
201
|
-
if (statement.type === "ExportAllDeclaration") return true;
|
|
202
|
-
return statement.type === "ExportNamedDeclaration" && statement.source !== null && statement.source !== void 0;
|
|
203
|
-
}
|
|
204
378
|
//#endregion
|
|
205
379
|
//#region src/plugin.ts
|
|
206
380
|
const plugin = {
|
|
@@ -210,17 +384,70 @@ const plugin = {
|
|
|
210
384
|
namespace: "exadev"
|
|
211
385
|
},
|
|
212
386
|
rules: {
|
|
387
|
+
"barrel-direct-siblings-only": barrelDirectSiblingsOnly,
|
|
388
|
+
"barrel-policy": barrelPolicy,
|
|
389
|
+
"no-index-files": noIndexFiles,
|
|
213
390
|
"no-non-barrel-index": noNonBarrelIndex,
|
|
214
391
|
"no-non-barrel-reexport": noNonBarrelReexport,
|
|
215
|
-
"no-pointless-reassignment":
|
|
392
|
+
"no-pointless-reassignment": {
|
|
393
|
+
meta: {
|
|
394
|
+
type: "problem",
|
|
395
|
+
fixable: "code",
|
|
396
|
+
schema: [],
|
|
397
|
+
messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
|
|
398
|
+
},
|
|
399
|
+
create(context) {
|
|
400
|
+
return { VariableDeclarator(node) {
|
|
401
|
+
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
402
|
+
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
403
|
+
const scope = context.sourceCode.getScope(node);
|
|
404
|
+
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
405
|
+
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
406
|
+
const aliasName = node.id.name;
|
|
407
|
+
const originalName = node.init.name;
|
|
408
|
+
context.report({
|
|
409
|
+
node,
|
|
410
|
+
messageId: "pointlessReassignment",
|
|
411
|
+
data: {
|
|
412
|
+
name: aliasName,
|
|
413
|
+
value: originalName
|
|
414
|
+
},
|
|
415
|
+
fix(fixer) {
|
|
416
|
+
const variable = scope.set.get(aliasName);
|
|
417
|
+
if (!variable) return null;
|
|
418
|
+
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
419
|
+
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
420
|
+
if (readRefs.some((reference) => {
|
|
421
|
+
const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
|
|
422
|
+
if (afterToken?.value === ":") return false;
|
|
423
|
+
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
424
|
+
let token = context.sourceCode.getTokenBefore(reference.identifier);
|
|
425
|
+
while (token) {
|
|
426
|
+
if (token.value === "{") return true;
|
|
427
|
+
if (token.value === "[" || token.value === "(") return false;
|
|
428
|
+
if (token.value === ":") return false;
|
|
429
|
+
token = context.sourceCode.getTokenBefore(token);
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
})) return null;
|
|
433
|
+
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
434
|
+
const declaration = node.parent;
|
|
435
|
+
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
436
|
+
fixes.push(fixer.remove(declaration));
|
|
437
|
+
return fixes;
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
} };
|
|
441
|
+
}
|
|
442
|
+
},
|
|
216
443
|
"no-side-effects-in-index": {
|
|
217
444
|
meta: {
|
|
218
445
|
type: "problem",
|
|
219
446
|
schema: [],
|
|
220
|
-
messages: { notAPureReexport: "
|
|
447
|
+
messages: { notAPureReexport: "A barrel (index) file 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 }}." }
|
|
221
448
|
},
|
|
222
449
|
create(context) {
|
|
223
|
-
if (!context.filename
|
|
450
|
+
if (!isIndexFile(context.filename)) return {};
|
|
224
451
|
return { Program(node) {
|
|
225
452
|
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
226
453
|
node: statement,
|
|
@@ -237,21 +464,15 @@ const plugin = {
|
|
|
237
464
|
plugins: { exadev: plugin },
|
|
238
465
|
linterOptions: { noInlineConfig: true },
|
|
239
466
|
rules: {
|
|
240
|
-
"exadev/
|
|
241
|
-
"exadev/no-
|
|
242
|
-
"exadev/no-pointless-reassignment": "error",
|
|
243
|
-
"exadev/no-side-effects-in-index": "error"
|
|
467
|
+
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
468
|
+
"exadev/no-pointless-reassignment": "error"
|
|
244
469
|
}
|
|
245
470
|
};
|
|
246
471
|
},
|
|
247
472
|
get barrel() {
|
|
248
473
|
return {
|
|
249
474
|
plugins: { exadev: plugin },
|
|
250
|
-
rules: {
|
|
251
|
-
"exadev/no-non-barrel-index": "error",
|
|
252
|
-
"exadev/no-non-barrel-reexport": "error",
|
|
253
|
-
"exadev/no-side-effects-in-index": "error"
|
|
254
|
-
}
|
|
475
|
+
rules: { "exadev/barrel-policy": ["error", { mode: "single" }] }
|
|
255
476
|
};
|
|
256
477
|
}
|
|
257
478
|
}
|
|
@@ -266,10 +487,8 @@ const recommendedTypeChecked = [
|
|
|
266
487
|
plugins: { exadev: plugin },
|
|
267
488
|
linterOptions: { noInlineConfig: true },
|
|
268
489
|
rules: {
|
|
269
|
-
"exadev/
|
|
270
|
-
"exadev/no-non-barrel-reexport": "error",
|
|
490
|
+
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
271
491
|
"exadev/no-pointless-reassignment": "error",
|
|
272
|
-
"exadev/no-side-effects-in-index": "error",
|
|
273
492
|
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
274
493
|
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }]
|
|
275
494
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,259 @@
|
|
|
1
1
|
import tseslint from "typescript-eslint";
|
|
2
|
+
import { posix } from "node:path";
|
|
2
3
|
//#region package.json
|
|
3
|
-
var version = "2.
|
|
4
|
+
var version = "2.1.1";
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/rules/barrel-helpers.ts
|
|
7
|
+
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
8
|
+
function basenameOf(filename) {
|
|
9
|
+
const slash = filename.lastIndexOf("/");
|
|
10
|
+
return slash === -1 ? filename : filename.slice(slash + 1);
|
|
11
|
+
}
|
|
12
|
+
function isIndexFile(filename) {
|
|
13
|
+
return INDEX_BASENAME$1.test(basenameOf(filename));
|
|
14
|
+
}
|
|
15
|
+
function isMainBarrel(filename) {
|
|
16
|
+
return filename.endsWith("/src/index.ts");
|
|
17
|
+
}
|
|
18
|
+
function isPureReexport(statement) {
|
|
19
|
+
if (statement.type === "ExportAllDeclaration") return true;
|
|
20
|
+
return statement.type === "ExportNamedDeclaration" && statement.source !== null && statement.source !== void 0;
|
|
21
|
+
}
|
|
22
|
+
function isDirectSibling(specifier) {
|
|
23
|
+
if (!specifier.startsWith("./")) return false;
|
|
24
|
+
let rest = posix.normalize(specifier.slice(2));
|
|
25
|
+
if (rest.endsWith("/")) rest = rest.slice(0, -1);
|
|
26
|
+
return rest !== "." && rest !== ".." && rest !== "" && !rest.includes("/");
|
|
27
|
+
}
|
|
28
|
+
function isBarrelMode(value) {
|
|
29
|
+
return value === "banned" || value === "single" || value === "siblings";
|
|
30
|
+
}
|
|
31
|
+
function isPermittedBarrel(filename, mode) {
|
|
32
|
+
if (mode === "banned") return false;
|
|
33
|
+
if (mode === "single") return isMainBarrel(filename);
|
|
34
|
+
return isIndexFile(filename);
|
|
35
|
+
}
|
|
36
|
+
function createSplitReexportDetector() {
|
|
37
|
+
const importsByName = /* @__PURE__ */ new Map();
|
|
38
|
+
const bareExportSpecifiers = [];
|
|
39
|
+
const defaultExportDeclarations = [];
|
|
40
|
+
return {
|
|
41
|
+
visitImport(node) {
|
|
42
|
+
for (const specifier of node.specifiers) importsByName.set(specifier.local.name, {
|
|
43
|
+
declaration: node,
|
|
44
|
+
specifier
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
visitExportNamed(node) {
|
|
48
|
+
if (node.source !== null && node.source !== void 0) return;
|
|
49
|
+
for (const specifier of node.specifiers) bareExportSpecifiers.push({
|
|
50
|
+
declaration: node,
|
|
51
|
+
specifier
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
visitExportDefault(node) {
|
|
55
|
+
defaultExportDeclarations.push(node);
|
|
56
|
+
},
|
|
57
|
+
violations() {
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const { declaration, specifier } of bareExportSpecifiers) {
|
|
60
|
+
const name = specifier.local.type === "Identifier" ? specifier.local.name : void 0;
|
|
61
|
+
if (name === void 0) continue;
|
|
62
|
+
const trackedImport = importsByName.get(name);
|
|
63
|
+
if (trackedImport === void 0) continue;
|
|
64
|
+
out.push({
|
|
65
|
+
kind: "named",
|
|
66
|
+
specifier,
|
|
67
|
+
declaration,
|
|
68
|
+
name,
|
|
69
|
+
trackedImport
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
for (const declarationNode of defaultExportDeclarations) {
|
|
73
|
+
const name = declarationNode.declaration.type === "Identifier" ? declarationNode.declaration.name : void 0;
|
|
74
|
+
if (name === void 0) continue;
|
|
75
|
+
const trackedImport = importsByName.get(name);
|
|
76
|
+
if (trackedImport === void 0) continue;
|
|
77
|
+
out.push({
|
|
78
|
+
kind: "default",
|
|
79
|
+
declaration: declarationNode,
|
|
80
|
+
name,
|
|
81
|
+
trackedImport
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/rules/barrel-direct-siblings-only.ts
|
|
90
|
+
const barrelDirectSiblingsOnly = {
|
|
91
|
+
meta: {
|
|
92
|
+
type: "problem",
|
|
93
|
+
schema: [],
|
|
94
|
+
messages: { 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." }
|
|
95
|
+
},
|
|
96
|
+
create(context) {
|
|
97
|
+
if (!isIndexFile(context.filename)) return {};
|
|
98
|
+
return {
|
|
99
|
+
ExportNamedDeclaration(node) {
|
|
100
|
+
if (node.source === null || node.source === void 0) return;
|
|
101
|
+
const source = node.source.value;
|
|
102
|
+
if (typeof source !== "string") return;
|
|
103
|
+
if (!isDirectSibling(source)) context.report({
|
|
104
|
+
node,
|
|
105
|
+
messageId: "notADirectSibling",
|
|
106
|
+
data: { source }
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
ExportAllDeclaration(node) {
|
|
110
|
+
const source = node.source.value;
|
|
111
|
+
if (typeof source !== "string") return;
|
|
112
|
+
if (!isDirectSibling(source)) context.report({
|
|
113
|
+
node,
|
|
114
|
+
messageId: "notADirectSibling",
|
|
115
|
+
data: { source }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/rules/barrel-policy.ts
|
|
123
|
+
function readMode(options) {
|
|
124
|
+
if (options === void 0 || typeof options !== "object" || options === null || !("mode" in options) || !isBarrelMode(options.mode)) throw new Error("exadev/barrel-policy requires options: { mode: 'banned' | 'single' | 'siblings' }.");
|
|
125
|
+
return options.mode;
|
|
126
|
+
}
|
|
127
|
+
const barrelPolicy = {
|
|
128
|
+
meta: {
|
|
129
|
+
type: "problem",
|
|
130
|
+
schema: [{
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: { mode: {
|
|
133
|
+
type: "string",
|
|
134
|
+
enum: [
|
|
135
|
+
"banned",
|
|
136
|
+
"single",
|
|
137
|
+
"siblings"
|
|
138
|
+
]
|
|
139
|
+
} },
|
|
140
|
+
required: ["mode"],
|
|
141
|
+
additionalProperties: false
|
|
142
|
+
}],
|
|
143
|
+
messages: {
|
|
144
|
+
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.",
|
|
145
|
+
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.",
|
|
146
|
+
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 }}.",
|
|
147
|
+
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.",
|
|
148
|
+
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."
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
create(context) {
|
|
152
|
+
const mode = readMode(context.options[0]);
|
|
153
|
+
const filename = context.filename;
|
|
154
|
+
const detector = createSplitReexportDetector();
|
|
155
|
+
function hasSource(node) {
|
|
156
|
+
return node.source !== null && node.source !== void 0;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
Program(node) {
|
|
160
|
+
if (mode === "banned") {
|
|
161
|
+
if (isIndexFile(filename)) context.report({
|
|
162
|
+
node,
|
|
163
|
+
messageId: "indexFileBanned"
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (mode === "single") {
|
|
168
|
+
if (isIndexFile(filename) && !isMainBarrel(filename)) {
|
|
169
|
+
context.report({
|
|
170
|
+
node,
|
|
171
|
+
messageId: "nonMainIndexFile"
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (isMainBarrel(filename)) {
|
|
176
|
+
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
177
|
+
node: statement,
|
|
178
|
+
messageId: "sideEffectInBarrel",
|
|
179
|
+
data: { description: statement.type }
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (isIndexFile(filename)) {
|
|
185
|
+
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
186
|
+
node: statement,
|
|
187
|
+
messageId: "sideEffectInBarrel",
|
|
188
|
+
data: { description: statement.type }
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
ImportDeclaration: (node) => detector.visitImport(node),
|
|
193
|
+
ExportNamedDeclaration(node) {
|
|
194
|
+
detector.visitExportNamed(node);
|
|
195
|
+
if (hasSource(node)) {
|
|
196
|
+
const source = node.source === null || node.source === void 0 ? void 0 : node.source.value;
|
|
197
|
+
if (!isPermittedBarrel(filename, mode)) context.report({
|
|
198
|
+
node,
|
|
199
|
+
messageId: "reexportOutsideBarrel"
|
|
200
|
+
});
|
|
201
|
+
else if (mode === "siblings" && typeof source === "string" && !isDirectSibling(source)) context.report({
|
|
202
|
+
node,
|
|
203
|
+
messageId: "notADirectSibling",
|
|
204
|
+
data: { source }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
ExportAllDeclaration(node) {
|
|
209
|
+
const source = node.source.value;
|
|
210
|
+
if (!isPermittedBarrel(filename, mode)) context.report({
|
|
211
|
+
node,
|
|
212
|
+
messageId: "reexportOutsideBarrel"
|
|
213
|
+
});
|
|
214
|
+
else if (mode === "siblings" && typeof source === "string" && !isDirectSibling(source)) context.report({
|
|
215
|
+
node,
|
|
216
|
+
messageId: "notADirectSibling",
|
|
217
|
+
data: { source }
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
ExportDefaultDeclaration: (node) => detector.visitExportDefault(node),
|
|
221
|
+
"Program:exit"() {
|
|
222
|
+
for (const violation of detector.violations()) if (isPermittedBarrel(filename, mode)) {
|
|
223
|
+
if (mode === "siblings") {
|
|
224
|
+
const importSource = violation.trackedImport.declaration.source.value;
|
|
225
|
+
if (typeof importSource === "string" && !isDirectSibling(importSource)) context.report({
|
|
226
|
+
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
227
|
+
messageId: "notADirectSibling",
|
|
228
|
+
data: { source: importSource }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
} else context.report({
|
|
232
|
+
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
233
|
+
messageId: "reexportOutsideBarrel"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/rules/no-index-files.ts
|
|
241
|
+
const noIndexFiles = {
|
|
242
|
+
meta: {
|
|
243
|
+
type: "problem",
|
|
244
|
+
schema: [],
|
|
245
|
+
messages: { indexFileBanned: "Index (barrel) files are banned -- import directly from the module that owns the export instead. Rename this file to something descriptive." }
|
|
246
|
+
},
|
|
247
|
+
create(context) {
|
|
248
|
+
if (!isIndexFile(context.filename)) return {};
|
|
249
|
+
return { Program(node) {
|
|
250
|
+
context.report({
|
|
251
|
+
node,
|
|
252
|
+
messageId: "indexFileBanned"
|
|
253
|
+
});
|
|
254
|
+
} };
|
|
255
|
+
}
|
|
256
|
+
};
|
|
4
257
|
//#endregion
|
|
5
258
|
//#region src/rules/no-non-barrel-index.ts
|
|
6
259
|
const INDEX_BASENAME = /^index\.[cm]?[tj]s$/;
|
|
@@ -48,39 +301,21 @@ const noNonBarrelReexport = {
|
|
|
48
301
|
fixable: "code",
|
|
49
302
|
schema: [],
|
|
50
303
|
messages: {
|
|
51
|
-
splitStatementReexport: "'{{ name }}' is imported here and handed straight back out via a bare export -- the identical re-export 'export { {{ name }} } from ...' would be, just split across two statements. Re-exports belong only in
|
|
52
|
-
splitStatementDefaultReexport: "'{{ name }}' is imported here and handed straight back out via `export default` -- the identical re-export 'export { {{ name }} as default } from ...' would be, just split across two statements. Re-exports belong only in
|
|
304
|
+
splitStatementReexport: "'{{ name }}' is imported here and handed straight back out via a bare export -- the identical re-export 'export { {{ name }} } from ...' would be, just split across two statements. Re-exports belong only in the public barrel.",
|
|
305
|
+
splitStatementDefaultReexport: "'{{ name }}' is imported here and handed straight back out via `export default` -- the identical re-export 'export { {{ name }} as default } from ...' would be, just split across two statements. Re-exports belong only in the public barrel."
|
|
53
306
|
}
|
|
54
307
|
},
|
|
55
308
|
create(context) {
|
|
56
|
-
if (context.filename
|
|
57
|
-
const
|
|
58
|
-
const bareExportSpecifiers = [];
|
|
59
|
-
const defaultExportDeclarations = [];
|
|
309
|
+
if (isIndexFile(context.filename)) return {};
|
|
310
|
+
const detector = createSplitReexportDetector();
|
|
60
311
|
return {
|
|
61
|
-
ImportDeclaration(node)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
specifier
|
|
65
|
-
});
|
|
66
|
-
},
|
|
67
|
-
ExportNamedDeclaration(node) {
|
|
68
|
-
if (node.source !== null && node.source !== void 0) return;
|
|
69
|
-
for (const specifier of node.specifiers) bareExportSpecifiers.push({
|
|
70
|
-
declaration: node,
|
|
71
|
-
specifier
|
|
72
|
-
});
|
|
73
|
-
},
|
|
74
|
-
ExportDefaultDeclaration(node) {
|
|
75
|
-
defaultExportDeclarations.push(node);
|
|
76
|
-
},
|
|
312
|
+
ImportDeclaration: (node) => detector.visitImport(node),
|
|
313
|
+
ExportNamedDeclaration: (node) => detector.visitExportNamed(node),
|
|
314
|
+
ExportDefaultDeclaration: (node) => detector.visitExportDefault(node),
|
|
77
315
|
"Program:exit"() {
|
|
78
316
|
const { sourceCode } = context;
|
|
79
|
-
for (const
|
|
80
|
-
const
|
|
81
|
-
if (name === void 0) continue;
|
|
82
|
-
const trackedImport = importsByName.get(name);
|
|
83
|
-
if (trackedImport === void 0) continue;
|
|
317
|
+
for (const violation of detector.violations()) if (violation.kind === "named") {
|
|
318
|
+
const { specifier, declaration, name, trackedImport } = violation;
|
|
84
319
|
context.report({
|
|
85
320
|
node: specifier,
|
|
86
321
|
messageId: "splitStatementReexport",
|
|
@@ -91,19 +326,15 @@ const noNonBarrelReexport = {
|
|
|
91
326
|
return fixes;
|
|
92
327
|
}
|
|
93
328
|
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const name = declarationNode.declaration.type === "Identifier" ? declarationNode.declaration.name : void 0;
|
|
97
|
-
if (name === void 0) continue;
|
|
98
|
-
const trackedImport = importsByName.get(name);
|
|
99
|
-
if (trackedImport === void 0) continue;
|
|
329
|
+
} else {
|
|
330
|
+
const { declaration, name, trackedImport } = violation;
|
|
100
331
|
context.report({
|
|
101
|
-
node:
|
|
332
|
+
node: declaration,
|
|
102
333
|
messageId: "splitStatementDefaultReexport",
|
|
103
334
|
data: { name },
|
|
104
335
|
fix(fixer) {
|
|
105
|
-
const fixes = [fixer.remove(
|
|
106
|
-
if (
|
|
336
|
+
const fixes = [fixer.remove(declaration)];
|
|
337
|
+
if (declaration.declaration.type === "Identifier" && importIsOnlyUsedByThisExport(sourceCode, trackedImport, declaration.declaration)) fixes.push(removeListMember(fixer, sourceCode, trackedImport.declaration, trackedImport.declaration.specifiers, trackedImport.specifier));
|
|
107
338
|
return fixes;
|
|
108
339
|
}
|
|
109
340
|
});
|
|
@@ -117,63 +348,6 @@ const noNonBarrelReexport = {
|
|
|
117
348
|
function isIdentifierReference(reference) {
|
|
118
349
|
return reference.identifier.type === "Identifier";
|
|
119
350
|
}
|
|
120
|
-
const noPointlessReassignment = {
|
|
121
|
-
meta: {
|
|
122
|
-
type: "problem",
|
|
123
|
-
fixable: "code",
|
|
124
|
-
schema: [],
|
|
125
|
-
messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
|
|
126
|
-
},
|
|
127
|
-
create(context) {
|
|
128
|
-
return { VariableDeclarator(node) {
|
|
129
|
-
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
130
|
-
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
131
|
-
const scope = context.sourceCode.getScope(node);
|
|
132
|
-
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
133
|
-
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
134
|
-
const aliasName = node.id.name;
|
|
135
|
-
const originalName = node.init.name;
|
|
136
|
-
context.report({
|
|
137
|
-
node,
|
|
138
|
-
messageId: "pointlessReassignment",
|
|
139
|
-
data: {
|
|
140
|
-
name: aliasName,
|
|
141
|
-
value: originalName
|
|
142
|
-
},
|
|
143
|
-
fix(fixer) {
|
|
144
|
-
const variable = scope.set.get(aliasName);
|
|
145
|
-
if (!variable) return null;
|
|
146
|
-
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
147
|
-
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
148
|
-
if (readRefs.some((reference) => {
|
|
149
|
-
const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
|
|
150
|
-
if (afterToken?.value === ":") return false;
|
|
151
|
-
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
152
|
-
let token = context.sourceCode.getTokenBefore(reference.identifier);
|
|
153
|
-
while (token) {
|
|
154
|
-
if (token.value === "{") return true;
|
|
155
|
-
if (token.value === "[" || token.value === "(") return false;
|
|
156
|
-
if (token.value === ":") return false;
|
|
157
|
-
token = context.sourceCode.getTokenBefore(token);
|
|
158
|
-
}
|
|
159
|
-
return false;
|
|
160
|
-
})) return null;
|
|
161
|
-
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
162
|
-
const declaration = node.parent;
|
|
163
|
-
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
164
|
-
fixes.push(fixer.remove(declaration));
|
|
165
|
-
return fixes;
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
} };
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
//#endregion
|
|
172
|
-
//#region src/rules/no-side-effects-in-index.ts
|
|
173
|
-
function isPureReexport(statement) {
|
|
174
|
-
if (statement.type === "ExportAllDeclaration") return true;
|
|
175
|
-
return statement.type === "ExportNamedDeclaration" && statement.source !== null && statement.source !== void 0;
|
|
176
|
-
}
|
|
177
351
|
//#endregion
|
|
178
352
|
//#region src/plugin.ts
|
|
179
353
|
const plugin = {
|
|
@@ -183,17 +357,70 @@ const plugin = {
|
|
|
183
357
|
namespace: "exadev"
|
|
184
358
|
},
|
|
185
359
|
rules: {
|
|
360
|
+
"barrel-direct-siblings-only": barrelDirectSiblingsOnly,
|
|
361
|
+
"barrel-policy": barrelPolicy,
|
|
362
|
+
"no-index-files": noIndexFiles,
|
|
186
363
|
"no-non-barrel-index": noNonBarrelIndex,
|
|
187
364
|
"no-non-barrel-reexport": noNonBarrelReexport,
|
|
188
|
-
"no-pointless-reassignment":
|
|
365
|
+
"no-pointless-reassignment": {
|
|
366
|
+
meta: {
|
|
367
|
+
type: "problem",
|
|
368
|
+
fixable: "code",
|
|
369
|
+
schema: [],
|
|
370
|
+
messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
|
|
371
|
+
},
|
|
372
|
+
create(context) {
|
|
373
|
+
return { VariableDeclarator(node) {
|
|
374
|
+
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
375
|
+
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
376
|
+
const scope = context.sourceCode.getScope(node);
|
|
377
|
+
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
378
|
+
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
379
|
+
const aliasName = node.id.name;
|
|
380
|
+
const originalName = node.init.name;
|
|
381
|
+
context.report({
|
|
382
|
+
node,
|
|
383
|
+
messageId: "pointlessReassignment",
|
|
384
|
+
data: {
|
|
385
|
+
name: aliasName,
|
|
386
|
+
value: originalName
|
|
387
|
+
},
|
|
388
|
+
fix(fixer) {
|
|
389
|
+
const variable = scope.set.get(aliasName);
|
|
390
|
+
if (!variable) return null;
|
|
391
|
+
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
392
|
+
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
393
|
+
if (readRefs.some((reference) => {
|
|
394
|
+
const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
|
|
395
|
+
if (afterToken?.value === ":") return false;
|
|
396
|
+
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
397
|
+
let token = context.sourceCode.getTokenBefore(reference.identifier);
|
|
398
|
+
while (token) {
|
|
399
|
+
if (token.value === "{") return true;
|
|
400
|
+
if (token.value === "[" || token.value === "(") return false;
|
|
401
|
+
if (token.value === ":") return false;
|
|
402
|
+
token = context.sourceCode.getTokenBefore(token);
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
})) return null;
|
|
406
|
+
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
407
|
+
const declaration = node.parent;
|
|
408
|
+
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
409
|
+
fixes.push(fixer.remove(declaration));
|
|
410
|
+
return fixes;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
} };
|
|
414
|
+
}
|
|
415
|
+
},
|
|
189
416
|
"no-side-effects-in-index": {
|
|
190
417
|
meta: {
|
|
191
418
|
type: "problem",
|
|
192
419
|
schema: [],
|
|
193
|
-
messages: { notAPureReexport: "
|
|
420
|
+
messages: { notAPureReexport: "A barrel (index) file 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 }}." }
|
|
194
421
|
},
|
|
195
422
|
create(context) {
|
|
196
|
-
if (!context.filename
|
|
423
|
+
if (!isIndexFile(context.filename)) return {};
|
|
197
424
|
return { Program(node) {
|
|
198
425
|
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
199
426
|
node: statement,
|
|
@@ -210,21 +437,15 @@ const plugin = {
|
|
|
210
437
|
plugins: { exadev: plugin },
|
|
211
438
|
linterOptions: { noInlineConfig: true },
|
|
212
439
|
rules: {
|
|
213
|
-
"exadev/
|
|
214
|
-
"exadev/no-
|
|
215
|
-
"exadev/no-pointless-reassignment": "error",
|
|
216
|
-
"exadev/no-side-effects-in-index": "error"
|
|
440
|
+
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
441
|
+
"exadev/no-pointless-reassignment": "error"
|
|
217
442
|
}
|
|
218
443
|
};
|
|
219
444
|
},
|
|
220
445
|
get barrel() {
|
|
221
446
|
return {
|
|
222
447
|
plugins: { exadev: plugin },
|
|
223
|
-
rules: {
|
|
224
|
-
"exadev/no-non-barrel-index": "error",
|
|
225
|
-
"exadev/no-non-barrel-reexport": "error",
|
|
226
|
-
"exadev/no-side-effects-in-index": "error"
|
|
227
|
-
}
|
|
448
|
+
rules: { "exadev/barrel-policy": ["error", { mode: "single" }] }
|
|
228
449
|
};
|
|
229
450
|
}
|
|
230
451
|
}
|
|
@@ -239,10 +460,8 @@ const recommendedTypeChecked = [
|
|
|
239
460
|
plugins: { exadev: plugin },
|
|
240
461
|
linterOptions: { noInlineConfig: true },
|
|
241
462
|
rules: {
|
|
242
|
-
"exadev/
|
|
243
|
-
"exadev/no-non-barrel-reexport": "error",
|
|
463
|
+
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
244
464
|
"exadev/no-pointless-reassignment": "error",
|
|
245
|
-
"exadev/no-side-effects-in-index": "error",
|
|
246
465
|
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
247
466
|
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }]
|
|
248
467
|
}
|