eslint-plugin-ai-rules 0.2.1 → 0.3.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/README.md +1144 -174
- package/dist/index.cjs +1640 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -2
- package/dist/index.d.ts +74 -2
- package/dist/index.js +1636 -2
- package/dist/index.js.map +1 -1
- package/package.json +25 -11
package/README.md
CHANGED
|
@@ -2,61 +2,212 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/eslint-plugin-ai-rules)
|
|
4
4
|
[](LICENSE)
|
|
5
|
-
[](package.json)
|
|
6
|
+
[](https://eslint.org)
|
|
7
7
|
|
|
8
|
-
Flat-config
|
|
8
|
+
Flat-config-only ESLint plugin for AI-generated code. It catches filename/import drift and enforces a project structure you declare, with ready-to-use `frontend` and `backend` presets.
|
|
9
9
|
|
|
10
|
-
> **ESLint 9+ only.** Uses flat config (`eslint.config.{ts
|
|
10
|
+
> **ESLint 9.22+ or 10 only.** Uses flat config (`eslint.config.{js,mjs,ts}`). No legacy `.eslintrc` support.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Start here
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
| If your project is... | Use |
|
|
15
|
+
|-----------------------|-----|
|
|
16
|
+
| Frontend only | `preset: 'frontend'` |
|
|
17
|
+
| Backend only | `preset: 'backend'` |
|
|
18
|
+
| Frontend + backend in one repo | two projects with different `rootFolder` |
|
|
19
|
+
| Neither frontend nor backend | `preset: 'other'` |
|
|
20
|
+
| Layered architecture | `relations` (controller → useCase → repository) |
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
Before installing, check:
|
|
20
23
|
|
|
21
|
-
- Node `>=
|
|
22
|
-
- ESLint `^9.
|
|
23
|
-
- TypeScript `>=5.0.0`
|
|
24
|
+
- Node.js `>= 20`
|
|
25
|
+
- ESLint `^9.22.0` or `^10.0.0`
|
|
26
|
+
- TypeScript `>= 5.0.0 < 6.1.0`
|
|
27
|
+
- `typescript-eslint` configured as parser
|
|
28
|
+
- `tsconfig.json` if you use aliases like `@/*`
|
|
29
|
+
- `eslint-import-resolver-typescript` installed for TS alias resolution
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
## Quick start
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
Install:
|
|
28
34
|
|
|
29
35
|
```bash
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
npm i -D eslint-plugin-ai-rules eslint typescript typescript-eslint eslint-import-resolver-typescript
|
|
37
|
+
```
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
yarn add -D eslint-plugin-ai-rules eslint typescript
|
|
39
|
+
Configure the project structure and let the plugin apply its recommended rules:
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
```ts
|
|
42
|
+
import { defineConfig } from 'eslint/config'
|
|
43
|
+
import aiRules from 'eslint-plugin-ai-rules'
|
|
44
|
+
import tseslint from 'typescript-eslint'
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
export default defineConfig([
|
|
47
|
+
...tseslint.configs.recommended,
|
|
48
|
+
...aiRules.configs.projects([
|
|
49
|
+
{ rootFolder: '.', preset: 'frontend' },
|
|
50
|
+
]),
|
|
51
|
+
])
|
|
41
52
|
```
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
Monorepo with frontend and backend:
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
```ts
|
|
57
|
+
export default defineConfig([
|
|
58
|
+
...tseslint.configs.recommended,
|
|
59
|
+
...aiRules.configs.projects([
|
|
60
|
+
{ rootFolder: 'apps/web', preset: 'frontend' },
|
|
61
|
+
{ rootFolder: 'apps/api', preset: 'backend' },
|
|
62
|
+
]),
|
|
63
|
+
])
|
|
64
|
+
```
|
|
46
65
|
|
|
47
|
-
|
|
66
|
+
Project that is neither frontend nor backend:
|
|
48
67
|
|
|
49
68
|
```ts
|
|
69
|
+
export default defineConfig([
|
|
70
|
+
...tseslint.configs.recommended,
|
|
71
|
+
...aiRules.configs.projects([
|
|
72
|
+
{ rootFolder: '.', preset: 'other' },
|
|
73
|
+
]),
|
|
74
|
+
])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Run:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx eslint .
|
|
81
|
+
npx eslint --fix .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Contents
|
|
85
|
+
|
|
86
|
+
- [Configuration warning](#configuration-warning)
|
|
87
|
+
- [Features](#features)
|
|
88
|
+
- [Requirements](#requirements)
|
|
89
|
+
- [Project structure](#project-structure)
|
|
90
|
+
- [Examples](#examples)
|
|
91
|
+
- [Architecture relations](#architecture-relations)
|
|
92
|
+
- [Expected diagnostics](#expected-diagnostics)
|
|
93
|
+
- [Preset reference](#preset-reference)
|
|
94
|
+
- [Structure DSL](#structure-dsl)
|
|
95
|
+
- [Limitations](#limitations)
|
|
96
|
+
- [Rules](#rules)
|
|
97
|
+
- [What runs automatically](#what-runs-automatically)
|
|
98
|
+
- [Recommended preset](#recommended-preset)
|
|
99
|
+
- [Troubleshooting](#troubleshooting)
|
|
100
|
+
- [Common mistakes](#common-mistakes)
|
|
101
|
+
- [Migrating from 0.2.x](#migrating-from-02x)
|
|
102
|
+
- [Development](#development)
|
|
103
|
+
- [Publishing](#publishing)
|
|
104
|
+
- [Legacy](#legacy)
|
|
105
|
+
|
|
106
|
+
## Configuration warning
|
|
107
|
+
|
|
108
|
+
If you install the plugin and use `aiRules.configs.recommended` without configuring the project structure, the plugin prints a warning the first time it lints a file:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
[eslint-plugin-ai-rules] Project structure validation is not configured.
|
|
112
|
+
Configure it with aiRules.configs.projects([{ rootFolder: '.', preset: 'frontend' | 'backend' }]).
|
|
113
|
+
If this project is not a frontend/backend, use preset: 'other' to disable this warning.
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The warning exists only to make sure the new structure rules are actually configured. It is printed once per process.
|
|
117
|
+
|
|
118
|
+
It disappears when you provide a valid configuration:
|
|
119
|
+
|
|
120
|
+
- `preset: 'frontend'`
|
|
121
|
+
- `preset: 'backend'`
|
|
122
|
+
- `preset: 'other'`
|
|
123
|
+
- a custom non-empty `structure`
|
|
124
|
+
- a `relations` configuration
|
|
125
|
+
|
|
126
|
+
A project containing only `rootFolder` and an empty `structure` is still considered unconfigured and will warn.
|
|
127
|
+
|
|
128
|
+
For CI or temporary runs, silence the warning without changing the config:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
AI_RULES_SILENT=1 npx eslint .
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The warning is still recommended as a setup check, so prefer `preset: 'other'` for projects that intentionally do not use structure validation.
|
|
135
|
+
|
|
136
|
+
## Features
|
|
137
|
+
|
|
138
|
+
- **Project structure validation** — folder hierarchy, dynamic folders, folder/file casing, prefixes, suffixes and regex patterns.
|
|
139
|
+
- **Frontend/backend presets** — ready-to-use structures and architecture relations based on real projects.
|
|
140
|
+
- **Custom structure** — declare exactly how your project must be organized.
|
|
141
|
+
- **Architecture relations** — enforce which layers can import which other layers, including global vs page-local scope.
|
|
142
|
+
- **Monorepo friendly** — one `rootFolder` per project, including nested roots.
|
|
143
|
+
- **Autofix** — `sort-imports` and `no-multiline-imports` are fixable.
|
|
144
|
+
- **Bundled rules** — ships with useful `import-x`, `@stylistic`, `boundaries` and `@typescript-eslint` rules.
|
|
145
|
+
- **ESM + CJS** — `dist/index.js` and `dist/index.cjs`, with correct type declarations for both.
|
|
146
|
+
|
|
147
|
+
## Requirements
|
|
148
|
+
|
|
149
|
+
| Tool | Version |
|
|
150
|
+
|------|---------|
|
|
151
|
+
| Node.js | `>= 20` |
|
|
152
|
+
| ESLint | `^9.22.0 || ^10.0.0` |
|
|
153
|
+
| TypeScript | `>= 5.0.0 < 6.1.0` |
|
|
154
|
+
| ESLint resolver | `eslint-import-resolver-typescript` `^4.4.5` |
|
|
155
|
+
|
|
156
|
+
Runtime dependencies are `@stylistic/eslint-plugin`, `eslint-plugin-import-x`, `eslint-plugin-boundaries` and `typescript-eslint`.
|
|
157
|
+
|
|
158
|
+
`eslint-import-resolver-typescript` is a peer dependency used by `relations` to resolve TS aliases. npm installs peers automatically; with Yarn 1 or pnpm, install it explicitly.
|
|
159
|
+
|
|
160
|
+
## Project structure
|
|
161
|
+
|
|
162
|
+
Each project definition has this shape:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
interface IProjectDefinition {
|
|
166
|
+
rootFolder: string
|
|
167
|
+
preset?: 'frontend' | 'backend' | 'other'
|
|
168
|
+
structure?: IProjectStructure
|
|
169
|
+
relations?: IRelationsConfig
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- `rootFolder` is the project root, usually the folder containing `package.json`.
|
|
174
|
+
- `rootFolder` is relative to the current working directory.
|
|
175
|
+
- Absolute paths inside the current working directory are also accepted.
|
|
176
|
+
- Nested roots are supported; the most specific root wins.
|
|
177
|
+
- Duplicate roots are rejected.
|
|
178
|
+
- `preset` provides a base structure.
|
|
179
|
+
- `structure` is merged on top of the preset.
|
|
180
|
+
- `relations` enables architecture dependency policies with `eslint-plugin-boundaries`.
|
|
181
|
+
- `preset: 'other'` disables structure validation unless a custom `structure` is provided.
|
|
182
|
+
|
|
183
|
+
`aiRules.configs.projects([...])` generates one flat config per project. Each config:
|
|
184
|
+
|
|
185
|
+
- scopes `files` to the project root;
|
|
186
|
+
- sets `settings['ai-rules'].project`;
|
|
187
|
+
- enables `ai-rules/project-structure`;
|
|
188
|
+
- adds `ignores` from `ignoredPaths`;
|
|
189
|
+
- includes the recommended rules;
|
|
190
|
+
- disables `ai-rules/filename-pascal-case`, because file naming is handled by `project-structure`;
|
|
191
|
+
- when `relations` is present, registers `boundaries` and enables `boundaries/dependencies`.
|
|
192
|
+
|
|
193
|
+
## Examples
|
|
194
|
+
|
|
195
|
+
### Frontend only
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { defineConfig } from 'eslint/config'
|
|
50
199
|
import aiRules from 'eslint-plugin-ai-rules'
|
|
51
200
|
import tseslint from 'typescript-eslint'
|
|
52
201
|
|
|
53
|
-
export default [
|
|
202
|
+
export default defineConfig([
|
|
54
203
|
...tseslint.configs.recommended,
|
|
55
|
-
|
|
56
|
-
|
|
204
|
+
...aiRules.configs.projects([
|
|
205
|
+
{ rootFolder: '.', preset: 'frontend' },
|
|
206
|
+
]),
|
|
207
|
+
])
|
|
57
208
|
```
|
|
58
209
|
|
|
59
|
-
|
|
210
|
+
### Frontend and backend in the same repository
|
|
60
211
|
|
|
61
212
|
```ts
|
|
62
213
|
import { defineConfig } from 'eslint/config'
|
|
@@ -65,125 +216,762 @@ import tseslint from 'typescript-eslint'
|
|
|
65
216
|
|
|
66
217
|
export default defineConfig([
|
|
67
218
|
...tseslint.configs.recommended,
|
|
68
|
-
|
|
219
|
+
...aiRules.configs.projects([
|
|
220
|
+
{ rootFolder: 'packages/web', preset: 'frontend' },
|
|
221
|
+
{ rootFolder: 'packages/api', preset: 'backend' },
|
|
222
|
+
]),
|
|
69
223
|
])
|
|
70
224
|
```
|
|
71
225
|
|
|
72
|
-
###
|
|
226
|
+
### Backend with preset (copy-paste)
|
|
73
227
|
|
|
74
|
-
```
|
|
228
|
+
```ts
|
|
229
|
+
import { defineConfig } from 'eslint/config'
|
|
75
230
|
import aiRules from 'eslint-plugin-ai-rules'
|
|
76
231
|
import tseslint from 'typescript-eslint'
|
|
77
232
|
|
|
78
|
-
export default [
|
|
233
|
+
export default defineConfig([
|
|
79
234
|
...tseslint.configs.recommended,
|
|
80
|
-
|
|
81
|
-
|
|
235
|
+
...aiRules.configs.projects([
|
|
236
|
+
{
|
|
237
|
+
rootFolder: '.',
|
|
238
|
+
preset: 'backend',
|
|
239
|
+
},
|
|
240
|
+
]),
|
|
241
|
+
])
|
|
82
242
|
```
|
|
83
243
|
|
|
84
|
-
|
|
244
|
+
The backend preset includes strict architecture relations. With this config:
|
|
85
245
|
|
|
86
246
|
```ts
|
|
87
|
-
|
|
88
|
-
import {
|
|
247
|
+
// src/http/controllers/ListProducts.ts
|
|
248
|
+
import { listProducts } from '@/use-case/product/List' // allowed
|
|
249
|
+
import { productRepository } from '@/repositories/ProductRepository' // blocked
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
To override a preset relation, declare only the layer/rule you want to change:
|
|
89
253
|
|
|
254
|
+
```ts
|
|
255
|
+
relations: {
|
|
256
|
+
rules: [
|
|
257
|
+
{ from: 'service', allow: ['generated', 'env'], allowSameLayer: false },
|
|
258
|
+
],
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Opting out with `other`
|
|
263
|
+
|
|
264
|
+
```ts
|
|
90
265
|
export default defineConfig([
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
266
|
+
...tseslint.configs.recommended,
|
|
267
|
+
...aiRules.configs.projects([
|
|
268
|
+
{ rootFolder: '.', preset: 'other' },
|
|
269
|
+
]),
|
|
270
|
+
])
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Custom structure
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
export default defineConfig([
|
|
277
|
+
...tseslint.configs.recommended,
|
|
278
|
+
...aiRules.configs.projects([
|
|
279
|
+
{
|
|
280
|
+
rootFolder: 'apps/api',
|
|
281
|
+
preset: 'backend',
|
|
282
|
+
structure: {
|
|
283
|
+
folders: {
|
|
284
|
+
src: {
|
|
285
|
+
folders: {
|
|
286
|
+
'use-case': {
|
|
287
|
+
folders: {
|
|
288
|
+
'[feature]': {
|
|
289
|
+
files: [
|
|
290
|
+
{
|
|
291
|
+
case: 'PascalCase',
|
|
292
|
+
suffix: 'UseCase',
|
|
293
|
+
extensions: ['.ts'],
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
]),
|
|
305
|
+
])
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Use case with a suffix
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
structure: {
|
|
312
|
+
folders: {
|
|
313
|
+
src: {
|
|
314
|
+
folders: {
|
|
315
|
+
'use-case': {
|
|
316
|
+
folders: {
|
|
317
|
+
'[feature]': {
|
|
318
|
+
files: [
|
|
319
|
+
{
|
|
320
|
+
case: 'PascalCase',
|
|
321
|
+
suffix: 'UseCase',
|
|
322
|
+
extensions: ['.ts'],
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
97
329
|
},
|
|
98
330
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Controller with a suffix
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
structure: {
|
|
338
|
+
folders: {
|
|
339
|
+
src: {
|
|
340
|
+
folders: {
|
|
341
|
+
http: {
|
|
342
|
+
folders: {
|
|
343
|
+
controllers: {
|
|
344
|
+
folders: {
|
|
345
|
+
'[feature]': {
|
|
346
|
+
files: [
|
|
347
|
+
{
|
|
348
|
+
case: 'PascalCase',
|
|
349
|
+
suffix: 'Controller',
|
|
350
|
+
extensions: ['.ts'],
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
103
359
|
},
|
|
104
360
|
},
|
|
105
|
-
|
|
361
|
+
}
|
|
106
362
|
```
|
|
107
363
|
|
|
108
|
-
|
|
364
|
+
### Dynamic folder with prefix, suffix and regex
|
|
109
365
|
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
|
|
366
|
+
```ts
|
|
367
|
+
structure: {
|
|
368
|
+
folders: {
|
|
369
|
+
'[module]': {
|
|
370
|
+
name: {
|
|
371
|
+
prefix: 'feature-',
|
|
372
|
+
suffix: '-module',
|
|
373
|
+
pattern: '^feature-[a-z-]+-module$',
|
|
374
|
+
},
|
|
375
|
+
files: ['index.ts'],
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
}
|
|
113
379
|
```
|
|
114
380
|
|
|
115
|
-
|
|
381
|
+
Valid: `feature-users-module/index.ts`.
|
|
116
382
|
|
|
117
|
-
|
|
383
|
+
Invalid: `users-module/index.ts`, `feature-users/index.ts`, `feature-Users-module/index.ts`.
|
|
118
384
|
|
|
119
|
-
|
|
385
|
+
### Adding files to a preset
|
|
120
386
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
387
|
+
```ts
|
|
388
|
+
structure: {
|
|
389
|
+
folders: {
|
|
390
|
+
src: {
|
|
391
|
+
additionalFiles: [
|
|
392
|
+
{ case: 'PascalCase', suffix: 'Service', extensions: ['.ts'] },
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
```
|
|
124
398
|
|
|
125
|
-
|
|
126
|
-
|------|---------|-------|
|
|
127
|
-
| `ai-rules/filename-pascal-case` | `error` | File-agnostic: checks every file ESLint lints. See below |
|
|
128
|
-
| `ai-rules/sort-imports` | `error` | Groups + length desc, collapses multiline |
|
|
129
|
-
| `ai-rules/no-multiline-imports` | `error` | Single-line imports |
|
|
130
|
-
| `import-x/no-duplicates` | `error` | |
|
|
131
|
-
| `import-x/no-default-export` | `off` | |
|
|
132
|
-
| `max-len` | `off` | |
|
|
133
|
-
| `@stylistic/semi` | `['error','never']` | No semicolons |
|
|
134
|
-
| `@typescript-eslint/consistent-type-imports` | `['error',{prefer:'type-imports'}]` | |
|
|
135
|
-
| `@typescript-eslint/no-unused-vars` | `['error',{argsIgnorePattern:'^_',varsIgnorePattern:'^_'}]` | |
|
|
136
|
-
| `@typescript-eslint/naming-convention` | `['error', {interface:'PascalCase' prefix I}, {typeAlias:'PascalCase' prefix T}]` | Disable per-project for `**/*.d.ts` via your own override if needed |
|
|
399
|
+
### Replacing preset files
|
|
137
400
|
|
|
138
|
-
|
|
401
|
+
```ts
|
|
402
|
+
structure: {
|
|
403
|
+
folders: {
|
|
404
|
+
src: {
|
|
405
|
+
files: ['index.ts'],
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Ignoring a preset subtree
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
structure: {
|
|
415
|
+
folders: {
|
|
416
|
+
e2e: { ignore: true },
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Ignoring paths globally
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
structure: {
|
|
425
|
+
ignoredPaths: ['dist/**', 'coverage/**', 'src/services/generated/**'],
|
|
426
|
+
}
|
|
427
|
+
```
|
|
139
428
|
|
|
140
|
-
###
|
|
429
|
+
### Manual settings without the factory
|
|
430
|
+
|
|
431
|
+
Advanced users can configure the rule directly:
|
|
141
432
|
|
|
142
433
|
```ts
|
|
143
434
|
import { plugin } from 'eslint-plugin-ai-rules'
|
|
144
|
-
import { defineConfig } from 'eslint/config'
|
|
145
435
|
|
|
146
|
-
export default
|
|
436
|
+
export default [
|
|
147
437
|
{
|
|
148
|
-
files: ['
|
|
438
|
+
files: ['src/**/*.ts'],
|
|
149
439
|
plugins: { 'ai-rules': plugin },
|
|
440
|
+
settings: {
|
|
441
|
+
'ai-rules': {
|
|
442
|
+
project: {
|
|
443
|
+
rootFolder: '.',
|
|
444
|
+
preset: 'other',
|
|
445
|
+
structure: {
|
|
446
|
+
folders: {
|
|
447
|
+
src: {
|
|
448
|
+
files: [{ case: 'PascalCase', extensions: ['.ts'] }],
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
},
|
|
150
455
|
rules: {
|
|
151
|
-
'ai-rules/
|
|
152
|
-
'ai-rules/no-multiline-imports': 'warn',
|
|
153
|
-
'ai-rules/filename-pascal-case': 'off',
|
|
456
|
+
'ai-rules/project-structure': 'error',
|
|
154
457
|
},
|
|
155
458
|
},
|
|
459
|
+
]
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Multiple projects can be passed directly through `settings['ai-rules'].projects`. The rule chooses the most specific root for each file.
|
|
463
|
+
|
|
464
|
+
## Architecture relations
|
|
465
|
+
|
|
466
|
+
`relations` enforces which layers can import which other layers. It complements `structure`: `structure` validates paths and names, `relations` validates import direction.
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
interface IRelationsRule {
|
|
470
|
+
from: string
|
|
471
|
+
allow?: string[]
|
|
472
|
+
deny?: string[]
|
|
473
|
+
allowSameLayer?: boolean
|
|
474
|
+
allowTypeImports?: boolean
|
|
475
|
+
message?: string
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
interface IRelationsScope {
|
|
479
|
+
capture: string
|
|
480
|
+
layers: string[]
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
interface IRelationsConfig {
|
|
484
|
+
layers: Record<string, string | string[]>
|
|
485
|
+
rules: IRelationsRule[]
|
|
486
|
+
allowTypeImports?: boolean
|
|
487
|
+
scope?: IRelationsScope
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Example for a backend:
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
export default defineConfig([
|
|
495
|
+
...tseslint.configs.recommended,
|
|
496
|
+
...aiRules.configs.projects([
|
|
497
|
+
{
|
|
498
|
+
rootFolder: 'apps/backend',
|
|
499
|
+
preset: 'other',
|
|
500
|
+
relations: {
|
|
501
|
+
layers: {
|
|
502
|
+
controller: 'src/http/controllers',
|
|
503
|
+
useCase: 'src/use-case',
|
|
504
|
+
repository: 'src/repositories',
|
|
505
|
+
service: 'src/services',
|
|
506
|
+
helper: 'src/helpers',
|
|
507
|
+
},
|
|
508
|
+
rules: [
|
|
509
|
+
{ from: 'controller', allow: ['useCase', 'helper'] },
|
|
510
|
+
{ from: 'useCase', allow: ['repository', 'service', 'helper'] },
|
|
511
|
+
{ from: 'repository', allow: ['helper'] },
|
|
512
|
+
{ from: 'service', allow: ['repository', 'helper'] },
|
|
513
|
+
{ from: 'helper', allow: [] },
|
|
514
|
+
],
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
]),
|
|
156
518
|
])
|
|
157
519
|
```
|
|
158
520
|
|
|
159
|
-
|
|
521
|
+
Semantics:
|
|
522
|
+
|
|
523
|
+
- Layers are folder paths relative to `rootFolder`.
|
|
524
|
+
- `allow` means the layer can import only the listed layers.
|
|
525
|
+
- `deny` means the layer cannot import the listed layers.
|
|
526
|
+
- `allow` and `deny` are mutually exclusive per rule.
|
|
527
|
+
- `allowSameLayer` defaults to `true`. Set it to `false` to block imports within the same layer.
|
|
528
|
+
- External packages and Node core modules are allowed automatically.
|
|
529
|
+
- Local files that do not match any layer are ignored by default.
|
|
530
|
+
- Only one rule per `from` layer is allowed.
|
|
531
|
+
- `scope` allows comparing a captured value, such as the current page, to restrict local imports to the same page.
|
|
532
|
+
|
|
533
|
+
Presets already include relation rules. You only need to declare `relations` when you want to add or override layers and rules:
|
|
160
534
|
|
|
161
535
|
```ts
|
|
162
|
-
|
|
536
|
+
aiRules.configs.projects([
|
|
537
|
+
{
|
|
538
|
+
rootFolder: 'apps/backend',
|
|
539
|
+
preset: 'backend',
|
|
540
|
+
relations: {
|
|
541
|
+
rules: [
|
|
542
|
+
{ from: 'service', allow: ['generated', 'env'], allowSameLayer: false },
|
|
543
|
+
],
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
])
|
|
163
547
|
```
|
|
164
548
|
|
|
165
|
-
|
|
549
|
+
The preset backend is strict: controllers only use use-cases plus infrastructure, use-cases cannot access database/helpers directly, repositories cannot import repositories, and services cannot import services. Structural exceptions exist for `generated` and `database/schema`. Service subfolders are generic modules with their own scope.
|
|
166
550
|
|
|
167
|
-
|
|
551
|
+
The preset frontend uses global layers plus page-local scope. Global layers cannot import page-local files. Page-local files can import global layers and files from the same page only.
|
|
168
552
|
|
|
169
|
-
###
|
|
553
|
+
### Backend preset relations
|
|
554
|
+
|
|
555
|
+
| Layer | Can import | Same layer |
|
|
556
|
+
|-------|------------|------------|
|
|
557
|
+
| controller | useCase, http, generated, helper, env | no |
|
|
558
|
+
| http | useCase, helper, generated | no |
|
|
559
|
+
| useCase | repository, service, serviceModule, generated | yes |
|
|
560
|
+
| repository | database, databaseSchema | no |
|
|
561
|
+
| service | generated, env | no |
|
|
562
|
+
| generated | — | yes |
|
|
563
|
+
| serviceModule | generated, env | yes, within the same module |
|
|
564
|
+
| database | databaseSchema, env | no |
|
|
565
|
+
| databaseSchema | — | yes |
|
|
566
|
+
| helper | — | no |
|
|
567
|
+
| env | — | no |
|
|
568
|
+
| types | — | no |
|
|
569
|
+
|
|
570
|
+
`generated` and `databaseSchema` are structural exceptions because they contain internal files that import each other. Any `src/services/<service-kebab>/` folder is a generic service module: files inside the same module can import each other, different modules cannot.
|
|
571
|
+
|
|
572
|
+
The strict backend preset will report existing violations such as:
|
|
573
|
+
|
|
574
|
+
- use-cases importing `database` or `helpers`;
|
|
575
|
+
- services importing other services;
|
|
576
|
+
- repositories importing other repositories, including `Setup`.
|
|
577
|
+
|
|
578
|
+
These violations are intentional and should be fixed during migration.
|
|
579
|
+
|
|
580
|
+
### Frontend preset relations
|
|
581
|
+
|
|
582
|
+
| Layer | Can import | Same layer |
|
|
583
|
+
|-------|------------|------------|
|
|
584
|
+
| components | components, hooks, contexts, services, generated, utils, helpers, constants, env | yes |
|
|
585
|
+
| hooks | hooks, contexts, services, generated, utils, helpers, constants, env | yes |
|
|
586
|
+
| contexts | services, generated, utils, helpers, constants, env | no |
|
|
587
|
+
| services | generated, utils, helpers, constants, env | no |
|
|
588
|
+
| generated | — | yes |
|
|
589
|
+
| utils | helpers, constants, env | no |
|
|
590
|
+
| helpers | utils, constants, env | no |
|
|
591
|
+
| constants | — | no |
|
|
592
|
+
| env | — | no |
|
|
593
|
+
| page | globals + same-page locals | yes (same page) |
|
|
594
|
+
| pageComponent | globals + same-page locals (including page main) | yes |
|
|
595
|
+
| pageHook | hooks/contexts/services/generated/utils/helpers/constants/env + same-page local hooks/services/contexts/helpers | yes |
|
|
596
|
+
| pageService | services, generated, utils, helpers, constants, env + same-page pageHelper | no |
|
|
597
|
+
| pageContext | contexts, services, generated, utils, helpers, constants, env + same-page pageService/pageHelper | no |
|
|
598
|
+
| pageHelper | helpers, utils, constants, env | no |
|
|
599
|
+
|
|
600
|
+
Local layers can import global layers. Global layers cannot import page-local layers. `pageHook` cannot import `pageComponent`.
|
|
601
|
+
|
|
602
|
+
`utils` and `helpers` can import each other. If this creates undesirable cycles, remove one direction.
|
|
603
|
+
|
|
604
|
+
### Same-layer and page scope
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
relations: {
|
|
608
|
+
layers: {
|
|
609
|
+
pageComponent: 'src/pages/*/components',
|
|
610
|
+
pageHook: 'src/pages/*/hooks',
|
|
611
|
+
page: 'src/pages/*',
|
|
612
|
+
},
|
|
613
|
+
scope: {
|
|
614
|
+
capture: 'page',
|
|
615
|
+
layers: ['pageComponent', 'pageHook', 'page'],
|
|
616
|
+
},
|
|
617
|
+
rules: [
|
|
618
|
+
{
|
|
619
|
+
from: 'pageComponent',
|
|
620
|
+
allow: ['pageHook'],
|
|
621
|
+
allowSameLayer: true,
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
A file from `src/pages/orders` can import another file from `src/pages/orders`, but not from `src/pages/users`.
|
|
628
|
+
|
|
629
|
+
### Type-only imports
|
|
630
|
+
|
|
631
|
+
Type-only imports follow the same restrictions by default:
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
import type { TUser } from '@/repositories/UserRepository' // blocked
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Allow them globally:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
relations: {
|
|
641
|
+
allowTypeImports: true,
|
|
642
|
+
layers: { /* ... */ },
|
|
643
|
+
rules: [/* ... */],
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
Or for a specific layer:
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
{
|
|
651
|
+
from: 'controller',
|
|
652
|
+
allow: ['useCase'],
|
|
653
|
+
allowTypeImports: true,
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Deny rules and messages
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
relations: {
|
|
661
|
+
layers: {
|
|
662
|
+
controller: 'src/http/controllers',
|
|
663
|
+
repository: 'src/repositories',
|
|
664
|
+
},
|
|
665
|
+
rules: [
|
|
666
|
+
{
|
|
667
|
+
from: 'controller',
|
|
668
|
+
deny: ['repository'],
|
|
669
|
+
message: 'Controllers must use use-cases instead of repositories.',
|
|
670
|
+
},
|
|
671
|
+
],
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Aliases and TypeScript
|
|
170
676
|
|
|
171
|
-
|
|
677
|
+
When the project root has a `tsconfig.json`, the generated config uses `eslint-import-resolver-typescript`, so aliases such as `@/repositories/UserRepository` are resolved automatically.
|
|
172
678
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
-
|
|
177
|
-
|
|
178
|
-
|
|
679
|
+
Make sure the resolver is installed at the project level:
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
npm i -D eslint-import-resolver-typescript
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
For JS projects without `tsconfig.json`, the Node resolver is used.
|
|
686
|
+
|
|
687
|
+
`relations` uses `eslint-plugin-boundaries` under the hood and enables `boundaries/dependencies`.
|
|
688
|
+
|
|
689
|
+
## Expected diagnostics
|
|
690
|
+
|
|
691
|
+
Configuration warning:
|
|
692
|
+
|
|
693
|
+
```text
|
|
694
|
+
[eslint-plugin-ai-rules] Project structure validation is not configured.
|
|
695
|
+
Configure it with aiRules.configs.projects([{ rootFolder: '.', preset: 'frontend' | 'backend' }]).
|
|
696
|
+
If this project is not a frontend/backend, use preset: 'other' to disable this warning.
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Structure violation:
|
|
700
|
+
|
|
701
|
+
```text
|
|
702
|
+
src/pages/MyFeature/Dashboard.tsx
|
|
703
|
+
error Folder "MyFeature" inside "src/pages" must match kebab-case. ai-rules/project-structure
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
Relation violation:
|
|
707
|
+
|
|
708
|
+
```text
|
|
709
|
+
src/http/controllers/ListProducts.ts
|
|
710
|
+
error Dependencies to elements of type "repository" are not allowed in elements of type "controller". boundaries/dependencies
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Custom relation message:
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
{
|
|
717
|
+
from: 'controller',
|
|
718
|
+
deny: ['repository'],
|
|
719
|
+
message: 'Controllers must use use-cases instead of repositories.',
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## Preset reference
|
|
724
|
+
|
|
725
|
+
### Frontend
|
|
726
|
+
|
|
727
|
+
```text
|
|
728
|
+
<root>/
|
|
729
|
+
├── root files: package.json, lockfiles, *.config.{js,mjs,cjs,ts},
|
|
730
|
+
│ tsconfig*.json, .env*, Dockerfile, entrypoint.sh, nginx.conf,
|
|
731
|
+
│ index.html, AGENTS.md, README.md, LICENSE
|
|
732
|
+
├── src/
|
|
733
|
+
│ ├── App.tsx, AppRoutes.tsx, main.tsx, index.css, vite-env.d.ts
|
|
734
|
+
│ ├── components/*.tsx
|
|
735
|
+
│ ├── components/ui/*.{ts,tsx}
|
|
736
|
+
│ ├── constants/*.ts
|
|
737
|
+
│ ├── contexts/*.tsx
|
|
738
|
+
│ ├── env/index.ts
|
|
739
|
+
│ ├── helpers/*.ts
|
|
740
|
+
│ ├── hooks/*.ts
|
|
741
|
+
│ ├── pages/*.tsx
|
|
742
|
+
│ ├── pages/<feature-kebab>/
|
|
743
|
+
│ │ ├── *.tsx, helpers.ts, schema.ts, types.ts
|
|
744
|
+
│ │ ├── components/*.tsx
|
|
745
|
+
│ │ ├── hooks/*.ts
|
|
746
|
+
│ │ ├── services/*.ts
|
|
747
|
+
│ │ ├── contexts/*.tsx
|
|
748
|
+
│ │ ├── helpers/*.ts
|
|
749
|
+
│ │ └── tabs/<tab-kebab>/
|
|
750
|
+
│ │ ├── *.tsx
|
|
751
|
+
│ │ └── <section-kebab>/
|
|
752
|
+
│ │ ├── *.tsx, *.ts
|
|
753
|
+
│ │ ├── components/*.tsx
|
|
754
|
+
│ │ └── contents/
|
|
755
|
+
│ │ ├── *.tsx
|
|
756
|
+
│ │ └── components/*.tsx
|
|
757
|
+
│ ├── services/generated/** (ignored)
|
|
758
|
+
│ ├── services/utils/*.ts
|
|
759
|
+
│ └── utils/*.ts
|
|
760
|
+
├── e2e/
|
|
761
|
+
│ ├── .auth/** (ignored)
|
|
762
|
+
│ ├── fixtures/*
|
|
763
|
+
│ ├── pages/*.ts
|
|
764
|
+
│ ├── specs/*.spec.ts
|
|
765
|
+
│ └── support/*.ts
|
|
766
|
+
│ └── api/*.ts
|
|
767
|
+
├── scripts/*.ts
|
|
768
|
+
├── scripts/types/** (ignored)
|
|
769
|
+
├── public/** (ignored)
|
|
770
|
+
└── .github/**, .vscode/** (ignored)
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Backend
|
|
774
|
+
|
|
775
|
+
```text
|
|
776
|
+
<root>/
|
|
777
|
+
├── root files: package.json, lockfiles, *.config.{js,mjs,cjs,ts},
|
|
778
|
+
│ tsconfig*.json, .env*, Dockerfile, .dockerignore,
|
|
779
|
+
│ AGENTS.md, README.md, LICENSE
|
|
780
|
+
├── src/
|
|
781
|
+
│ ├── index.ts, Server.ts, Schemas.ts
|
|
782
|
+
│ ├── env/index.ts
|
|
783
|
+
│ ├── helpers/*.ts
|
|
784
|
+
│ ├── @types/*.d.ts
|
|
785
|
+
│ ├── database/
|
|
786
|
+
│ │ ├── Connection.ts
|
|
787
|
+
│ │ ├── schema/index.ts, *.ts
|
|
788
|
+
│ │ ├── seeds/index.ts
|
|
789
|
+
│ │ └── migrations/** (ignored)
|
|
790
|
+
│ ├── repositories/Setup.ts, *.ts
|
|
791
|
+
│ ├── http/
|
|
792
|
+
│ │ ├── Routes.ts
|
|
793
|
+
│ │ ├── controllers/Health.ts
|
|
794
|
+
│ │ ├── controllers/<feature-kebab>/
|
|
795
|
+
│ │ │ ├── *.ts
|
|
796
|
+
│ │ │ └── <resource-kebab>/*.ts
|
|
797
|
+
│ │ ├── hooks/*.ts
|
|
798
|
+
│ │ └── types/*.d.ts
|
|
799
|
+
│ ├── services/*.ts
|
|
800
|
+
│ ├── services/<service-kebab>/index.ts, *.ts
|
|
801
|
+
│ ├── services/generated/** (ignored)
|
|
802
|
+
│ └── use-case/
|
|
803
|
+
│ ├── Errors.ts
|
|
804
|
+
│ └── <feature-kebab>/
|
|
805
|
+
│ ├── *.ts
|
|
806
|
+
│ └── <resource-kebab>/*.ts
|
|
807
|
+
├── test/*.test.ts, GlobalSetup.ts, Setup.ts, ProvidedContext.d.ts
|
|
808
|
+
├── test/Helpers/*.ts
|
|
809
|
+
├── docs/** (ignored)
|
|
810
|
+
├── scripts/*.ts
|
|
811
|
+
├── uploads/** (ignored)
|
|
812
|
+
└── .github/**, .vscode/** (ignored)
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
### Other
|
|
816
|
+
|
|
817
|
+
`preset: 'other'` has an empty structure. It disables validation and suppresses the configuration warning. If you provide a `structure`, it validates exactly what you declared.
|
|
818
|
+
|
|
819
|
+
## Structure DSL
|
|
179
820
|
|
|
180
821
|
```ts
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
822
|
+
type TProjectPreset = 'frontend' | 'backend' | 'other'
|
|
823
|
+
|
|
824
|
+
type TNameCase = 'PascalCase' | 'camelCase' | 'kebab-case' | 'snake_case'
|
|
825
|
+
|
|
826
|
+
interface INameRule {
|
|
827
|
+
case?: TNameCase
|
|
828
|
+
prefix?: string
|
|
829
|
+
suffix?: string
|
|
830
|
+
pattern?: string
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
type TFileRule =
|
|
834
|
+
| string
|
|
835
|
+
| ({ extensions: string[] } & INameRule)
|
|
836
|
+
|
|
837
|
+
interface IStructureNode {
|
|
838
|
+
name?: INameRule
|
|
839
|
+
files?: TFileRule[]
|
|
840
|
+
additionalFiles?: TFileRule[]
|
|
841
|
+
folders?: Record<string, IStructureNode>
|
|
842
|
+
ignore?: boolean
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
interface IProjectStructure extends IStructureNode {
|
|
846
|
+
ignoredPaths?: string[]
|
|
847
|
+
}
|
|
185
848
|
```
|
|
186
849
|
|
|
850
|
+
### Folder keys
|
|
851
|
+
|
|
852
|
+
Literal keys match exactly:
|
|
853
|
+
|
|
854
|
+
```ts
|
|
855
|
+
folders: {
|
|
856
|
+
components: { files: ['*.tsx'] },
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
Dynamic keys use brackets:
|
|
861
|
+
|
|
862
|
+
```ts
|
|
863
|
+
folders: {
|
|
864
|
+
'[feature]': {
|
|
865
|
+
name: { case: 'kebab-case' },
|
|
866
|
+
files: ['*.tsx'],
|
|
867
|
+
},
|
|
868
|
+
}
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
- Only one dynamic folder is allowed per level.
|
|
872
|
+
- `name` applies to dynamic folders.
|
|
873
|
+
- A static folder key cannot have a `name` rule.
|
|
874
|
+
- A dynamic folder without `name` accepts any folder name.
|
|
875
|
+
|
|
876
|
+
### File rules
|
|
877
|
+
|
|
878
|
+
Strings match exact names or globs:
|
|
879
|
+
|
|
880
|
+
```ts
|
|
881
|
+
files: [
|
|
882
|
+
'index.ts',
|
|
883
|
+
'*.spec.ts',
|
|
884
|
+
'*.config.{js,mjs,cjs,ts}',
|
|
885
|
+
]
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
Supported glob syntax: `*`, `**`, `?` and `{a,b}`.
|
|
889
|
+
|
|
890
|
+
Object rules require `extensions` and validate the base name:
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
files: [
|
|
894
|
+
{
|
|
895
|
+
extensions: ['.ts', '.tsx'],
|
|
896
|
+
case: 'PascalCase',
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
extensions: ['.ts'],
|
|
900
|
+
case: 'PascalCase',
|
|
901
|
+
suffix: 'UseCase',
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
extensions: ['.ts'],
|
|
905
|
+
case: 'PascalCase',
|
|
906
|
+
prefix: 'Use',
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
extensions: ['.ts'],
|
|
910
|
+
pattern: '^(Create|Read|Update|Delete|List)[A-Z][A-Za-z0-9]*$',
|
|
911
|
+
},
|
|
912
|
+
]
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
- `extensions` also supports compound extensions: `.d.ts`, `.test.mjs`, `.spec.ts`.
|
|
916
|
+
- `case`, `prefix`, `suffix` and `pattern` apply to the base name after removing the extension.
|
|
917
|
+
- A file is valid when it matches at least one rule completely.
|
|
918
|
+
|
|
919
|
+
### Merge behavior
|
|
920
|
+
|
|
921
|
+
| Field | Behavior |
|
|
922
|
+
|-------|----------|
|
|
923
|
+
| `folders` | recursive merge by key; new keys are added |
|
|
924
|
+
| `files` | replaces the preset files at that node |
|
|
925
|
+
| `additionalFiles` | appends to the preset files |
|
|
926
|
+
| `name` | fields override the preset |
|
|
927
|
+
| `ignore` | overrides the preset |
|
|
928
|
+
| `ignoredPaths` | union of preset and custom paths |
|
|
929
|
+
|
|
930
|
+
## Limitations
|
|
931
|
+
|
|
932
|
+
- Structure validation only runs for files ESLint actually lints.
|
|
933
|
+
- Required empty folders are not detected.
|
|
934
|
+
- Glob matching supports `*`, `**`, `?` and `{a,b}` only.
|
|
935
|
+
- Nested roots are supported, but duplicate roots are rejected.
|
|
936
|
+
- Relation layers must point to folders, not individual files.
|
|
937
|
+
- Scoped layers must use patterns with a wildcard, such as `src/pages/*`.
|
|
938
|
+
- TypeScript aliases in `relations` require the project `tsconfig.json`.
|
|
939
|
+
- Type-only imports follow the same restrictions by default.
|
|
940
|
+
- The backend preset is strict and will flag existing imports that violate AGENTS.md.
|
|
941
|
+
- The frontend preset allows `utils` and `helpers` to import each other, which can create cycles.
|
|
942
|
+
- ESLint versions below 9.22 are not supported.
|
|
943
|
+
|
|
944
|
+
## Rules
|
|
945
|
+
|
|
946
|
+
### `ai-rules/project-structure`
|
|
947
|
+
|
|
948
|
+
Validates the configured hierarchy, folder names and file names.
|
|
949
|
+
|
|
950
|
+
- Type: `problem`
|
|
951
|
+
- No autofix
|
|
952
|
+
- Reads `settings['ai-rules'].project` or `settings['ai-rules'].projects`
|
|
953
|
+
- Messages: `folderNotAllowed`, `folderNameMismatch`, `fileNotAllowed`, `fileNameMismatch`
|
|
954
|
+
- Ignores files outside the project root, `ignoredPaths` and `ignore: true` subtrees
|
|
955
|
+
|
|
956
|
+
### `ai-rules/project-setup`
|
|
957
|
+
|
|
958
|
+
Prints the configuration warning described above.
|
|
959
|
+
|
|
960
|
+
- Type: `suggestion`
|
|
961
|
+
- Enabled as `warn` in `recommended`
|
|
962
|
+
- Does not report lint messages
|
|
963
|
+
- Warns once per process
|
|
964
|
+
- Suppressed by `preset: 'other'`, `frontend`, `backend` or a non-empty custom structure
|
|
965
|
+
|
|
966
|
+
### `ai-rules/filename-pascal-case`
|
|
967
|
+
|
|
968
|
+
Enforces PascalCase for file names.
|
|
969
|
+
|
|
970
|
+
- Ignores `index.{ts,tsx,js,jsx}`, `*.d.ts`, `*.css` and dotfiles.
|
|
971
|
+
- Options:
|
|
972
|
+
- `ignore`: regex strings matched against the basename and relative path.
|
|
973
|
+
- `ignoreMiddleExtensions`: defaults to `true`, so `ValidationRanges.test.ts` is valid.
|
|
974
|
+
|
|
187
975
|
```ts
|
|
188
976
|
'ai-rules/filename-pascal-case': [
|
|
189
977
|
'error',
|
|
@@ -194,86 +982,224 @@ Enforces `PascalCase` for file names. The rule is file-agnostic: it validates ev
|
|
|
194
982
|
]
|
|
195
983
|
```
|
|
196
984
|
|
|
197
|
-
|
|
985
|
+
`projects()` disables this rule because `project-structure` enforces file names.
|
|
986
|
+
|
|
987
|
+
### `ai-rules/no-multiline-imports`
|
|
988
|
+
|
|
989
|
+
Disallows multiline import declarations. Fixable.
|
|
198
990
|
|
|
199
991
|
```ts
|
|
200
|
-
|
|
201
|
-
import {
|
|
992
|
+
// before
|
|
993
|
+
import {
|
|
994
|
+
b,
|
|
995
|
+
a,
|
|
996
|
+
} from 'lodash'
|
|
997
|
+
|
|
998
|
+
// after --fix
|
|
999
|
+
import { b, a } from 'lodash'
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### `ai-rules/sort-imports`
|
|
1003
|
+
|
|
1004
|
+
Sorts imports by group and line length descending, collapsing multiline imports. Fixable.
|
|
1005
|
+
|
|
1006
|
+
Groups:
|
|
1007
|
+
|
|
1008
|
+
1. Directives (`'use client'`, `'use server'`, `'use strict'`)
|
|
1009
|
+
2. External packages (`react` first, then by line length desc)
|
|
1010
|
+
3. Internal imports (`.`, `@/`)
|
|
1011
|
+
4. Re-exports
|
|
1012
|
+
|
|
1013
|
+
## Bundled rules from other plugins
|
|
1014
|
+
|
|
1015
|
+
`recommended` also registers and applies these rules. They existed before project structure support and remain part of the default setup.
|
|
202
1016
|
|
|
1017
|
+
| Rule | Default | Purpose |
|
|
1018
|
+
|------|---------|---------|
|
|
1019
|
+
| `import-x/no-duplicates` | `error` | Prevent importing the same module twice |
|
|
1020
|
+
| `import-x/no-default-export` | `off` | Default exports are allowed |
|
|
1021
|
+
| `max-len` | `off` | No line length limit |
|
|
1022
|
+
| `@stylistic/semi` | `['error', 'never']` | No semicolons |
|
|
1023
|
+
| `@typescript-eslint/consistent-type-imports` | `['error', { prefer: 'type-imports' }]` | Prefer `import type` |
|
|
1024
|
+
| `@typescript-eslint/no-unused-vars` | `['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]` | Ignore unused values prefixed with `_` |
|
|
1025
|
+
| `@typescript-eslint/naming-convention` | interfaces `I` prefix, type aliases `T` prefix | Enforce `IUser` and `TUser` style |
|
|
1026
|
+
|
|
1027
|
+
When `relations` is configured, `projects()` also registers `eslint-plugin-boundaries` and enables `boundaries/dependencies` with the generated layer policies.
|
|
1028
|
+
|
|
1029
|
+
The plugin also configures the `import-x` resolver with:
|
|
1030
|
+
|
|
1031
|
+
```ts
|
|
1032
|
+
extensions: ['.ts', '.tsx', '.d.ts', '.js', '.jsx']
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
Override any bundled rule after the preset:
|
|
1036
|
+
|
|
1037
|
+
```ts
|
|
203
1038
|
export default defineConfig([
|
|
204
|
-
...
|
|
205
|
-
{
|
|
1039
|
+
...aiRules.configs.projects([{ rootFolder: '.', preset: 'frontend' }]),
|
|
1040
|
+
{
|
|
1041
|
+
rules: {
|
|
1042
|
+
'@stylistic/semi': ['error', 'always'],
|
|
1043
|
+
'import-x/no-default-export': 'off',
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
206
1046
|
])
|
|
207
1047
|
```
|
|
208
1048
|
|
|
209
|
-
|
|
1049
|
+
## What runs automatically
|
|
1050
|
+
|
|
1051
|
+
| You use | What is enabled |
|
|
1052
|
+
|---------|-----------------|
|
|
1053
|
+
| `aiRules.configs.recommended` | bundled rules + configuration warning |
|
|
1054
|
+
| `aiRules.configs.projects([...])` | recommended rules + `project-structure` |
|
|
1055
|
+
| `projects([...])` with `preset: 'frontend' \| 'backend'` | preset structure + preset relations |
|
|
1056
|
+
| `projects([...])` with `structure` | structure validation |
|
|
1057
|
+
| `projects([...])` with `relations` | `boundaries/dependencies` |
|
|
1058
|
+
| `projects([...])` with both | structure + relations |
|
|
1059
|
+
|
|
1060
|
+
`projects()` does **not** configure the TypeScript parser. Pair it with `typescript-eslint` as shown in the examples.
|
|
1061
|
+
|
|
1062
|
+
## Recommended preset
|
|
1063
|
+
|
|
1064
|
+
`aiRules.configs.recommended` is file-agnostic. It provides:
|
|
1065
|
+
|
|
1066
|
+
- plugins: `@stylistic`, `import-x`, `@typescript-eslint`, `ai-rules`
|
|
1067
|
+
- `settings['import-x/resolver-next']`
|
|
1068
|
+
- the rule set documented above
|
|
1069
|
+
|
|
1070
|
+
It does **not** configure:
|
|
1071
|
+
|
|
1072
|
+
- `files`
|
|
1073
|
+
- `ignores`
|
|
1074
|
+
- parser
|
|
1075
|
+
- language options
|
|
1076
|
+
- globals
|
|
1077
|
+
|
|
1078
|
+
Pair it with `typescript-eslint` or `@typescript-eslint/parser` and define `files` in your config. `aiRules.configs.projects([...])` already does the file scoping for you.
|
|
1079
|
+
|
|
1080
|
+
If you want the recommended rules without project structure validation, use `projects([{ rootFolder: '.', preset: 'other' }])`.
|
|
210
1081
|
|
|
211
|
-
|
|
1082
|
+
## Troubleshooting
|
|
1083
|
+
|
|
1084
|
+
### The configuration warning keeps appearing
|
|
212
1085
|
|
|
213
|
-
|
|
214
|
-
- **Fix:** collapses `{`, `}`, `,` and whitespace to single line: `import { b, a } from 'lodash'`
|
|
1086
|
+
Use a valid project configuration:
|
|
215
1087
|
|
|
216
1088
|
```ts
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
} from 'lodash'
|
|
1089
|
+
...aiRules.configs.projects([
|
|
1090
|
+
{ rootFolder: '.', preset: 'frontend' },
|
|
1091
|
+
])
|
|
1092
|
+
```
|
|
222
1093
|
|
|
223
|
-
|
|
224
|
-
|
|
1094
|
+
Or explicitly opt out:
|
|
1095
|
+
|
|
1096
|
+
```ts
|
|
1097
|
+
...aiRules.configs.projects([
|
|
1098
|
+
{ rootFolder: '.', preset: 'other' },
|
|
1099
|
+
])
|
|
225
1100
|
```
|
|
226
1101
|
|
|
227
|
-
|
|
1102
|
+
A project with only `rootFolder` and an empty `structure` still warns.
|
|
228
1103
|
|
|
229
|
-
|
|
1104
|
+
For temporary runs or CI, use:
|
|
1105
|
+
|
|
1106
|
+
```bash
|
|
1107
|
+
AI_RULES_SILENT=1 npx eslint .
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
### `project-structure` is not running
|
|
1111
|
+
|
|
1112
|
+
Make sure you are using `aiRules.configs.projects([...])` or manually setting `settings['ai-rules'].project` and enabling `ai-rules/project-structure`.
|
|
230
1113
|
|
|
231
|
-
|
|
1114
|
+
### Type-aware rule error
|
|
232
1115
|
|
|
233
|
-
-
|
|
234
|
-
- **Groups (in order, each separated by one blank line):**
|
|
235
|
-
1. Directives (`'use client'`, `'use server'`, `'use strict'`)
|
|
236
|
-
2. External (`react` first, then other externals sorted by `line.length` desc → `localeCompare`)
|
|
237
|
-
3. Internal (`.`, `@/` sorted same)
|
|
238
|
-
4. Re-exports (`export { ... } from`, `export * from` sorted same)
|
|
239
|
-
- **Rest:** code after imports, joined with `\n\n\n` (two blank lines between imports and code)
|
|
240
|
-
- **Directives:** preserved at top while `canAcceptDirectives` is true
|
|
1116
|
+
`@typescript-eslint/consistent-type-imports` needs the TypeScript parser. Pair the plugin with:
|
|
241
1117
|
|
|
242
1118
|
```ts
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
1119
|
+
...tseslint.configs.recommended
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
### Missing `dist` before lint
|
|
247
1123
|
|
|
248
|
-
|
|
249
|
-
import React from 'react'
|
|
250
|
-
import { a } from 'lodash'
|
|
1124
|
+
This repository imports the local build from `dist`. Consumers should import the published package:
|
|
251
1125
|
|
|
252
|
-
|
|
1126
|
+
```ts
|
|
1127
|
+
import aiRules from 'eslint-plugin-ai-rules'
|
|
253
1128
|
```
|
|
254
1129
|
|
|
255
|
-
|
|
1130
|
+
### File ignored because outside of base path
|
|
256
1131
|
|
|
257
|
-
|
|
1132
|
+
Run ESLint from the project root, or configure the correct `basePath`.
|
|
258
1133
|
|
|
259
|
-
|
|
1134
|
+
### Flat config only
|
|
260
1135
|
|
|
261
|
-
|
|
1136
|
+
This plugin exports flat config arrays. It does not support legacy `.eslintrc`.
|
|
262
1137
|
|
|
263
|
-
|
|
1138
|
+
### Filename rule too strict
|
|
1139
|
+
|
|
1140
|
+
Use the `ignore` option or disable `ai-rules/filename-pascal-case` for tool-mandated files:
|
|
1141
|
+
|
|
1142
|
+
```ts
|
|
1143
|
+
{
|
|
1144
|
+
files: ['*.config.*', 'scripts/**/*'],
|
|
1145
|
+
rules: {
|
|
1146
|
+
'ai-rules/filename-pascal-case': 'off',
|
|
1147
|
+
},
|
|
1148
|
+
}
|
|
264
1149
|
```
|
|
265
|
-
|
|
1150
|
+
|
|
1151
|
+
## Common mistakes
|
|
1152
|
+
|
|
1153
|
+
### Using `recommended` without `projects()`
|
|
1154
|
+
|
|
1155
|
+
You get the configuration warning and no project structure validation. Use `projects([...])` or explicitly opt out with `preset: 'other'`.
|
|
1156
|
+
|
|
1157
|
+
### Forgetting the TypeScript parser
|
|
1158
|
+
|
|
1159
|
+
`aiRules.configs.recommended` does not configure `languageOptions.parser`. Pair it with `tseslint.configs.recommended` or a parser.
|
|
1160
|
+
|
|
1161
|
+
### Aliases are not enforced in relations
|
|
1162
|
+
|
|
1163
|
+
Make sure the project has `tsconfig.json` with `paths`, and install `eslint-import-resolver-typescript`.
|
|
1164
|
+
|
|
1165
|
+
### A relation layer does not match
|
|
1166
|
+
|
|
1167
|
+
Layers must point to folders, not files:
|
|
1168
|
+
|
|
1169
|
+
```ts
|
|
1170
|
+
layers: {
|
|
1171
|
+
controller: 'src/http/controllers', // correct
|
|
1172
|
+
// controller: 'src/http/controllers/*.ts', // avoid
|
|
1173
|
+
}
|
|
266
1174
|
```
|
|
267
|
-
Fix: `npm run build` first. This repo's `eslint.config.mjs` imports from `dist` (no `jiti`), so `prelint` runs `build` automatically (`package.json` `prelint`).
|
|
268
1175
|
|
|
269
|
-
|
|
270
|
-
|
|
1176
|
+
### Type-only imports are blocked
|
|
1177
|
+
|
|
1178
|
+
That is the default. Set `allowTypeImports: true` globally or per rule.
|
|
1179
|
+
|
|
1180
|
+
### An external package or unknown file is not blocked
|
|
1181
|
+
|
|
1182
|
+
`relations` only checks known local layers. External packages, Node core modules, same-layer imports and files outside any layer are allowed.
|
|
1183
|
+
|
|
1184
|
+
### Duplicate or overlapping roots
|
|
1185
|
+
|
|
1186
|
+
Nested roots are supported, but duplicate roots are rejected. Use the most specific configuration for each project.
|
|
1187
|
+
|
|
1188
|
+
## Migrating from 0.2.x
|
|
1189
|
+
|
|
1190
|
+
1. Replace `aiRules.configs.recommended` with `aiRules.configs.projects([...])`.
|
|
1191
|
+
2. Choose a preset: `frontend`, `backend`, or `other`.
|
|
1192
|
+
3. Set `rootFolder` to the folder containing `package.json`.
|
|
1193
|
+
4. Add `relations` if you want layer boundaries.
|
|
1194
|
+
5. Keep pairing with `typescript-eslint`.
|
|
271
1195
|
|
|
272
|
-
|
|
273
|
-
This plugin exports flat config arrays. It does not support legacy `.eslintrc`. Use `eslint.config.{js,ts}`.
|
|
1196
|
+
If you intentionally do not want structure validation:
|
|
274
1197
|
|
|
275
|
-
|
|
276
|
-
|
|
1198
|
+
```ts
|
|
1199
|
+
...aiRules.configs.projects([
|
|
1200
|
+
{ rootFolder: '.', preset: 'other' },
|
|
1201
|
+
])
|
|
1202
|
+
```
|
|
277
1203
|
|
|
278
1204
|
## Development
|
|
279
1205
|
|
|
@@ -282,60 +1208,104 @@ git clone <repo>
|
|
|
282
1208
|
cd eslint-ai-guard
|
|
283
1209
|
npm install
|
|
284
1210
|
|
|
285
|
-
npm run typecheck
|
|
286
|
-
npm test
|
|
287
|
-
npm run lint
|
|
1211
|
+
npm run typecheck # tsc --noEmit
|
|
1212
|
+
npm test # node:test against dist
|
|
1213
|
+
npm run lint # eslint .
|
|
288
1214
|
npm run lint -- --fix
|
|
289
|
-
npm run build
|
|
290
|
-
npm run dev
|
|
291
|
-
npm run verify
|
|
292
|
-
npm
|
|
1215
|
+
npm run build # tsup -> ESM + CJS + declarations
|
|
1216
|
+
npm run dev # tsup --watch
|
|
1217
|
+
npm run verify # typecheck + test + lint + pack
|
|
1218
|
+
npm run test:coverage # c8 coverage with thresholds
|
|
1219
|
+
npm run test:consumer # install packed tarball in a temp project
|
|
1220
|
+
npm run test:package # publint + arethetypeswrong
|
|
1221
|
+
npm run test:release # consumer + package checks
|
|
1222
|
+
npm pack --dry-run # verify published files
|
|
293
1223
|
```
|
|
294
1224
|
|
|
295
|
-
|
|
1225
|
+
`test:consumer` and `test:package` are release checks and are not part of `verify`. They may use the network.
|
|
1226
|
+
|
|
1227
|
+
The consumer smoke test covers structure validation, relations with aliases and type-only imports, warning/suppression, ESM and CJS. It accepts `ESLINT_VERSION`:
|
|
296
1228
|
|
|
1229
|
+
```bash
|
|
1230
|
+
ESLINT_VERSION=9.22.0 npm run test:consumer
|
|
1231
|
+
ESLINT_VERSION='^10.0.0' npm run test:consumer
|
|
297
1232
|
```
|
|
298
|
-
|
|
1233
|
+
|
|
1234
|
+
CI runs `verify`, coverage and consumer checks on Node 20, 22 and 24.
|
|
1235
|
+
|
|
1236
|
+
### Project structure
|
|
1237
|
+
|
|
1238
|
+
```text
|
|
1239
|
+
eslint-ai-guard/
|
|
299
1240
|
├── src/
|
|
300
|
-
│ ├── index.ts
|
|
301
|
-
│ ├── plugin.ts
|
|
1241
|
+
│ ├── index.ts
|
|
1242
|
+
│ ├── plugin.ts
|
|
302
1243
|
│ ├── configs/
|
|
303
|
-
│ │ ├── recommended.ts
|
|
1244
|
+
│ │ ├── recommended.ts
|
|
1245
|
+
│ │ ├── projects.ts
|
|
304
1246
|
│ │ └── index.ts
|
|
1247
|
+
│ ├── types/
|
|
1248
|
+
│ │ └── structure.ts
|
|
1249
|
+
│ ├── structures/
|
|
1250
|
+
│ │ ├── buildRelationsConfig.ts
|
|
1251
|
+
│ │ ├── hasStructureRules.ts
|
|
1252
|
+
│ │ ├── mergeRelations.ts
|
|
1253
|
+
│ │ ├── mergeStructure.ts
|
|
1254
|
+
│ │ ├── validateRelations.ts
|
|
1255
|
+
│ │ ├── validateStructure.ts
|
|
1256
|
+
│ │ └── presets/
|
|
1257
|
+
│ │ ├── frontend.ts
|
|
1258
|
+
│ │ ├── backend.ts
|
|
1259
|
+
│ │ ├── other.ts
|
|
1260
|
+
│ │ ├── helpers.ts
|
|
1261
|
+
│ │ └── index.ts
|
|
1262
|
+
│ ├── utils/
|
|
305
1263
|
│ └── rules/
|
|
306
1264
|
│ ├── FilenamePascalCase.ts
|
|
307
1265
|
│ ├── NoMultilineImports.ts
|
|
308
|
-
│ ├──
|
|
1266
|
+
│ ├── ProjectSetup.ts
|
|
1267
|
+
│ ├── ProjectStructure.ts
|
|
1268
|
+
│ ├── SortImports.ts
|
|
309
1269
|
│ └── index.ts
|
|
310
|
-
├── test/
|
|
311
|
-
├──
|
|
312
|
-
|
|
313
|
-
├──
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
1270
|
+
├── test/
|
|
1271
|
+
├── scripts/
|
|
1272
|
+
│ └── TestConsumer.mjs
|
|
1273
|
+
├── .github/
|
|
1274
|
+
│ └── workflows/
|
|
1275
|
+
│ └── ci.yml
|
|
1276
|
+
├── eslint/
|
|
1277
|
+
├── CHANGELOG.md
|
|
1278
|
+
├── eslint.config.mjs
|
|
1279
|
+
├── tsconfig.json
|
|
1280
|
+
├── tsup.config.ts
|
|
1281
|
+
├── package.json
|
|
1282
|
+
└── dist/
|
|
317
1283
|
```
|
|
318
1284
|
|
|
319
1285
|
### Adding a new rule
|
|
320
1286
|
|
|
321
|
-
1. Create `src/rules/MyRule.ts
|
|
322
|
-
2. Export it from `src/rules/index.ts
|
|
323
|
-
3. Register in `src/plugin.ts
|
|
324
|
-
4.
|
|
325
|
-
5. Add
|
|
326
|
-
6. `npm run verify
|
|
1287
|
+
1. Create `src/rules/MyRule.ts`.
|
|
1288
|
+
2. Export it from `src/rules/index.ts`.
|
|
1289
|
+
3. Register it in `src/plugin.ts`.
|
|
1290
|
+
4. Add it to `src/configs/recommended.ts` if it should be enabled by default.
|
|
1291
|
+
5. Add tests in `test/`.
|
|
1292
|
+
6. Run `npm run verify`.
|
|
327
1293
|
|
|
328
|
-
|
|
1294
|
+
## Publishing
|
|
329
1295
|
|
|
330
1296
|
```bash
|
|
1297
|
+
npm run verify
|
|
1298
|
+
npm run test:coverage
|
|
1299
|
+
npm run test:release
|
|
331
1300
|
npm version patch|minor|major
|
|
332
|
-
npm publish --access public
|
|
333
|
-
# or manually: npm run verify && npm publish --access public
|
|
1301
|
+
npm publish --access public
|
|
334
1302
|
```
|
|
335
1303
|
|
|
1304
|
+
`prepublishOnly` runs `verify` and refuses to publish with any uncommitted, staged or untracked changes.
|
|
1305
|
+
|
|
336
1306
|
## Legacy
|
|
337
1307
|
|
|
338
|
-
`eslint/` keeps the three original rule
|
|
1308
|
+
`eslint/` keeps the three original rule implementations before packaging. Source of truth is `src/rules/`.
|
|
339
1309
|
|
|
340
1310
|
## License
|
|
341
1311
|
|