@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.
@@ -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.2.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": "^3"
62
+ "vitest": "^4"
63
63
  }
64
64
  }