eslint-plugin-code-policy 0.2.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/LICENSE +21 -0
- package/README.md +376 -0
- package/dist/chunk-URER6VHN.js +525 -0
- package/dist/chunk-URER6VHN.js.map +1 -0
- package/dist/chunk-YKNN7DF3.cjs +531 -0
- package/dist/chunk-YKNN7DF3.cjs.map +1 -0
- package/dist/configs/next.cjs +9 -0
- package/dist/configs/next.cjs.map +1 -0
- package/dist/configs/next.d.ts +14 -0
- package/dist/configs/next.d.ts.map +1 -0
- package/dist/configs/next.js +6 -0
- package/dist/configs/next.js.map +1 -0
- package/dist/configs/react.cjs +9 -0
- package/dist/configs/react.cjs.map +1 -0
- package/dist/configs/react.d.ts +14 -0
- package/dist/configs/react.d.ts.map +1 -0
- package/dist/configs/react.js +6 -0
- package/dist/configs/react.js.map +1 -0
- package/dist/configs/recommended.cjs +9 -0
- package/dist/configs/recommended.cjs.map +1 -0
- package/dist/configs/recommended.d.ts +14 -0
- package/dist/configs/recommended.d.ts.map +1 -0
- package/dist/configs/recommended.js +6 -0
- package/dist/configs/recommended.js.map +1 -0
- package/dist/configs/strict.cjs +9 -0
- package/dist/configs/strict.cjs.map +1 -0
- package/dist/configs/strict.d.ts +14 -0
- package/dist/configs/strict.d.ts.map +1 -0
- package/dist/configs/strict.js +6 -0
- package/dist/configs/strict.js.map +1 -0
- package/dist/index.cjs +18 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/atomic-file.d.ts +4 -0
- package/dist/rules/atomic-file.d.ts.map +1 -0
- package/dist/rules/no-cross-module-deep-imports.d.ts +31 -0
- package/dist/rules/no-cross-module-deep-imports.d.ts.map +1 -0
- package/dist/rules/no-inline-types.d.ts +4 -0
- package/dist/rules/no-inline-types.d.ts.map +1 -0
- package/dist/rules/public-api-imports.d.ts +4 -0
- package/dist/rules/public-api-imports.d.ts.map +1 -0
- package/dist/rules/view-logic-separation.d.ts +4 -0
- package/dist/rules/view-logic-separation.d.ts.map +1 -0
- package/package.json +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cristian Deluxe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<h1>eslint-plugin-code-policy</h1>
|
|
4
|
+
|
|
5
|
+
<p><strong>Architectural linting for TypeScript · React · Next.js</strong></p>
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/eslint-plugin-code-policy)
|
|
8
|
+
[](../../LICENSE)
|
|
9
|
+
[](https://eslint.org/docs/latest/use/configure/configuration-files-new)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
**Requirements**
|
|
19
|
+
|
|
20
|
+
- ESLint `^9.0.0` (flat config)
|
|
21
|
+
- TypeScript `^5.4.0`
|
|
22
|
+
- Node.js `>=20`
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# npm
|
|
26
|
+
npm install --save-dev eslint-plugin-code-policy
|
|
27
|
+
|
|
28
|
+
# pnpm
|
|
29
|
+
pnpm add --save-dev eslint-plugin-code-policy
|
|
30
|
+
|
|
31
|
+
# yarn
|
|
32
|
+
yarn add --dev eslint-plugin-code-policy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Flat config (`eslint.config.mjs`)
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import codePolicy from 'eslint-plugin-code-policy'
|
|
43
|
+
|
|
44
|
+
export default [codePolicy.configs.recommended]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Choosing a preset
|
|
48
|
+
|
|
49
|
+
| Preset | Import path | Best for |
|
|
50
|
+
| ------------- | -------------------------------- | ---------------------- |
|
|
51
|
+
| `recommended` | `codePolicy.configs.recommended` | Any TypeScript project |
|
|
52
|
+
| `strict` | `codePolicy.configs.strict` | Maximum enforcement |
|
|
53
|
+
| `react` | `codePolicy.configs.react` | React (Vite, CRA, …) |
|
|
54
|
+
| `next` | `codePolicy.configs.next` | Next.js App Router |
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
// Next.js example
|
|
58
|
+
import codePolicy from 'eslint-plugin-code-policy'
|
|
59
|
+
|
|
60
|
+
export default [codePolicy.configs.next]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Manual rule configuration
|
|
64
|
+
|
|
65
|
+
If you prefer to cherry-pick rules:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import codePolicy from 'eslint-plugin-code-policy'
|
|
69
|
+
|
|
70
|
+
export default [
|
|
71
|
+
{
|
|
72
|
+
plugins: { 'code-policy': codePolicy },
|
|
73
|
+
rules: {
|
|
74
|
+
'code-policy/atomic-file': 'error',
|
|
75
|
+
'code-policy/no-inline-types': 'error',
|
|
76
|
+
'code-policy/public-api-imports': 'error',
|
|
77
|
+
'code-policy/no-cross-module-deep-imports': 'error',
|
|
78
|
+
'code-policy/view-logic-separation': 'error',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Rules
|
|
87
|
+
|
|
88
|
+
### `code-policy/atomic-file`
|
|
89
|
+
|
|
90
|
+
> **Enforce exactly one top-level declaration per file.**
|
|
91
|
+
|
|
92
|
+
Each file must export exactly one top-level unit — a function, class, constant, or type. This is the foundation of hyper-modular architecture: if a file has two things, one of them belongs in a new file.
|
|
93
|
+
|
|
94
|
+
**❌ Incorrect**
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// ❌ src/UserUtils.ts — two exports in one file
|
|
98
|
+
export function formatUserName(user: User) {
|
|
99
|
+
return `${user.firstName} ${user.lastName}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getUserAge(user: User) {
|
|
103
|
+
return new Date().getFullYear() - user.birthYear
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**✅ Correct**
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// ✅ src/formatUserName.ts
|
|
111
|
+
export function formatUserName(user: User) {
|
|
112
|
+
return `${user.firstName} ${user.lastName}`
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// ✅ src/getUserAge.ts
|
|
118
|
+
export function getUserAge(user: User) {
|
|
119
|
+
return new Date().getFullYear() - user.birthYear
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Exemptions (automatically skipped)**
|
|
124
|
+
|
|
125
|
+
- `*.config.ts` / `*.config.js` / `*.config.mjs`
|
|
126
|
+
- `index.ts` / `index.tsx` / `index.js` (barrel files)
|
|
127
|
+
- `*.d.ts` (ambient declaration files)
|
|
128
|
+
- Next.js special files: `page.tsx`, `layout.tsx`, `route.ts`, etc. — reserved exports like `GET`, `POST`, `metadata` are not counted.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### `code-policy/no-inline-types`
|
|
133
|
+
|
|
134
|
+
> **Enforce that type aliases and interfaces live in their own files.**
|
|
135
|
+
|
|
136
|
+
Type declarations that appear alongside implementation code create hidden coupling and violate the single responsibility principle. Every `type` or `interface` must be in a dedicated file, ideally inside a `types/` directory.
|
|
137
|
+
|
|
138
|
+
**❌ Incorrect**
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// ❌ Mixed type + implementation in one file
|
|
142
|
+
type UserRole = 'admin' | 'member' | 'guest'
|
|
143
|
+
|
|
144
|
+
export function canEdit(role: UserRole) {
|
|
145
|
+
return role === 'admin'
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**✅ Correct**
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// ✅ src/types/UserRole.ts
|
|
153
|
+
export type UserRole = 'admin' | 'member' | 'guest'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// ✅ src/canEdit.ts
|
|
158
|
+
import type { UserRole } from '@/types/UserRole'
|
|
159
|
+
|
|
160
|
+
export function canEdit(role: UserRole) {
|
|
161
|
+
return role === 'admin'
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Exemptions**
|
|
166
|
+
|
|
167
|
+
- Files inside `types/` or `types/**` directories
|
|
168
|
+
- `*.d.ts` files
|
|
169
|
+
- "Pure type files" — files whose entire body consists only of `import` + `type`/`interface` declarations
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### `code-policy/public-api-imports`
|
|
174
|
+
|
|
175
|
+
> **Prevent importing directly from internal module subpaths.**
|
|
176
|
+
|
|
177
|
+
When consuming a package or module, you must import from its public API (the root / index), not from a deep internal path. Deep imports couple you to internal implementation details.
|
|
178
|
+
|
|
179
|
+
**❌ Incorrect**
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
// ❌ Bypassing the public API
|
|
183
|
+
import { Button } from '@myorg/ui/src/components/Button'
|
|
184
|
+
import { formatDate } from '@myorg/utils/src/date/formatDate'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**✅ Correct**
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
// ✅ Always import through the public surface
|
|
191
|
+
import { Button } from '@myorg/ui'
|
|
192
|
+
import { formatDate } from '@myorg/utils'
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Options**
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
{
|
|
199
|
+
'code-policy/public-api-imports': ['error', {
|
|
200
|
+
bannedSubpaths: ['/src/'] // default
|
|
201
|
+
}]
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
| Option | Type | Default | Description |
|
|
206
|
+
| ---------------- | ---------- | ----------- | ------------------------------------------- |
|
|
207
|
+
| `bannedSubpaths` | `string[]` | `['/src/']` | Segments that signal a deep internal import |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### `code-policy/no-cross-module-deep-imports`
|
|
212
|
+
|
|
213
|
+
> **Prevent relative imports that bypass another module's public API within a monorepo.**
|
|
214
|
+
|
|
215
|
+
In a monorepo, relative paths like `../../core/src/utils/helper` skip the `core` module's public API entirely. This rule detects that pattern by counting `../` traversal depth and checking for internal directory names in the descent.
|
|
216
|
+
|
|
217
|
+
**❌ Incorrect**
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
// ❌ packages/ui/src/Button.tsx
|
|
221
|
+
import { helper } from '../../core/src/utils/helper'
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**✅ Correct**
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
// ✅ Import through the published public API
|
|
228
|
+
import { helper } from '@myorg/core'
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Options**
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
{
|
|
235
|
+
'code-policy/no-cross-module-deep-imports': ['error', {
|
|
236
|
+
minParentTraversals: 2, // how many `../` levels before checking
|
|
237
|
+
internalDirs: ['src'] // dirs that signal internal code
|
|
238
|
+
}]
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| Option | Type | Default | Description |
|
|
243
|
+
| --------------------- | ---------- | --------- | ------------------------------------------------ |
|
|
244
|
+
| `minParentTraversals` | `number` | `2` | Minimum `../` segments before the rule activates |
|
|
245
|
+
| `internalDirs` | `string[]` | `['src']` | Directory names that indicate internal code |
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### `code-policy/view-logic-separation`
|
|
250
|
+
|
|
251
|
+
> **Prevent state, effects, and inline handlers inside React view components.**
|
|
252
|
+
|
|
253
|
+
React view components (`.tsx` files) are responsible for rendering only. State management, side effects, and event handler logic must live in a dedicated custom hook. This enforces a clean view/controller split.
|
|
254
|
+
|
|
255
|
+
**❌ Incorrect**
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
// ❌ src/UserCard.tsx — logic inside a view
|
|
259
|
+
export function UserCard({ userId }: UserCardProps) {
|
|
260
|
+
const [user, setUser] = useState<User | null>(null)
|
|
261
|
+
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
fetchUser(userId).then(setUser)
|
|
264
|
+
}, [userId])
|
|
265
|
+
|
|
266
|
+
const handleDelete = () => {
|
|
267
|
+
deleteUser(userId)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return <div onClick={handleDelete}>{user?.name}</div>
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**✅ Correct**
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
// ✅ src/useUserCard.ts
|
|
278
|
+
export function useUserCard(userId: string) {
|
|
279
|
+
const [user, setUser] = useState<User | null>(null)
|
|
280
|
+
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
fetchUser(userId).then(setUser)
|
|
283
|
+
}, [userId])
|
|
284
|
+
|
|
285
|
+
const handleDelete = () => deleteUser(userId)
|
|
286
|
+
|
|
287
|
+
return { user, handleDelete }
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
// ✅ src/UserCard.tsx — pure view
|
|
293
|
+
import { useUserCard } from './useUserCard'
|
|
294
|
+
|
|
295
|
+
export function UserCard({ userId }: UserCardProps) {
|
|
296
|
+
const { user, handleDelete } = useUserCard(userId)
|
|
297
|
+
return <div onClick={handleDelete}>{user?.name}</div>
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**What triggers this rule (inside `.tsx` files)**
|
|
302
|
+
|
|
303
|
+
- Calling React hooks: `useState`, `useEffect`, `useReducer`, `useCallback`, `useMemo`, `useRef`, and more
|
|
304
|
+
- Declaring inline functions/handlers directly inside a view component body
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Shareable Configs Reference
|
|
309
|
+
|
|
310
|
+
### `recommended`
|
|
311
|
+
|
|
312
|
+
Enables all five rules as errors. Best starting point for any TypeScript project.
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
// Rules enabled:
|
|
316
|
+
'code-policy/atomic-file': 'error'
|
|
317
|
+
'code-policy/no-inline-types': 'error'
|
|
318
|
+
'code-policy/view-logic-separation': 'error'
|
|
319
|
+
'code-policy/public-api-imports': 'error'
|
|
320
|
+
'code-policy/no-cross-module-deep-imports': 'error'
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### `strict`
|
|
324
|
+
|
|
325
|
+
Extends `recommended`. Intended for projects that want zero tolerance for architectural deviation. Reserved for additional strictness overrides in future versions.
|
|
326
|
+
|
|
327
|
+
### `react`
|
|
328
|
+
|
|
329
|
+
Extends `recommended` with React-specific adjustments.
|
|
330
|
+
|
|
331
|
+
### `next`
|
|
332
|
+
|
|
333
|
+
Extends `recommended`. Correctly handles Next.js App Router special files (`page.tsx`, `layout.tsx`, `route.ts`, etc.) and reserved exports (`metadata`, `GET`, `POST`, …), preventing false positives.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Migrating from Legacy Config
|
|
338
|
+
|
|
339
|
+
This plugin only supports the **ESLint flat config** format (ESLint v9+). If you're still on the legacy `.eslintrc` format, migrate using the [official ESLint migration guide](https://eslint.org/docs/latest/use/configure/migration-guide) before installing this plugin.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## FAQ
|
|
344
|
+
|
|
345
|
+
**Q: Why do I get errors on my `index.ts` barrel files?**
|
|
346
|
+
|
|
347
|
+
`index.ts` files are automatically exempted from the `atomic-file` rule because barrel files by design re-export multiple things.
|
|
348
|
+
|
|
349
|
+
**Q: How do I exempt a specific file from a rule?**
|
|
350
|
+
|
|
351
|
+
Use ESLint's standard inline disable comment:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
// eslint-disable-next-line code-policy/atomic-file
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Or add file overrides in your `eslint.config.mjs`:
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
{
|
|
361
|
+
files: ['src/legacy/**'],
|
|
362
|
+
rules: {
|
|
363
|
+
'code-policy/atomic-file': 'off',
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Q: Does this work with JavaScript (non-TypeScript) projects?**
|
|
369
|
+
|
|
370
|
+
The rules are language-agnostic at the ESLint AST level. TypeScript-specific nodes are handled gracefully. You can use the plugin on `.js` files, though some rules (like `no-inline-types`) are most meaningful in TypeScript codebases.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## License
|
|
375
|
+
|
|
376
|
+
[MIT](../../LICENSE)
|