@coryrylan/tools 0.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/dist/eslint/configs/browser.d.ts +12 -0
  4. package/dist/eslint/configs/browser.js +40 -0
  5. package/dist/eslint/configs/html.d.ts +10 -0
  6. package/dist/eslint/configs/html.js +38 -0
  7. package/dist/eslint/configs/json.d.ts +13 -0
  8. package/dist/eslint/configs/json.js +39 -0
  9. package/dist/eslint/configs/shared.d.ts +11 -0
  10. package/dist/eslint/configs/shared.js +19 -0
  11. package/dist/eslint/configs/tests.d.ts +8 -0
  12. package/dist/eslint/configs/tests.js +20 -0
  13. package/dist/eslint/configs/typescript.d.ts +18 -0
  14. package/dist/eslint/configs/typescript.js +104 -0
  15. package/dist/eslint/index.d.ts +27 -0
  16. package/dist/eslint/index.js +19 -0
  17. package/dist/eslint/plugin.d.ts +8 -0
  18. package/dist/eslint/plugin.js +36 -0
  19. package/dist/eslint/rules/consistent-error-messages.d.ts +10 -0
  20. package/dist/eslint/rules/consistent-error-messages.js +127 -0
  21. package/dist/eslint/rules/no-dead-code.d.ts +16 -0
  22. package/dist/eslint/rules/no-dead-code.js +110 -0
  23. package/dist/eslint/rules/no-deep-class-inheritance.d.ts +19 -0
  24. package/dist/eslint/rules/no-deep-class-inheritance.js +132 -0
  25. package/dist/eslint/rules/no-reexport-barrels.d.ts +10 -0
  26. package/dist/eslint/rules/no-reexport-barrels.js +55 -0
  27. package/dist/eslint/rules/no-single-consumer-abstraction.d.ts +3 -0
  28. package/dist/eslint/rules/no-single-consumer-abstraction.js +364 -0
  29. package/dist/eslint/rules/no-unjustified-disable.d.ts +11 -0
  30. package/dist/eslint/rules/no-unjustified-disable.js +75 -0
  31. package/dist/eslint/rules/no-unpinned-dependency-ranges.d.ts +46 -0
  32. package/dist/eslint/rules/no-unpinned-dependency-ranges.js +79 -0
  33. package/dist/eslint/rules/require-listener-cleanup.d.ts +8 -0
  34. package/dist/eslint/rules/require-listener-cleanup.js +202 -0
  35. package/dist/eslint/rules/require-observer-cleanup.d.ts +8 -0
  36. package/dist/eslint/rules/require-observer-cleanup.js +69 -0
  37. package/dist/eslint/rules/require-timer-cleanup.d.ts +10 -0
  38. package/dist/eslint/rules/require-timer-cleanup.js +142 -0
  39. package/dist/eslint/rules/utils.d.ts +45 -0
  40. package/dist/eslint/rules/utils.js +109 -0
  41. package/dist/prettier/index.d.ts +15 -0
  42. package/dist/prettier/index.js +26 -0
  43. package/dist/stylelint/index.d.ts +10 -0
  44. package/dist/stylelint/index.js +50 -0
  45. package/dist/vale/styles/config/vocabularies/Tools/accept.txt +80 -0
  46. package/dist/vale/styles/config/vocabularies/Tools/reject.txt +5 -0
  47. package/dist/vale/vale.ini +27 -0
  48. package/dist/vite/index.d.ts +37 -0
  49. package/dist/vite/index.js +61 -0
  50. package/dist/vite/plugins/dts.d.ts +26 -0
  51. package/dist/vite/plugins/dts.js +106 -0
  52. package/dist/vite/plugins/write-if-changed.d.ts +20 -0
  53. package/dist/vite/plugins/write-if-changed.js +33 -0
  54. package/dist/vitest/browser.d.ts +31 -0
  55. package/dist/vitest/browser.js +105 -0
  56. package/dist/vitest/index.d.ts +28 -0
  57. package/dist/vitest/index.js +34 -0
  58. package/package.json +200 -0
@@ -0,0 +1,36 @@
1
+ import rule from "./rules/no-dead-code.js";
2
+ import rule$1 from "./rules/no-deep-class-inheritance.js";
3
+ import rule$2 from "./rules/no-single-consumer-abstraction.js";
4
+ import rule$3 from "./rules/no-unjustified-disable.js";
5
+ import rule$4 from "./rules/no-reexport-barrels.js";
6
+ import rule$5 from "./rules/consistent-error-messages.js";
7
+ import rule$6 from "./rules/require-listener-cleanup.js";
8
+ import rule$7 from "./rules/require-observer-cleanup.js";
9
+ import rule$8 from "./rules/require-timer-cleanup.js";
10
+ import rule$9 from "./rules/no-unpinned-dependency-ranges.js";
11
+ /**
12
+ * The `tools/*` rule registry, shared by every config in `./configs/*.ts`
13
+ * under the `tools` namespace. Lives in its own module (rather than
14
+ * `index.ts`, where it was previously declared empty) so the configs can
15
+ * import it directly without a circular import back through `index.ts`.
16
+ */
17
+ var plugin = {
18
+ meta: {
19
+ name: "@coryrylan/tools",
20
+ version: "0.1.0"
21
+ },
22
+ rules: {
23
+ "no-dead-code": rule,
24
+ "no-deep-class-inheritance": rule$1,
25
+ "no-single-consumer-abstraction": rule$2,
26
+ "no-unjustified-disable": rule$3,
27
+ "no-reexport-barrels": rule$4,
28
+ "consistent-error-messages": rule$5,
29
+ "require-listener-cleanup": rule$6,
30
+ "require-observer-cleanup": rule$7,
31
+ "require-timer-cleanup": rule$8,
32
+ "no-unpinned-dependency-ranges": rule$9
33
+ }
34
+ };
35
+ //#endregion
36
+ export { plugin };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ESLint rule that requires thrown/constructed `Error`s (and subclasses) to
3
+ * carry a non-empty, informative message. Agents routinely emit placeholder
4
+ * throw sites (`new Error('error')`, `new Error('')`) that make production
5
+ * debugging impossible; deterministic message hygiene catches this before
6
+ * review has to.
7
+ */
8
+ import type { Rule } from 'eslint';
9
+ declare const rule: Rule.RuleModule;
10
+ export default rule;
@@ -0,0 +1,127 @@
1
+ //#region src/eslint/rules/consistent-error-messages.ts
2
+ var DEFAULT_DENYLIST = [
3
+ "error",
4
+ "err",
5
+ "failed",
6
+ "failure",
7
+ "oops",
8
+ "something went wrong",
9
+ "unknown error",
10
+ "invalid",
11
+ "bad"
12
+ ];
13
+ function readOptions(context) {
14
+ const provided = context.options[0];
15
+ return {
16
+ disallow: provided?.disallow ?? DEFAULT_DENYLIST,
17
+ allowLowercase: provided?.allowLowercase ?? false
18
+ };
19
+ }
20
+ /**
21
+ * Error-constructor-shaped identifier names this rule cares about (built-ins
22
+ * and custom `*Error` classes), excluding `AggregateError`. Requires a PascalCase
23
+ * (uppercase-first) name so ordinary helpers whose name merely ends in `Error`
24
+ * - `reportError`, `logError`, `handleError`, `onError` - are not mistaken for
25
+ * constructors; every built-in and conventional custom error class is PascalCase.
26
+ */
27
+ function isErrorConstructorName(name) {
28
+ return name !== "AggregateError" && /^[A-Z]/.test(name) && /Error$/.test(name);
29
+ }
30
+ /** Extracts the callee's identifier name, or `null` if the callee isn't a plain identifier. */
31
+ function calleeName(node) {
32
+ return node.callee.type === "Identifier" ? node.callee.name : null;
33
+ }
34
+ /** Shared trim/noise/lowercase analysis for a message's literal text, used for both string literals and template-literal leading quasis. */
35
+ function analyzeText(text, options) {
36
+ const trimmed = text.trim();
37
+ if (trimmed === "") return { messageId: "emptyMessage" };
38
+ const lower = trimmed.toLowerCase();
39
+ if (options.disallow.some((entry) => entry.toLowerCase() === lower)) return {
40
+ messageId: "noiseMessage",
41
+ text: trimmed
42
+ };
43
+ if (!options.allowLowercase && /^[a-z]/.test(trimmed)) return { messageId: "lowercaseMessage" };
44
+ return null;
45
+ }
46
+ /**
47
+ * A template literal starting with `${` (empty leading quasi plus at least
48
+ * one expression) is always treated as informative. Typed against the plain
49
+ * `estree` shape (not `Rule.Node`) because this is a call argument, not a
50
+ * traversed node, so it doesn't carry a `.parent` back-pointer.
51
+ */
52
+ function analyzeTemplateLiteral(node, options) {
53
+ const cooked = node.quasis[0]?.value.cooked ?? "";
54
+ if (node.expressions.length > 0 && cooked === "") return null;
55
+ return analyzeText(cooked, options);
56
+ }
57
+ /** Analyzes the first argument to a flagged Error constructor/call, or `null` if it's not a message-shaped literal worth checking. */
58
+ function analyzeFirstArgument(firstArgument, options) {
59
+ if (firstArgument.type === "Literal" && typeof firstArgument.value === "string") return analyzeText(firstArgument.value, options);
60
+ if (firstArgument.type === "TemplateLiteral") return analyzeTemplateLiteral(firstArgument, options);
61
+ return null;
62
+ }
63
+ function checkNode(context, node, options) {
64
+ const name = calleeName(node);
65
+ if (name === null || !isErrorConstructorName(name)) return;
66
+ const [firstArgument] = node.arguments;
67
+ if (!firstArgument) {
68
+ context.report({
69
+ node,
70
+ messageId: "missingMessage"
71
+ });
72
+ return;
73
+ }
74
+ const verdict = analyzeFirstArgument(firstArgument, options);
75
+ if (!verdict) return;
76
+ if (verdict.messageId === "noiseMessage") {
77
+ context.report({
78
+ node,
79
+ messageId: "noiseMessage",
80
+ data: { text: verdict.text ?? "" }
81
+ });
82
+ return;
83
+ }
84
+ context.report({
85
+ node,
86
+ messageId: verdict.messageId
87
+ });
88
+ }
89
+ var rule = {
90
+ meta: {
91
+ type: "problem",
92
+ docs: {
93
+ description: "Require Error (and subclass) constructor/call sites to carry a non-empty, informative message.",
94
+ recommended: true
95
+ },
96
+ schema: [{
97
+ type: "object",
98
+ additionalProperties: false,
99
+ properties: {
100
+ disallow: {
101
+ type: "array",
102
+ items: { type: "string" }
103
+ },
104
+ allowLowercase: { type: "boolean" }
105
+ }
106
+ }],
107
+ messages: {
108
+ missingMessage: "Construct errors with a message",
109
+ emptyMessage: "Error message must not be empty",
110
+ lowercaseMessage: "Error message should start with a capital letter or interpolated identifier",
111
+ noiseMessage: "Error message '{{text}}' is low-information noise; describe what failed and why"
112
+ }
113
+ },
114
+ create(context) {
115
+ const options = readOptions(context);
116
+ return {
117
+ NewExpression(node) {
118
+ checkNode(context, node, options);
119
+ },
120
+ CallExpression(node) {
121
+ checkNode(context, node, options);
122
+ }
123
+ };
124
+ }
125
+ };
126
+ //#endregion
127
+ export { rule as default };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Removing dead/commented-out code keeps an agent's context free of zombie
3
+ * snippets that look load-bearing but aren't. Version control already
4
+ * remembers deleted code, so a comment that only exists to preserve an old
5
+ * implementation is pure noise for the next agent that reads the file.
6
+ *
7
+ * @see https://kentcdodds.com/blog/please-dont-commit-commented-out-code
8
+ */
9
+ import type { Rule } from 'eslint';
10
+ /** Options for the `no-dead-code` rule. */
11
+ export interface NoDeadCodeOptions {
12
+ /** Regex source strings; a comment whose text matches any of these is never flagged. */
13
+ readonly allowPatterns: readonly string[];
14
+ }
15
+ declare const rule: Rule.RuleModule;
16
+ export default rule;
@@ -0,0 +1,110 @@
1
+ //#region src/eslint/rules/no-dead-code.ts
2
+ var DEFAULT_ALLOW_PATTERNS = [];
3
+ /**
4
+ * Comments that exist to talk to tooling rather than to a reader - ESLint
5
+ * directive comments, TypeScript pragma comments, Prettier's ignore marker,
6
+ * and c8/istanbul coverage markers - are never dead code, no matter what
7
+ * text follows the marker. Checked against the trimmed comment text.
8
+ */
9
+ var DIRECTIVE_COMMENT_PATTERN = /^(?:eslint-disable|eslint-enable|eslint-env|globals?\b|exported\b|@ts-|prettier-ignore|c8\b|istanbul\b)/;
10
+ /**
11
+ * Heuristics for "this comment contains commented-out source code", ported
12
+ * from the rule this replaces: each pattern targets one JS/TS construct that
13
+ * shows up in real code but essentially never in prose (see inline comments
14
+ * on patterns that need extra care to avoid false positives).
15
+ */
16
+ var CODE_LIKE_PATTERNS = {
17
+ importStatement: /\s*import\s+.*from\s+['"].*['"];/,
18
+ exportStatement: /\s*export\s+.*from\s+['"].*['"];/,
19
+ functionKeyword: /\bfunction\b\s+(\w+)\s*\(/,
20
+ arrowFunction: /\([^()]*\)\s*=>|[(=]\s*\w+\s*=>|=>\s*[{(]/,
21
+ variableDeclaration: /(?:^|\s)(?:const|let|var)\s+(\w+|\[.*\])\s*(?:[=,;]|$)/,
22
+ controlFlowKeyword: /\b(?:if|else|try|catch|switch|while|for|do)\b\s*\(|^(?:if|else|try|catch|switch|while|for|do)$/,
23
+ returnStatement: /\breturn\b[^;]*;/,
24
+ consoleLog: /\bconsole\s*\.?\s*log\b\s*\(/,
25
+ debuggerStatement: /\bdebugger\b/,
26
+ describeBlock: /\bdescribe\(/,
27
+ itBlock: /\bit\(/
28
+ };
29
+ /** Whether `text` opens with a marker that tooling (not a human) reads. */
30
+ function isDirectiveComment(text) {
31
+ return DIRECTIVE_COMMENT_PATTERN.test(text);
32
+ }
33
+ /**
34
+ * Strips markdown-style inline code spans (`` `...` ``) before running the
35
+ * commented-out-code heuristics. Prose and JSDoc routinely reference a
36
+ * construct by name for illustration (`` `it(...)` ``, `` `export * from
37
+ * './mod.js';` ``) and real dead code is never itself wrapped in backticks,
38
+ * so anything that only "looks like code" inside a backtick span is a doc
39
+ * reference, not a commented-out statement.
40
+ */
41
+ function stripInlineCodeSpans(text) {
42
+ return text.replace(/`[^`]*`/g, "");
43
+ }
44
+ /** Whether `text` matches any of the commented-out-code heuristics. */
45
+ function looksLikeCode(text) {
46
+ const withoutInlineCode = stripInlineCodeSpans(text);
47
+ return Object.values(CODE_LIKE_PATTERNS).some((pattern) => pattern.test(withoutInlineCode));
48
+ }
49
+ function matchesAnyPattern(text, patterns) {
50
+ return patterns.some((pattern) => pattern.test(text));
51
+ }
52
+ function readOptions(context) {
53
+ return { allowPatterns: context.options[0]?.allowPatterns ?? DEFAULT_ALLOW_PATTERNS };
54
+ }
55
+ /**
56
+ * The JSON schema only validates `allowPatterns` entries as strings, so a
57
+ * malformed regex source (e.g. `'[test'`) reaches here uncompiled. Skipping a
58
+ * pattern that fails to compile - rather than letting `new RegExp` throw -
59
+ * keeps one bad option entry from crashing the entire lint run.
60
+ */
61
+ function tryCompile(source) {
62
+ try {
63
+ return new RegExp(source);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ function compileAllowPatterns(sources) {
69
+ const patterns = [];
70
+ for (const source of sources) {
71
+ const compiled = tryCompile(source);
72
+ if (compiled !== null) patterns.push(compiled);
73
+ }
74
+ return patterns;
75
+ }
76
+ function shouldSkipComment(text, allowPatterns) {
77
+ return text === "" || isDirectiveComment(text) || matchesAnyPattern(text, allowPatterns);
78
+ }
79
+ function checkComment(context, comment, allowPatterns) {
80
+ const text = comment.value.trim();
81
+ if (shouldSkipComment(text, allowPatterns) || !looksLikeCode(text)) return;
82
+ context.report({
83
+ node: comment,
84
+ messageId: "unexpected-dead-code",
85
+ data: { type: comment.type.toLowerCase() }
86
+ });
87
+ }
88
+ var rule = {
89
+ meta: {
90
+ type: "problem",
91
+ docs: { description: "Disallow dead code paths or dead source-code comment blocks." },
92
+ schema: [{
93
+ type: "object",
94
+ additionalProperties: false,
95
+ properties: { allowPatterns: {
96
+ type: "array",
97
+ items: { type: "string" }
98
+ } }
99
+ }],
100
+ messages: { "unexpected-dead-code": "Remove dead/commented {{type}} to avoid incorrect context." }
101
+ },
102
+ create(context) {
103
+ const allowPatterns = compileAllowPatterns(readOptions(context).allowPatterns);
104
+ return { Program() {
105
+ for (const comment of context.sourceCode.getAllComments()) checkComment(context, comment, allowPatterns);
106
+ } };
107
+ }
108
+ };
109
+ //#endregion
110
+ export { rule as default };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Agents tend to solve "I need a slightly different X" by subclassing X
3
+ * instead of composing behavior, and the habit compounds: each new layer
4
+ * makes sense in isolation, but understanding any one class now requires
5
+ * reading the whole chain up to its root. This rule counts superclass hops
6
+ * and reports once a class's inheritance chain passes a configured maximum,
7
+ * so local wrappers around an allowed root stay possible without letting the
8
+ * hierarchy grow without bound.
9
+ */
10
+ import type { Rule } from 'eslint';
11
+ /** Options for the `no-deep-class-inheritance` rule. */
12
+ export interface NoDeepClassInheritanceOptions {
13
+ /** Maximum number of superclass hops allowed before an allowed root is reached. */
14
+ readonly maxDepth: number;
15
+ /** Class names that end the depth count early once reached. */
16
+ readonly allowedRoots: readonly string[];
17
+ }
18
+ declare const rule: Rule.RuleModule;
19
+ export default rule;
@@ -0,0 +1,132 @@
1
+ //#region src/eslint/rules/no-deep-class-inheritance.ts
2
+ var DEFAULT_MAX_DEPTH = 2;
3
+ var DEFAULT_ALLOWED_ROOTS = [];
4
+ var ANONYMOUS_CLASS_NAME = "<anonymous>";
5
+ function hasTypeAwareParserServices(services) {
6
+ if (typeof services !== "object" || services === null) return false;
7
+ const candidate = services;
8
+ return Boolean(candidate.program) && Boolean(candidate.esTreeNodeToTSNodeMap);
9
+ }
10
+ /** Returns `null` when the active parser doesn't provide type information (e.g. plain espree). */
11
+ function getTypeAwareParserServices(context) {
12
+ const services = context.sourceCode.parserServices;
13
+ return hasTypeAwareParserServices(services) ? services : null;
14
+ }
15
+ function readOptions(context) {
16
+ const provided = context.options[0];
17
+ return {
18
+ maxDepth: provided?.maxDepth ?? DEFAULT_MAX_DEPTH,
19
+ allowedRoots: provided?.allowedRoots ?? DEFAULT_ALLOWED_ROOTS
20
+ };
21
+ }
22
+ function getClassName(node) {
23
+ return node.id?.name ?? ANONYMOUS_CLASS_NAME;
24
+ }
25
+ /** The `extends` heritage clause of a class-like declaration, if it has one (as opposed to `implements`). */
26
+ function getExtendsExpression(classDeclaration) {
27
+ return (classDeclaration.heritageClauses?.find((clause) => clause.getText().startsWith("extends")))?.types[0]?.expression ?? null;
28
+ }
29
+ /**
30
+ * Structural stand-in for `ts.isClassLike`, again to keep typescript out of the
31
+ * runtime import graph. Declaration merging means this can also match an
32
+ * interface with the same name; interfaces use `extends` heritage clauses too,
33
+ * so chain walking still terminates correctly in that case.
34
+ */
35
+ function isClassLikeDeclaration(declaration) {
36
+ const candidate = declaration;
37
+ return Array.isArray(candidate.members) && candidate.heritageClauses !== void 0;
38
+ }
39
+ /** The display name for one link in the inheritance chain: the resolved symbol's name, or the raw source text if it has none. */
40
+ function resolveClassName(symbol, expression) {
41
+ return symbol?.getName() ?? expression.getText();
42
+ }
43
+ /**
44
+ * The next superclass expression to follow from `symbol`'s declaration, or
45
+ * `null` once there's no further (or already-visited) class-like declaration
46
+ * to walk into. Marks the declaration visited as a side effect so `visited`
47
+ * stays accurate for the caller's next iteration.
48
+ */
49
+ function resolveNextExpression(symbol, visited) {
50
+ const declaration = symbol?.declarations?.find(isClassLikeDeclaration);
51
+ if (!declaration || visited.has(declaration)) return null;
52
+ visited.add(declaration);
53
+ return getExtendsExpression(declaration);
54
+ }
55
+ /**
56
+ * Walks superclass hops starting from `superClass`, collecting each link's
57
+ * display name, until it runs out of resolvable declarations or hits a name
58
+ * in `allowedRoots`. `visited` guards against revisiting the same
59
+ * declaration if a project's types ever formed a cycle.
60
+ */
61
+ function getInheritanceChain(superClass, checker, allowedRoots) {
62
+ const chain = [];
63
+ const visited = /* @__PURE__ */ new Set();
64
+ let expression = superClass;
65
+ while (expression) {
66
+ const symbol = checker.getTypeAtLocation(expression).getSymbol();
67
+ const className = resolveClassName(symbol, expression);
68
+ chain.push(className);
69
+ if (allowedRoots.has(className)) break;
70
+ expression = resolveNextExpression(symbol, visited);
71
+ }
72
+ return chain;
73
+ }
74
+ function reportTooDeep(context, report) {
75
+ const className = getClassName(report.node);
76
+ context.report({
77
+ node: report.node,
78
+ messageId: "too-deep",
79
+ data: {
80
+ className,
81
+ depth: String(report.chain.length),
82
+ maxDepth: String(report.maxDepth),
83
+ chain: `${className} -> ${report.chain.join(" -> ")}`
84
+ }
85
+ });
86
+ }
87
+ function checkClassDeclaration(context, node, config) {
88
+ if (!node.superClass) return;
89
+ const services = getTypeAwareParserServices(context);
90
+ if (!services) return;
91
+ const chain = getInheritanceChain(services.esTreeNodeToTSNodeMap.get(node.superClass), services.program.getTypeChecker(), config.allowedRoots);
92
+ if (chain.length <= config.maxDepth) return;
93
+ reportTooDeep(context, {
94
+ node,
95
+ chain,
96
+ maxDepth: config.maxDepth
97
+ });
98
+ }
99
+ var rule = {
100
+ meta: {
101
+ type: "problem",
102
+ docs: { description: "Disallow class inheritance chains deeper than the configured maximum." },
103
+ schema: [{
104
+ type: "object",
105
+ additionalProperties: false,
106
+ properties: {
107
+ maxDepth: {
108
+ type: "integer",
109
+ minimum: 1
110
+ },
111
+ allowedRoots: {
112
+ type: "array",
113
+ items: { type: "string" },
114
+ uniqueItems: true
115
+ }
116
+ }
117
+ }],
118
+ messages: { "too-deep": "`{{className}}` has inheritance depth {{depth}} (`{{chain}}`). Maximum allowed depth is {{maxDepth}}." }
119
+ },
120
+ create(context) {
121
+ const options = readOptions(context);
122
+ const config = {
123
+ maxDepth: options.maxDepth,
124
+ allowedRoots: new Set(options.allowedRoots)
125
+ };
126
+ return { ClassDeclaration(node) {
127
+ checkClassDeclaration(context, node, config);
128
+ } };
129
+ }
130
+ };
131
+ //#endregion
132
+ export { rule as default };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ESLint rule that flags files re-exporting from more modules than
3
+ * `maxReexports` allows. Agents follow barrel files (`export * from './x'`)
4
+ * transitively - reading one barrel to find a symbol drags an entire
5
+ * dependency closure into context. Deep barrels also block tree-shaking and
6
+ * are a common source of import cycles.
7
+ */
8
+ import type { Rule } from 'eslint';
9
+ declare const rule: Rule.RuleModule;
10
+ export default rule;
@@ -0,0 +1,55 @@
1
+ //#region src/eslint/rules/no-reexport-barrels.ts
2
+ var DEFAULT_MAX_REEXPORTS = 5;
3
+ function readOptions(context) {
4
+ const provided = context.options[0];
5
+ return {
6
+ maxReexports: provided?.maxReexports ?? DEFAULT_MAX_REEXPORTS,
7
+ allowNamed: provided?.allowNamed ?? true
8
+ };
9
+ }
10
+ var rule = {
11
+ meta: {
12
+ type: "suggestion",
13
+ docs: {
14
+ description: "Disallow re-exporting from more than a configured number of modules (barrel files).",
15
+ recommended: true
16
+ },
17
+ schema: [{
18
+ type: "object",
19
+ additionalProperties: false,
20
+ properties: {
21
+ maxReexports: {
22
+ type: "integer",
23
+ minimum: 0
24
+ },
25
+ allowNamed: { type: "boolean" }
26
+ }
27
+ }],
28
+ messages: { tooManyReexports: "This file re-exports from {{count}} modules (max {{max}}). Barrel files bloat agent context and break tree-shaking; import from concrete modules instead." }
29
+ },
30
+ create(context) {
31
+ const options = readOptions(context);
32
+ const reexports = [];
33
+ return {
34
+ ExportAllDeclaration(node) {
35
+ reexports.push(node);
36
+ },
37
+ ExportNamedDeclaration(node) {
38
+ if (!options.allowNamed && node.source) reexports.push(node);
39
+ },
40
+ "Program:exit"() {
41
+ if (reexports.length <= options.maxReexports) return;
42
+ for (const node of reexports) context.report({
43
+ node,
44
+ messageId: "tooManyReexports",
45
+ data: {
46
+ count: reexports.length,
47
+ max: options.maxReexports
48
+ }
49
+ });
50
+ }
51
+ };
52
+ }
53
+ };
54
+ //#endregion
55
+ export { rule as default };
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;