Skip to content
This repository was archived by the owner on Aug 20, 2026. It is now read-only.

Commit 18f8093

Browse files
committed
chore(lint): ban index.* filenames outside the src/index.ts barrel
1 parent b6f0a6e commit 18f8093

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Rule } from 'eslint';
2+
3+
// Forward guard: bans any file whose basename is index.[cm]?[tj]s except the single allowed public barrel at src/index.ts. An audit confirmed every file in this package currently has only src/index.ts and no other index.* file, so this rule flags nothing today -- it exists to keep a second barrel from accreting unnoticed, since two files sharing the index.* name would shadow one another under several bundlers and Node's own resolution and silently surface the wrong module under a name a consumer expects to mean something else. String operations only, no node:path import needed.
4+
//
5+
// `context.filename` is the ESLint 9+ API; the legacy `getFilename()` fallback covers an older parser this package no longer ships but keeps the rule portable across the family. Narrowed through a real type guard rather than a type assertion because this codebase bans assertions entirely (see `@typescript-eslint/consistent-type-assertions` in eslint.config.ts) -- the optional-chain call `context.getFilename?.()` itself trips `@typescript-eslint/no-unsafe-call` because ESLint 10's types don't resolve a callable signature for the deprecated method.
6+
interface ContextWithGetFilename { getFilename: () => string }
7+
8+
function hasGetFilename(context: unknown): context is ContextWithGetFilename {
9+
if (typeof context !== 'object' || context === null) return false;
10+
if (!('getFilename' in context)) return false;
11+
return typeof context.getFilename === 'function';
12+
}
13+
14+
const noNonBarrelIndex: Rule.RuleModule = {
15+
meta: {
16+
type: 'problem',
17+
schema: [],
18+
messages: {
19+
barrel:
20+
"Only src/index.ts may be named index.* (the public convenience barrel); give any other module a descriptive filename.",
21+
},
22+
},
23+
create(context) {
24+
let filename = context.filename;
25+
if (filename === undefined && hasGetFilename(context)) {
26+
filename = context.getFilename();
27+
}
28+
const path = filename ?? '';
29+
const slashIndex = path.lastIndexOf('/');
30+
const basename = slashIndex === -1 ? path : path.slice(slashIndex + 1);
31+
if (!/^index\.[cm]?[tj]s$/.test(basename)) return {};
32+
// The one allowed barrel. Match on the path suffix so the rule stays robust to the repo being checked out at any root.
33+
if (path.endsWith('/src/index.ts')) return {};
34+
return {
35+
Program(node) {
36+
context.report({ node, messageId: 'barrel' });
37+
},
38+
};
39+
},
40+
};
41+
42+
export default noNonBarrelIndex;

eslint.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import js from '@eslint/js';
22
import globals from 'globals';
33
import tseslint from 'typescript-eslint';
4+
import noNonBarrelIndex from './eslint-rules/no-non-barrel-index.js';
45
import noPointlessReassignment from './eslint-rules/no-pointless-reassignment.js';
56
import noSideEffectsInIndex from './eslint-rules/no-side-effects-in-index.js';
67

@@ -53,8 +54,8 @@ export default tseslint.config(
5354
},
5455
{
5556
// Local custom rule (eslint-rules/no-pointless-reassignment.ts) -- not published as a package, matching this family's own convention of keeping shared dev-tooling config as identical per-repo copies rather than a shared devDependency.
56-
plugins: { local: { rules: { 'no-pointless-reassignment': noPointlessReassignment, 'no-side-effects-in-index': noSideEffectsInIndex } } },
57-
rules: { 'local/no-pointless-reassignment': 'error' },
57+
plugins: { local: { rules: { 'no-non-barrel-index': noNonBarrelIndex, 'no-pointless-reassignment': noPointlessReassignment, 'no-side-effects-in-index': noSideEffectsInIndex } } },
58+
rules: { 'local/no-non-barrel-index': 'error', 'local/no-pointless-reassignment': 'error' },
5859
},
5960
{
6061
// Re-exports belong only in src/index.ts, the public barrel -- a re-export anywhere else risks silently surfacing the wrong thing under a name a consumer expects to mean something else.

0 commit comments

Comments
 (0)