@noctcore/eslint-plugin-architecture 0.2.0 → 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 +1 -0
- package/dist/index.cjs +581 -3
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +581 -3
- package/docs/rules/single-semantic-module.md +180 -0
- package/package.json +2 -2
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# `noctcore-architecture/single-semantic-module`
|
|
2
|
+
|
|
3
|
+
> Each module exports one semantic concern: types, constants, functions, classes, React components,
|
|
4
|
+
> hooks, schemas or enums. **Ships `off` in `recommended`**: choose the files and the allowed mixes
|
|
5
|
+
> for your codebase, then enable it.
|
|
6
|
+
|
|
7
|
+
Ported from
|
|
8
|
+
[`@boring-stack-pkg/eslint-plugin-module-boundaries`](https://github.com/boringstack-xyz/eslint-plugins/tree/main/eslint-plugin-module-boundaries)
|
|
9
|
+
0.2.0 (MIT). Upstream peers ESLint `8.57.0 || ^9.0.0`; this port runs on ESLint 9 and 10.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
A file that exports a component, the hook it uses, the constant that configures it and the type of its
|
|
14
|
+
props has four reasons to change and four kinds of consumer. Splitting by concern keeps imports
|
|
15
|
+
honest: a type-only consumer never pulls in runtime code, and a constant never drags a component into
|
|
16
|
+
a server bundle.
|
|
17
|
+
|
|
18
|
+
The rule classifies by **AST shape**, never by filename or suffix:
|
|
19
|
+
|
|
20
|
+
| Category | Shape |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `type` | `interface`, `type`, `namespace`, `declare ...` |
|
|
23
|
+
| `enum` | `enum` (or `type`, with `enumCategory: 'type'`) |
|
|
24
|
+
| `constant` | any other top-level value |
|
|
25
|
+
| `function` | function declaration or function-valued `const` |
|
|
26
|
+
| `class` | class declaration or class expression |
|
|
27
|
+
| `react-component` | PascalCase function returning JSX (or typed `FC` / `JSX.Element`) |
|
|
28
|
+
| `hook` | a function named like a hook (`^use[A-Z0-9]`) |
|
|
29
|
+
| `schema` | a call on an identifier imported from `zod`, `yup` or `valibot` |
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// ✗ a type and a runtime constant
|
|
35
|
+
export interface User {
|
|
36
|
+
id: string;
|
|
37
|
+
}
|
|
38
|
+
export const DEFAULT_USER: User = { id: 'anonymous' };
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// ✗ a component and a hook
|
|
43
|
+
export function UserCard() {
|
|
44
|
+
return <div />;
|
|
45
|
+
}
|
|
46
|
+
export function useUserCard() {
|
|
47
|
+
return useQuery(userCardQuery);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// ✓ one concern: types only
|
|
53
|
+
export interface User {
|
|
54
|
+
id: string;
|
|
55
|
+
}
|
|
56
|
+
export type UserId = User['id'];
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Private helpers do not count
|
|
60
|
+
|
|
61
|
+
Only the **exported** surface defines a module (`ignorePrivateDeclarations`, default `true`). A
|
|
62
|
+
non-exported render helper beside a component, or a filter constant inside a hook file, serves that
|
|
63
|
+
surface and is not a second concern:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// ✓ the helper is private
|
|
67
|
+
function renderBadge(count: number) {
|
|
68
|
+
return <span className="badge">{count}</span>;
|
|
69
|
+
}
|
|
70
|
+
export function Inbox({ unread }: { unread: number }) {
|
|
71
|
+
return <div>{renderBadge(unread)}</div>;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// ✓ the filter object is private
|
|
77
|
+
const ACTIVE_FILTER = { status: 'active', archived: false } as const;
|
|
78
|
+
export function useActiveProjects() {
|
|
79
|
+
return useQuery({ queryKey: ['projects', ACTIVE_FILTER] });
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A declaration exported further down by name (`export { DEFAULT_USER }`, `export default UserCard`)
|
|
84
|
+
is surface all the same.
|
|
85
|
+
|
|
86
|
+
## Options
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
type SemanticCategory =
|
|
90
|
+
| 'type' | 'constant' | 'function' | 'class'
|
|
91
|
+
| 'react-component' | 'hook' | 'schema' | 'enum';
|
|
92
|
+
|
|
93
|
+
type Options = {
|
|
94
|
+
/** Category sets a module may mix. Default []. */
|
|
95
|
+
allow?: SemanticCategory[][];
|
|
96
|
+
/** Whether an enum is its own category or a type. Default 'enum'. */
|
|
97
|
+
enumCategory?: 'enum' | 'type';
|
|
98
|
+
/** List every classified declaration and the reason in the report. Default false. */
|
|
99
|
+
debug?: boolean;
|
|
100
|
+
/** Skip `declare ...` and `declare global` entirely. Default false. */
|
|
101
|
+
ignoreAmbientDeclarations?: boolean;
|
|
102
|
+
/** Classify exported declarations only. Default true. */
|
|
103
|
+
ignorePrivateDeclarations?: boolean;
|
|
104
|
+
/** Libraries whose builders count as `schema`. Default ['zod', 'yup', 'valibot']. */
|
|
105
|
+
schemaLibraries?: Array<'zod' | 'yup' | 'valibot'>;
|
|
106
|
+
/** Default { enabled: true }. */
|
|
107
|
+
reactComponentDetection?: { enabled?: boolean };
|
|
108
|
+
/** Default { enabled: true, namePattern: '^use[A-Z0-9].*' }. */
|
|
109
|
+
hookDetection?: { enabled?: boolean; namePattern?: string };
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `allow`: a NestJS `.constants.ts`
|
|
114
|
+
|
|
115
|
+
In a NestJS module, `<name>.constants.ts` holds one module's enums, unions and DI tokens. That mixes
|
|
116
|
+
categories **by design**, so allow the mix for those files only:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
// eslint.config.js
|
|
120
|
+
export default [
|
|
121
|
+
{
|
|
122
|
+
files: ['apps/api/src/**/*.ts'],
|
|
123
|
+
rules: { 'noctcore-architecture/single-semantic-module': 'error' },
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
files: ['apps/api/src/**/*.constants.ts'],
|
|
127
|
+
rules: {
|
|
128
|
+
'noctcore-architecture/single-semantic-module': [
|
|
129
|
+
'error',
|
|
130
|
+
{ allow: [['constant', 'type', 'enum']] },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// ✓ billing.constants.ts with the config above
|
|
139
|
+
export const BILLING_QUEUE = Symbol('BILLING_QUEUE');
|
|
140
|
+
export const INVOICE_STATUSES = ['draft', 'sent', 'paid'] as const;
|
|
141
|
+
export type InvoiceStatus = (typeof INVOICE_STATUSES)[number];
|
|
142
|
+
export enum BillingEvent {
|
|
143
|
+
Paid = 'billing.paid',
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ✗ still reported: a function is outside the allowed group
|
|
147
|
+
export function isPaid(status: InvoiceStatus) {
|
|
148
|
+
return status === 'paid';
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
An `allow` group must cover **every** detected category; a module whose categories fit inside any one
|
|
153
|
+
group passes.
|
|
154
|
+
|
|
155
|
+
### `enumCategory`
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
{ enumCategory: 'type' } // `export enum Role {}` beside `export type User = {}` is one concern
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `ignorePrivateDeclarations: false`
|
|
162
|
+
|
|
163
|
+
Restores upstream's original behaviour, where every top-level declaration counts. Expect it to fight
|
|
164
|
+
every non-trivial file.
|
|
165
|
+
|
|
166
|
+
### `debug`
|
|
167
|
+
|
|
168
|
+
Adds each classified declaration and the reason to the report, which is the quickest way to see why a
|
|
169
|
+
file was flagged:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
- type: User (TypeScript type-space declaration)
|
|
173
|
+
- constant: DEFAULT_USER (object literal runtime value)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## When not to use it
|
|
177
|
+
|
|
178
|
+
Barrels and generated files. Re-exports (`export * from`, `export { x } from`) are never classified, so
|
|
179
|
+
a pure barrel passes anyway, but generated clients routinely mix every category; scope the rule away
|
|
180
|
+
from them with `files` / `ignores`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noctcore/eslint-plugin-architecture",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Framework-agnostic folder-per-component and feature-boundary architecture ESLint rules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -59,6 +59,6 @@
|
|
|
59
59
|
"@typescript-eslint/rule-tester": "^8.61.1",
|
|
60
60
|
"tsup": "^8.5.1",
|
|
61
61
|
"typescript": "^5.6.0",
|
|
62
|
-
"vitest": "^
|
|
62
|
+
"vitest": "^4"
|
|
63
63
|
}
|
|
64
64
|
}
|