@exadev/eslint-config 1.4.1 → 2.1.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/dist/index.cjs CHANGED
@@ -1,2 +1,506 @@
1
- const require_plugin = require("./plugin-BscQLyGY.cjs");
2
- module.exports = require_plugin.plugin;
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ //#region \0rolldown/runtime.js
6
+ var __create = Object.create;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
14
+ key = keys[i];
15
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
16
+ get: ((k) => from[k]).bind(null, key),
17
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
18
+ });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+ //#endregion
27
+ let typescript_eslint = require("typescript-eslint");
28
+ typescript_eslint = __toESM(typescript_eslint, 1);
29
+ let node_path = require("node:path");
30
+ //#region package.json
31
+ var version = "2.1.0";
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
+ };
284
+ //#endregion
285
+ //#region src/rules/no-non-barrel-index.ts
286
+ const INDEX_BASENAME = /^index\.[cm]?[tj]s$/;
287
+ const noNonBarrelIndex = {
288
+ meta: {
289
+ type: "problem",
290
+ schema: [],
291
+ messages: { barrel: "Only src/index.ts may be named index.* (the public convenience barrel); give any other module a descriptive filename." }
292
+ },
293
+ create(context) {
294
+ const filename = context.filename;
295
+ const slash = filename.lastIndexOf("/");
296
+ const basename = slash === -1 ? filename : filename.slice(slash + 1);
297
+ if (!INDEX_BASENAME.test(basename)) return {};
298
+ if (filename.endsWith("/src/index.ts")) return {};
299
+ return { Program(node) {
300
+ context.report({
301
+ node,
302
+ messageId: "barrel"
303
+ });
304
+ } };
305
+ }
306
+ };
307
+ //#endregion
308
+ //#region src/rules/no-non-barrel-reexport.ts
309
+ function removeListMember(fixer, sourceCode, declaration, members, target) {
310
+ if (members.length === 1) return fixer.remove(declaration);
311
+ const targetIndex = members.indexOf(target);
312
+ const isLast = targetIndex === members.length - 1;
313
+ const neighbor = members[isLast ? targetIndex - 1 : targetIndex + 1];
314
+ if (neighbor === void 0) throw new Error("Unreachable: a list with more than one member always has a neighbor either side of any member within it.");
315
+ return isLast ? fixer.removeRange([sourceCode.getRange(neighbor)[1], sourceCode.getRange(target)[1]]) : fixer.removeRange([sourceCode.getRange(target)[0], sourceCode.getRange(neighbor)[0]]);
316
+ }
317
+ function importIsOnlyUsedByThisExport(sourceCode, trackedImport, usageIdentifier) {
318
+ const variable = sourceCode.getDeclaredVariables(trackedImport.declaration).find((candidate) => candidate.defs.some((def) => def.node === trackedImport.specifier));
319
+ if (variable === void 0) return false;
320
+ if (variable.references.length !== 1) return false;
321
+ const [onlyReference] = variable.references;
322
+ if (onlyReference === void 0) throw new Error("Unreachable: the length check above guarantees exactly one element.");
323
+ return onlyReference.identifier === usageIdentifier;
324
+ }
325
+ const noNonBarrelReexport = {
326
+ meta: {
327
+ type: "problem",
328
+ fixable: "code",
329
+ schema: [],
330
+ messages: {
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."
333
+ }
334
+ },
335
+ create(context) {
336
+ if (isIndexFile(context.filename)) return {};
337
+ const detector = createSplitReexportDetector();
338
+ return {
339
+ ImportDeclaration: (node) => detector.visitImport(node),
340
+ ExportNamedDeclaration: (node) => detector.visitExportNamed(node),
341
+ ExportDefaultDeclaration: (node) => detector.visitExportDefault(node),
342
+ "Program:exit"() {
343
+ const { sourceCode } = context;
344
+ for (const violation of detector.violations()) if (violation.kind === "named") {
345
+ const { specifier, declaration, name, trackedImport } = violation;
346
+ context.report({
347
+ node: specifier,
348
+ messageId: "splitStatementReexport",
349
+ data: { name },
350
+ fix(fixer) {
351
+ const fixes = [removeListMember(fixer, sourceCode, declaration, declaration.specifiers, specifier)];
352
+ if (specifier.local.type === "Identifier" && importIsOnlyUsedByThisExport(sourceCode, trackedImport, specifier.local)) fixes.push(removeListMember(fixer, sourceCode, trackedImport.declaration, trackedImport.declaration.specifiers, trackedImport.specifier));
353
+ return fixes;
354
+ }
355
+ });
356
+ } else {
357
+ const { declaration, name, trackedImport } = violation;
358
+ context.report({
359
+ node: declaration,
360
+ messageId: "splitStatementDefaultReexport",
361
+ data: { name },
362
+ fix(fixer) {
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));
365
+ return fixes;
366
+ }
367
+ });
368
+ }
369
+ }
370
+ };
371
+ }
372
+ };
373
+ //#endregion
374
+ //#region src/rules/no-pointless-reassignment.ts
375
+ function isIdentifierReference(reference) {
376
+ return reference.identifier.type === "Identifier";
377
+ }
378
+ //#endregion
379
+ //#region src/plugin.ts
380
+ const plugin = {
381
+ meta: {
382
+ name: "@exadev/eslint-config",
383
+ version,
384
+ namespace: "exadev"
385
+ },
386
+ rules: {
387
+ "barrel-direct-siblings-only": barrelDirectSiblingsOnly,
388
+ "barrel-policy": barrelPolicy,
389
+ "no-index-files": noIndexFiles,
390
+ "no-non-barrel-index": noNonBarrelIndex,
391
+ "no-non-barrel-reexport": noNonBarrelReexport,
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
+ },
443
+ "no-side-effects-in-index": {
444
+ meta: {
445
+ type: "problem",
446
+ schema: [],
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 }}." }
448
+ },
449
+ create(context) {
450
+ if (!isIndexFile(context.filename)) return {};
451
+ return { Program(node) {
452
+ for (const statement of node.body) if (!isPureReexport(statement)) context.report({
453
+ node: statement,
454
+ messageId: "notAPureReexport",
455
+ data: { description: statement.type }
456
+ });
457
+ } };
458
+ }
459
+ }
460
+ },
461
+ configs: {
462
+ get recommended() {
463
+ return {
464
+ plugins: { exadev: plugin },
465
+ linterOptions: { noInlineConfig: true },
466
+ rules: {
467
+ "exadev/barrel-policy": ["error", { mode: "banned" }],
468
+ "exadev/no-pointless-reassignment": "error"
469
+ }
470
+ };
471
+ },
472
+ get barrel() {
473
+ return {
474
+ plugins: { exadev: plugin },
475
+ rules: { "exadev/barrel-policy": ["error", { mode: "single" }] }
476
+ };
477
+ }
478
+ }
479
+ };
480
+ //#endregion
481
+ //#region src/recommended-type-checked.ts
482
+ const TEST_FILE_PATTERNS = "**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
483
+ const recommendedTypeChecked = [
484
+ ...typescript_eslint.default.configs.recommendedTypeChecked,
485
+ ...typescript_eslint.default.configs.stylisticTypeChecked,
486
+ {
487
+ plugins: { exadev: plugin },
488
+ linterOptions: { noInlineConfig: true },
489
+ rules: {
490
+ "exadev/barrel-policy": ["error", { mode: "banned" }],
491
+ "exadev/no-pointless-reassignment": "error",
492
+ "@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
493
+ "@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }]
494
+ }
495
+ },
496
+ {
497
+ files: [TEST_FILE_PATTERNS],
498
+ rules: {
499
+ "@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": "allow-with-description" }],
500
+ "@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }]
501
+ }
502
+ }
503
+ ];
504
+ //#endregion
505
+ exports.default = recommendedTypeChecked;
506
+ exports.plugin = plugin;
package/dist/index.d.cts CHANGED
@@ -1,2 +1,10 @@
1
- import plugin from "./plugin.cjs";
2
- export = plugin;
1
+ import { ESLint } from "eslint";
2
+ //#region src/recommended-type-checked.d.ts
3
+ type ConfigValue = NonNullable<ESLint.Plugin['configs']>[string];
4
+ type ConfigArrayValue = Extract<ConfigValue, unknown[]>;
5
+ declare const recommendedTypeChecked: ConfigArrayValue;
6
+ //#endregion
7
+ //#region src/plugin.d.ts
8
+ declare const plugin: ESLint.Plugin;
9
+ //#endregion
10
+ export { recommendedTypeChecked as default, plugin };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,10 @@
1
- import plugin from "./plugin.js";
2
- export { plugin as default };
1
+ import { ESLint } from "eslint";
2
+ //#region src/recommended-type-checked.d.ts
3
+ type ConfigValue = NonNullable<ESLint.Plugin['configs']>[string];
4
+ type ConfigArrayValue = Extract<ConfigValue, unknown[]>;
5
+ declare const recommendedTypeChecked: ConfigArrayValue;
6
+ //#endregion
7
+ //#region src/plugin.d.ts
8
+ declare const plugin: ESLint.Plugin;
9
+ //#endregion
10
+ export { recommendedTypeChecked as default, plugin };