Skip to content

Commit a250473

Browse files
authored
Merge pull request #53 from bartstc/chore/eslint-rule-for-layers
chore: add eslint rule for enforcing layer dependencies
2 parents 9d922d5 + 0e7b5bd commit a250473

5 files changed

Lines changed: 99 additions & 10 deletions

File tree

docs/architecture.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,20 @@ Each feature follows feature slice architecture patterns with four layers:
5252

5353
- **components/** - UI components, presentational and decoupled from business logic (application) and router state. Data access is only through `providers/`.
5454
- **application/** - Business logic, portable state management (stores, FSMs, form validation), custom hooks. Should not depend on router state or external APIs directly (only through `providers/`).
55-
- **providers/** - Hook composition and data access gateway for the feature slice. Exposes query hooks, mutations, loaders, domain errors, and DTOs sourced from `src/lib/api/`. Library-specific code (React Query, etc.) must not leak beyond this layer.
55+
- **providers/** - Hook composition and data access gateway for the feature slice. Exposes query hooks, mutations, loaders, and domain errors sourced from `src/lib/api/`. When the DTO shape diverges from the domain model, the mapping function lives here — applied inside the query/mutation hook so consumers always receive the correct domain model type.
5656
- files are named after their primary export or reexport: `useCartProductsQuery``use-cart-products-query.ts`, `useAddToCartMutation``use-add-to-cart-mutation.ts`
57-
- **models/** - Domain type definitions, utilities, and type mapping functions.
58-
- exposes frontend models for the feature. Components, application, and pages import types from `models/`, never directly from `src/lib/api/`. When the DTO shape is identical, a simple re-export with a domain name suffices (`export type { ProductDto as Product }`). When it diverges, map to a dedicated frontend model.
57+
- **models/** - Domain type definitions only. Exposes frontend models for the feature. When the DTO shape is identical to the domain model, re-export with a domain name (`export type { ProductDto as Product }`). When it diverges, define the domain type here.
5958

60-
**Dependency rule:** `components/` and `application/` import from `models/` and `providers/`. `providers/` and `models/` have no internal feature dependencies.
59+
**Dependency rule:**
6160

62-
## API Layer
61+
| Layer | May import from |
62+
| -------------- | ------------------------------------------------ |
63+
| `components/` | `application/`, `providers/`, `models/`, `lib/*` |
64+
| `application/` | `providers/`, `models/`, `lib/*` |
65+
| `providers/` | `models/`, `lib/api/`, `lib/*` |
66+
| `models/` | `lib/api/`, `lib/*` |
67+
68+
## API Library
6369

6470
`src/lib/api/` is the global home for all HTTP logic: `queryOptions` factories, loaders, mutation hooks, query keys, domain errors, and DTOs, organised by resource. Query files expose `queryOptions` factories (no `useQuery` hooks — hook composition belongs in `providers/`). Feature `providers/` compose hooks on top of those factories and re-export them for feature slice. New API logic always goes in `src/lib/api/` first, then gets exposed through the relevant feature's `providers/`.
6571

@@ -72,8 +78,6 @@ Each feature follows feature slice architecture patterns with four layers:
7278
| DTO interface | `-dto.ts` | `cart-product-dto.ts` |
7379
| Query keys | `-query-keys.ts` | `cart-query-keys.ts` |
7480

75-
Never use `-command.ts`, `-service.ts`, or other suffixes.
76-
7781
## Key Patterns
7882

7983
| Pattern | Description |

eslint.config.mjs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,77 @@ import reactRefresh from "eslint-plugin-react-refresh";
1111
import reactYouMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";
1212

1313
// used by import/no-restricted-paths
14+
// AIDEV-NOTE: import/no-restricted-paths uses path.relative() for matching — glob wildcards
15+
// in target/from are NOT supported. All zone lists must be generated per-feature.
1416
const featureSlices = ["carts", "marketing", "products"];
17+
const allFeatureSlices = [
18+
"auth",
19+
"authv2",
20+
"carts",
21+
"demo",
22+
"marketing",
23+
"products",
24+
];
25+
1526
const featureToFeatureZones = featureSlices.map((feature) => ({
1627
target: `./src/features/${feature}`,
1728
from: "./src/features",
1829
except: [`./${feature}`, "./auth"],
1930
message: "Avoid importing from other features.",
2031
}));
2132

33+
const featureLayerZones = allFeatureSlices.flatMap((feature) => [
34+
// application/ ← components/ (forbidden)
35+
{
36+
target: `./src/features/${feature}/application`,
37+
from: `./src/features/${feature}/components`,
38+
message: "application/ must not depend on components/.",
39+
},
40+
// providers/ ← application/ or components/ (forbidden)
41+
{
42+
target: `./src/features/${feature}/providers`,
43+
from: `./src/features/${feature}/application`,
44+
message: "providers/ must not depend on application/.",
45+
},
46+
{
47+
target: `./src/features/${feature}/providers`,
48+
from: `./src/features/${feature}/components`,
49+
message: "providers/ must not depend on components/.",
50+
},
51+
// models/ ← any feature layer (forbidden)
52+
{
53+
target: `./src/features/${feature}/models`,
54+
from: `./src/features/${feature}/application`,
55+
message: "models/ must not depend on application/.",
56+
},
57+
{
58+
target: `./src/features/${feature}/models`,
59+
from: `./src/features/${feature}/components`,
60+
message: "models/ must not depend on components/.",
61+
},
62+
{
63+
target: `./src/features/${feature}/models`,
64+
from: `./src/features/${feature}/providers`,
65+
message: "models/ must not depend on providers/.",
66+
},
67+
]);
68+
69+
// Prevents lib/api/ from leaking beyond providers/ and models/.
70+
const apiLayerIsolationZones = allFeatureSlices.flatMap((feature) => [
71+
{
72+
target: `./src/features/${feature}/components`,
73+
from: "./src/lib/api",
74+
message:
75+
"src/lib/api/ must not be imported in components/. Use providers/ for data queries and mutations and models/ for types.",
76+
},
77+
{
78+
target: `./src/features/${feature}/application`,
79+
from: "./src/lib/api",
80+
message:
81+
"src/lib/api/ must not be imported in application/. Use providers/ for data queries and mutations and models/ for types.",
82+
},
83+
]);
84+
2285
export default defineConfig(
2386
{
2487
ignores: [
@@ -197,7 +260,29 @@ export default defineConfig(
197260
"import/no-restricted-paths": [
198261
"error",
199262
{
200-
zones: featureToFeatureZones,
263+
zones: [
264+
...featureToFeatureZones,
265+
...featureLayerZones,
266+
...apiLayerIsolationZones,
267+
],
268+
},
269+
],
270+
},
271+
},
272+
{
273+
files: ["./src/pages/**"],
274+
rules: {
275+
"import/no-restricted-paths": [
276+
"error",
277+
{
278+
zones: [
279+
{
280+
target: "./src/pages",
281+
from: "./src/lib/api",
282+
message:
283+
"src/lib/api/ must not be imported in pages/. Use features/*/providers/ for data and features/*/models/ for types.",
284+
},
285+
],
201286
},
202287
],
203288
},

src/features/carts/application/use-add-to-cart.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useAuthStore } from "@/features/auth/application/auth-store";
2-
import { useProductAddedDialogStore } from "@/features/carts/components/AddToCartButton/use-product-added-dialog-store";
2+
import { useProductAddedDialogStore } from "@/features/carts/application/use-product-added-dialog-store";
33
import {
44
useAddToCartMutation,
55
UnknownProductError,

src/features/carts/components/AddToCartButton/use-product-added-dialog-store.ts renamed to src/features/carts/application/use-product-added-dialog-store.ts

File renamed without changes.

src/features/carts/components/AddToCartButton/ProductAddedDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
Text,
88
} from "@chakra-ui/react";
99

10-
import { useProductAddedDialogStore } from "@/features/carts/components/AddToCartButton/use-product-added-dialog-store";
10+
import { useProductAddedDialogStore } from "@/features/carts/application/use-product-added-dialog-store";
1111
import { useTranslations } from "@/lib/i18n/use-transations";
1212
import { useNavigate } from "@/lib/router";
1313
import { routes } from "@/lib/router/routes";

0 commit comments

Comments
 (0)