@noctcore/eslint-plugin-architecture 0.3.0 → 0.3.2

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/dist/index.cjs CHANGED
@@ -1385,7 +1385,7 @@ var rules = {
1385
1385
 
1386
1386
  // src/index.ts
1387
1387
  var NAMESPACE = "noctcore-architecture";
1388
- var VERSION = "0.3.0";
1388
+ var VERSION = "0.3.2";
1389
1389
  var plugin = {
1390
1390
  meta: { name: "@noctcore/eslint-plugin-architecture", version: VERSION },
1391
1391
  rules,
package/dist/index.js CHANGED
@@ -1347,7 +1347,7 @@ var rules = {
1347
1347
 
1348
1348
  // src/index.ts
1349
1349
  var NAMESPACE = "noctcore-architecture";
1350
- var VERSION = "0.3.0";
1350
+ var VERSION = "0.3.2";
1351
1351
  var plugin = {
1352
1352
  meta: { name: "@noctcore/eslint-plugin-architecture", version: VERSION },
1353
1353
  rules,
@@ -14,25 +14,26 @@ folder now silently pulls that behavior in, and the folder no longer has a clean
14
14
  Only `index.ts` / `index.tsx` (and the other index extensions) are inspected. Each top-level
15
15
  statement that is not a pure re-export is reported.
16
16
 
17
- ```ts
18
- // index.ts
19
-
20
- export { Card } from './Card'; // named re-export
21
- export * from './Card.types'; // star re-export
22
- export * as card from './Card'; // namespace re-export
23
- export { default as Card } from './Card'; // default re-export
24
- import { a } from './a'; // import that feeds a re-export
25
- export { a }; // …its matching specifier
26
- export type { Props } from './Card'; // ✓ type re-export
27
- export default Card; // ✓ re-export of a binding by name
28
-
29
- export const helper = 1; // ✗ local declaration
30
- export function build() {} // local declaration
31
- export type T = string; // local declaration
32
- export default () => 1; // a value, not a re-export
33
- import './styles.css'; // side-effect import
34
- console.log('hi'); // side-effect statement
35
- const cache = new Map(); // ✗ non-export code
17
+ ```ts good filename=src/components/Card/index.ts
18
+ export { Card } from './Card'; // named re-export
19
+ export * from './Card.types'; // star re-export
20
+ export * as card from './Card'; // namespace re-export
21
+ export { default as CardView } from './Card'; // default re-export
22
+ import { a } from './a'; // import that feeds a re-export
23
+ export { a }; // ...its matching specifier
24
+ export type { Props } from './Card'; // type re-export
25
+ import Card from './Card';
26
+ export default Card; // re-export of a binding by name
27
+ ```
28
+
29
+ ```ts bad reports=7 filename=src/components/Card/index.ts
30
+ export const helper = 1; // local declaration
31
+ export function build() {} // local declaration
32
+ export type T = string; // local declaration
33
+ export default () => 1; // a value, not a re-export
34
+ import './styles.css'; // side-effect import
35
+ console.log('hi'); // side-effect statement
36
+ const cache = new Map(); // non-export code
36
37
  ```
37
38
 
38
39
  An import that carries bindings is allowed because it feeds a re-export; a specifier-less
@@ -14,7 +14,7 @@ is obvious when missing.
14
14
  For a file matching `include`, the rule reads the file's directory and looks for a sibling whose name
15
15
  is `<stem>.test.<ext>` or `<stem>.spec.<ext>` (any extension). If none exists, it reports.
16
16
 
17
- ```
17
+ ```text prose reason="the rule checks for a sibling test file on disk"
18
18
  src/hooks/useCart.ts ← include: ['**/use*.ts']
19
19
  src/hooks/useCart.test.ts ✓ colocated test present
20
20
  src/hooks/useWishlist.ts ✗ no useWishlist.test.* / .spec.* sibling
@@ -9,13 +9,19 @@ In a folder-per-component layout, a component is a folder — not a lone `.tsx`.
9
9
  `index.ts` barrel always travel with the component, refactors stay local and nothing is quietly
10
10
  untested or undocumented. This rule enforces that colocation by construction.
11
11
 
12
+ A purely presentational component has no logic to extract, and its `.hooks.ts` ends up as an
13
+ `export {};` with a comment. That is the cost of the convention, and it is deliberate: the file
14
+ being present means there is one obvious place for logic to go the day the component grows some,
15
+ and a reviewer never has to ask where it lives. A project that disagrees drops `.hooks.ts` from
16
+ `requiredSiblings` rather than carrying stub files.
17
+
12
18
  ## What it flags
13
19
 
14
20
  For every **component entry file** — a PascalCase `.tsx` whose basename equals its parent folder
15
21
  (`TaskCard/TaskCard.tsx`) — that lives under the configured `componentRoot`, the rule reads the
16
22
  component's directory and reports any sibling from the required set that is missing on disk.
17
23
 
18
- ```
24
+ ```text prose reason="the rule checks for sibling files on disk"
19
25
  components/board/TaskCard/
20
26
  TaskCard.tsx ← entry file (checked)
21
27
  TaskCard.hooks.ts ┐
@@ -22,10 +22,30 @@ The rule first resolves the file's **primary export**:
22
22
  The basename and the identifier are compared **case- and separator-insensitively**, so naming
23
23
  conventions never collide:
24
24
 
25
- ```ts
26
- // TaskCard.tsx → export default function TaskCard() {} ✓
27
- // use-thing.ts → export const useThing = () => {} ✓ (kebab ↔ camel)
28
- // helpers.ts → export const formatDate = () => {} ✗ (genuine mismatch)
25
+ ```ts bad filename=src/utils/helpers.ts
26
+ // genuine mismatch
27
+ export const formatDate = () => {};
28
+ ```
29
+
30
+ Renaming the file is one fix, renaming the export the other:
31
+
32
+ ```ts good filename=src/utils/format-date.ts relocation
33
+ export const formatDate = () => {};
34
+ ```
35
+
36
+ ```ts good filename=src/utils/helpers.ts
37
+ export const helpers = () => {};
38
+ ```
39
+
40
+ The comparison ignores case and separators:
41
+
42
+ ```tsx good filename=src/components/TaskCard.tsx relocation
43
+ export default function TaskCard() {}
44
+ ```
45
+
46
+ ```ts good filename=src/hooks/use-thing.ts relocation
47
+ // kebab-case file, camelCase export
48
+ export const useThing = () => {};
29
49
  ```
30
50
 
31
51
  `index` files and files matching an `ignore` glob are always skipped.
@@ -15,7 +15,7 @@ The rule only activates for an `index.ts` that sits next to a `<Folder>.tsx` of
15
15
  name on disk (`Card/Card.tsx` beside `Card/index.ts`). For those barrels, it reports when the file
16
16
  never re-exports the sibling's default export.
17
17
 
18
- ```ts
18
+ ```ts prose reason="the rule only runs when a sibling Card.tsx exists on disk"
19
19
  // Card/index.ts
20
20
 
21
21
  export { default as Card } from './Card'; // ✓
@@ -14,10 +14,13 @@ reach before it must become an alias.
14
14
  Any relative specifier whose leading `..` run exceeds `max` is reported, on every source-carrying
15
15
  construct: `import`, `import()`, `export … from`, and `export * from`.
16
16
 
17
- ```ts
17
+ ```ts bad
18
18
  // max: 3 (default)
19
- import a from '../../../shared'; // exactly at the limit
20
- import b from '../../../../shared/log'; // ✗ climbs 4 levels
19
+ import b from '../../../../shared/log'; // climbs 4 levels
20
+ ```
21
+
22
+ ```ts good
23
+ import a from '../../../shared'; // exactly at the limit
21
24
  ```
22
25
 
23
26
  ## Autofix
@@ -31,9 +34,13 @@ When `alias` maps a directory-anchor segment to an alias prefix, a too-deep impo
31
34
  }]
32
35
  ```
33
36
 
34
- ```ts
35
- // from src/a/b/c/d/deep.ts
36
- import x from '../../../../shared/log'; // → import x from '@/shared/log';
37
+ ```ts bad filename=src/a/b/c/d/deep.ts options={"alias":{"src":"@"}}
38
+ // autofixes to: import x from '@/shared/log';
39
+ import x from '../../../../shared/log';
40
+ ```
41
+
42
+ ```ts good filename=src/a/b/c/d/deep.ts options={"alias":{"src":"@"}}
43
+ import x from '@/shared/log';
37
44
  ```
38
45
 
39
46
  With no matching alias anchor on the resolved path, the violation is **reported without a fix** —
@@ -24,13 +24,14 @@ Both the alias form (`<alias>/<feature>/…`) and relative paths that climb into
24
24
  to forbid them). Imports of the current feature, of a shared feature, or of non-feature modules are
25
25
  always fine.
26
26
 
27
- ```tsx
28
- // in components/board/Board/Board.tsx
27
+ ```tsx good filename=src/components/board/Board/Board.tsx options={"featureRoot":"components","alias":"@/components","sharedFeatures":["ui"]}
28
+ import { Button } from '@/components/ui/Button'; // shared feature
29
+ import { cn } from '@/lib/utils'; // non-feature module
30
+ import { TaskCard } from '../TaskCard/TaskCard'; // same feature
31
+ ```
29
32
 
30
- import { Button } from '@/components/ui/Button'; // ✓ shared feature
31
- import { cn } from '@/lib/utils'; // ✓ non-feature module
32
- import { TaskCard } from '../TaskCard/TaskCard'; // ✓ same feature
33
- import { ProjectCard } from '@/components/projects/…'; // ✗ cross-feature
33
+ ```tsx bad filename=src/components/board/Board/Board.tsx options={"featureRoot":"components","alias":"@/components","sharedFeatures":["ui"]}
34
+ import { ProjectCard } from '@/components/projects/ProjectCard'; // cross-feature
34
35
  ```
35
36
 
36
37
  ## Options
@@ -30,16 +30,16 @@ The rule classifies by **AST shape**, never by filename or suffix:
30
30
 
31
31
  ## Examples
32
32
 
33
- ```ts
34
- // a type and a runtime constant
33
+ ```ts bad
34
+ // a type and a runtime constant
35
35
  export interface User {
36
36
  id: string;
37
37
  }
38
38
  export const DEFAULT_USER: User = { id: 'anonymous' };
39
39
  ```
40
40
 
41
- ```ts
42
- // a component and a hook
41
+ ```tsx bad
42
+ // a component and a hook
43
43
  export function UserCard() {
44
44
  return <div />;
45
45
  }
@@ -48,8 +48,8 @@ export function useUserCard() {
48
48
  }
49
49
  ```
50
50
 
51
- ```ts
52
- // one concern: types only
51
+ ```ts good
52
+ // one concern: types only
53
53
  export interface User {
54
54
  id: string;
55
55
  }
@@ -62,8 +62,8 @@ Only the **exported** surface defines a module (`ignorePrivateDeclarations`, def
62
62
  non-exported render helper beside a component, or a filter constant inside a hook file, serves that
63
63
  surface and is not a second concern:
64
64
 
65
- ```tsx
66
- // the helper is private
65
+ ```tsx good
66
+ // the helper is private
67
67
  function renderBadge(count: number) {
68
68
  return <span className="badge">{count}</span>;
69
69
  }
@@ -72,8 +72,8 @@ export function Inbox({ unread }: { unread: number }) {
72
72
  }
73
73
  ```
74
74
 
75
- ```ts
76
- // the filter object is private
75
+ ```ts good
76
+ // the filter object is private
77
77
  const ACTIVE_FILTER = { status: 'active', archived: false } as const;
78
78
  export function useActiveProjects() {
79
79
  return useQuery({ queryKey: ['projects', ACTIVE_FILTER] });
@@ -85,7 +85,7 @@ is surface all the same.
85
85
 
86
86
  ## Options
87
87
 
88
- ```ts
88
+ ```ts prose reason="the options type, not a lint example"
89
89
  type SemanticCategory =
90
90
  | 'type' | 'constant' | 'function' | 'class'
91
91
  | 'react-component' | 'hook' | 'schema' | 'enum';
@@ -134,16 +134,20 @@ export default [
134
134
  ];
135
135
  ```
136
136
 
137
- ```ts
138
- // billing.constants.ts with the config above
137
+ ```ts good filename=apps/api/src/billing/billing.constants.ts options={"allow":[["constant","type","enum"]]}
138
+ // billing.constants.ts with the config above
139
139
  export const BILLING_QUEUE = Symbol('BILLING_QUEUE');
140
140
  export const INVOICE_STATUSES = ['draft', 'sent', 'paid'] as const;
141
141
  export type InvoiceStatus = (typeof INVOICE_STATUSES)[number];
142
142
  export enum BillingEvent {
143
143
  Paid = 'billing.paid',
144
144
  }
145
+ ```
145
146
 
146
- // still reported: a function is outside the allowed group
147
+ ```ts bad filename=apps/api/src/billing/billing.constants.ts options={"allow":[["constant","type","enum"]]}
148
+ // still reported: a function is outside the allowed group
149
+ export const INVOICE_STATUSES = ['draft', 'sent', 'paid'] as const;
150
+ export type InvoiceStatus = (typeof INVOICE_STATUSES)[number];
147
151
  export function isPaid(status: InvoiceStatus) {
148
152
  return status === 'paid';
149
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noctcore/eslint-plugin-architecture",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Framework-agnostic folder-per-component and feature-boundary architecture ESLint rules.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,7 +45,7 @@
45
45
  "test": "vitest run"
46
46
  },
47
47
  "dependencies": {
48
- "@noctcore/eslint-utils": "^0.1.0",
48
+ "@noctcore/eslint-utils": "^0.1.1",
49
49
  "@typescript-eslint/utils": "^8.61.1"
50
50
  },
51
51
  "peerDependencies": {