pasika 0.1.1 → 0.1.6
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 +31 -25
- package/claude/hooks/.vulyk +3 -0
- package/claude/hooks/AGENTS.md +3 -0
- package/claude/hooks/claude-hooks.md +30 -0
- package/claude/{.claude/hooks → hooks}/notification.sh +0 -0
- package/claude/{.claude/hooks → hooks}/protect-files.sh +0 -0
- package/claude/scripts/render-settings.ts +25 -9
- package/dist/claude/scripts/render-settings.js +22 -9
- package/dist/eslint/pasika/index.d.ts +2 -0
- package/dist/eslint/pasika/index.js +17 -0
- package/dist/eslint/pasika/rules/filename-case.d.ts +2 -0
- package/dist/eslint/pasika/rules/filename-case.js +90 -0
- package/dist/eslint/pasika/rules/organization-imports.d.ts +2 -0
- package/dist/eslint/pasika/rules/organization-imports.js +124 -0
- package/docs/agent-conventions.md +27 -0
- package/docs/code-organization-guide/code-organization-guide.md +69 -0
- package/docs/code-organization-guide/references/application-architecture-reference.md +149 -0
- package/docs/code-organization-guide/rules/component-placement-rule.md +119 -0
- package/docs/code-organization-guide/rules/configuration-rule.md +42 -0
- package/docs/code-organization-guide/rules/constants-rule.md +74 -0
- package/docs/code-organization-guide/rules/exports-and-imports-rule.md +84 -0
- package/docs/code-organization-guide/rules/folder-nesting-rule.md +82 -0
- package/docs/code-organization-guide/rules/hook-extraction-rule.md +141 -0
- package/docs/code-organization-guide/rules/interactive-component-rule.md +97 -0
- package/docs/code-organization-guide/rules/jsx-hygiene-rule.md +67 -0
- package/docs/code-organization-guide/rules/locales-rule.md +53 -0
- package/docs/code-organization-guide/rules/nameable-visual-concept-rule.md +66 -0
- package/docs/code-organization-guide/rules/no-mixed-concerns-rule.md +63 -0
- package/docs/code-organization-guide/rules/repeated-structure-rule.md +93 -0
- package/docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md +112 -0
- package/docs/code-organization-guide/rules/sole-state-owner-rule.md +101 -0
- package/docs/code-organization-guide/rules/types-and-schemas-rule.md +139 -0
- package/docs/code-organization-guide/rules/utilities-rule.md +86 -0
- package/docs/documentation-guide/_templates/grouped-reference.md +11 -0
- package/docs/documentation-guide/_templates/guide.md +19 -0
- package/docs/documentation-guide/_templates/rule.md +21 -0
- package/docs/documentation-guide/_templates/single-lookup-reference.md +5 -0
- package/docs/documentation-guide/documentation-guide.md +13 -0
- package/docs/documentation-guide/references/documentation-types-reference.md +9 -0
- package/docs/documentation-guide/rules/guide-creation-rule.md +113 -0
- package/docs/documentation-guide/rules/reference-creation-rule.md +132 -0
- package/docs/documentation-guide/rules/rule-creation-rule.md +81 -0
- package/docs/documentation-guide/rules/template-usage-rule.md +49 -0
- package/docs/shadcn-theme.md +121 -0
- package/docs/styling-guide/rules/arbitrary-value-rule.md +31 -0
- package/docs/styling-guide/rules/class-composition-rule.md +52 -0
- package/docs/styling-guide/rules/component-ui-state-rule.md +53 -0
- package/docs/styling-guide/rules/component-variant-rule.md +125 -0
- package/docs/styling-guide/rules/global-stylesheet-rule.md +67 -0
- package/docs/styling-guide/rules/theme-and-utility-definition-rule.md +86 -0
- package/docs/styling-guide/styling-guide.md +14 -0
- package/package.json +22 -12
- package/AGENTS.md +0 -15
- package/docs/common/merge-behavior.md +0 -17
- package/docs/common/overview.md +0 -20
- /package/{CLAUDE.md → claude/hooks/CLAUDE.md} +0 -0
- /package/claude/{.claude/hooks → hooks}/status-line/index.js +0 -0
- /package/claude/{.claude/settings.base.json → settings.base.json} +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Application Architecture Reference
|
|
2
|
+
|
|
3
|
+
Use this reference to look up the canonical shape of the repository's `src/` tree and its Layer Model.
|
|
4
|
+
|
|
5
|
+
## Layer Model
|
|
6
|
+
|
|
7
|
+
This section describes the repository's five layers and their dependency direction. Use it to decide where a component or support file belongs and which layers it can import from.
|
|
8
|
+
|
|
9
|
+
| Layer | Path | Permitted contents | Imports from |
|
|
10
|
+
| -------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
|
11
|
+
| `app` | `src/app/` | [Next.js App Router routing files](https://nextjs.org/docs/app/getting-started/project-structure#routing-files), [metadata assets](https://nextjs.org/docs/app/getting-started/project-structure#metadata-file-conventions), and [styles required by routing files](https://nextjs.org/docs/app/getting-started/css) | compositions, features, shared, root |
|
|
12
|
+
| `compositions` | `src/compositions/` | Components and support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`) | compositions, features, shared, root |
|
|
13
|
+
| `features` | `src/features/<feature>/` | Components and support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`). `src/features/` itself holds feature folders only — never components or support folders | same feature, shared, root |
|
|
14
|
+
| `shared` | `src/shared/` | Components and support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`) | shared, root |
|
|
15
|
+
| `root` | `src/` | App-wide support folders: `hooks/`, `types/`, `schemas/`, `constants/`, `utils/`, `config/`, and `locales/` | root |
|
|
16
|
+
|
|
17
|
+
## Closest Common Folder (CCF)
|
|
18
|
+
|
|
19
|
+
The CCF determines the placement of components, hooks, types, schemas, constants, and utilities. It is the closest folder shared by the files under `src/` that use the item.
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
src/features/payments/payment-card.tsx
|
|
23
|
+
src/features/payments/payment-history.tsx
|
|
24
|
+
└─ both import StatusBadge
|
|
25
|
+
|
|
26
|
+
CCF: src/features/payments/
|
|
27
|
+
Location: src/features/payments/status-badge.tsx
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For components, imports from `src/app/` and configuration modules do not affect the CCF. A component used by `src/app/` and a feature therefore stays with the feature.
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
src/app/products/page.tsx ─┐
|
|
34
|
+
src/features/products/product-card.tsx ─┴─ imports ProductPrice
|
|
35
|
+
|
|
36
|
+
CCF and location: src/features/products/product-price.tsx
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
A component with no consumers outside `src/app/` or configuration modules stays in the feature it represents or supports. When no existing feature applies, it creates a feature folder.
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
src/app/search/page.tsx → SearchForm → src/features/search/search-form.tsx
|
|
43
|
+
|
|
44
|
+
src/app/account/page.tsx ─┐
|
|
45
|
+
src/app/orders/page.tsx ─┴→ AccountNav → src/features/account/account-nav.tsx
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
When a component has consumers both inside and outside `src/compositions/`, only the consumers outside `src/compositions/` count. When all consumers are in `src/compositions/`, that folder counts.
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
src/compositions/checkout.tsx ─┐
|
|
52
|
+
src/features/payments/payment-summary.tsx ─┴→ Total → src/features/payments/total.tsx
|
|
53
|
+
|
|
54
|
+
src/compositions/checkout.tsx ─┐
|
|
55
|
+
src/compositions/receipt.tsx ─┴→ Total → src/compositions/total.tsx
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A component stays at a nested component folder, feature folder, `src/compositions/`, or `src/shared/`. A CCF of `src/features/` places it in `src/shared/`.
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
src/features/payments/payment-card.tsx ─┐
|
|
62
|
+
src/features/orders/order-card.tsx ─┴→ StatusBadge → src/shared/status-badge.tsx
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For a support file imported by a file under `src/app/`, use the matching root support folder, such as `src/hooks/` or `src/utils/`. Otherwise, the CCF passes through a support folder to its parent folder, and the file lives in the matching support folder directly below that parent.
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
src/app/products/page.tsx → useSearch → src/hooks/use-search.ts
|
|
69
|
+
src/features/payments/hooks/use-payment.ts → formatAmount → src/features/payments/utils/format-amount.ts
|
|
70
|
+
src/features/payments/payment-card.tsx ─┐
|
|
71
|
+
src/features/orders/order-card.tsx ─┴→ formatDate → src/utils/format-date.ts
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Application Structure
|
|
75
|
+
|
|
76
|
+
This tree shows the canonical directory layout and naming conventions for files and folders inside `src/`. It is included so readers can verify their structure matches the project standard at a glance.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
src/
|
|
80
|
+
├── app/ # Next.js framework-convention files, assets, and route styles
|
|
81
|
+
├── compositions/ # Components stay flat siblings by default
|
|
82
|
+
│ ├── <ComponentA>.tsx # Smart component (PascalCase-named)
|
|
83
|
+
│ ├── <component-b>.tsx # Dumb component (kebab-case-named)
|
|
84
|
+
│ ├── <component-part>.tsx # Dumb component — shared by A and B, so it stays flat too
|
|
85
|
+
│ ├── <NestedComponent>/ # Smart (PascalCase-named) — or kebab-case-named for dumb; nested only once it gains exclusive children
|
|
86
|
+
│ │ ├── index.ts # Named-re-exports only the nested parent
|
|
87
|
+
│ │ ├── <NestedComponent>.tsx # Smart (PascalCase-named) — or kebab-case-named for dumb
|
|
88
|
+
│ │ ├── <nested-part>.tsx # Exclusive child — not in index.ts
|
|
89
|
+
│ │ ├── hooks/ # Nested-scoped support folders
|
|
90
|
+
│ │ ├── types/
|
|
91
|
+
│ │ ├── schemas/
|
|
92
|
+
│ │ ├── constants/
|
|
93
|
+
│ │ └── utils/
|
|
94
|
+
│ ├── hooks/ # Composition-scoped custom hooks
|
|
95
|
+
│ ├── types/ # Composition-scoped TypeScript types
|
|
96
|
+
│ ├── schemas/ # Composition-scoped validation schemas
|
|
97
|
+
│ ├── constants/ # Composition-scoped constants
|
|
98
|
+
│ └── utils/ # Composition-scoped pure functions
|
|
99
|
+
├── features/ # Feature folders only — no components, no support folders here
|
|
100
|
+
│ └── <feature>/ # One folder per feature — a feature can hold several components
|
|
101
|
+
│ ├── <ComponentA>.tsx # Smart component (PascalCase-named)
|
|
102
|
+
│ ├── <component-b>.tsx # Dumb component (kebab-case-named)
|
|
103
|
+
│ ├── <component-part>.tsx # Dumb component — shared by A and B, so it stays flat too
|
|
104
|
+
│ ├── <NestedComponent>/ # Smart (PascalCase-named) — or kebab-case-named for dumb; nested only once it gains exclusive children
|
|
105
|
+
│ │ ├── index.ts # Named-re-exports only the nested parent
|
|
106
|
+
│ │ ├── <NestedComponent>.tsx # Smart (PascalCase-named) — or kebab-case-named for dumb
|
|
107
|
+
│ │ ├── <nested-part>.tsx # Exclusive child — not in index.ts
|
|
108
|
+
│ │ ├── hooks/ # Nested-scoped support folders
|
|
109
|
+
│ │ ├── types/
|
|
110
|
+
│ │ ├── schemas/
|
|
111
|
+
│ │ ├── constants/
|
|
112
|
+
│ │ └── utils/
|
|
113
|
+
│ ├── hooks/ # Feature-scoped custom hooks
|
|
114
|
+
│ ├── types/ # Feature-scoped TypeScript types
|
|
115
|
+
│ ├── schemas/ # Feature-scoped validation schemas
|
|
116
|
+
│ ├── constants/ # Feature-scoped constants
|
|
117
|
+
│ └── utils/ # Feature-scoped pure functions
|
|
118
|
+
├── shared/ # Components stay flat siblings by default
|
|
119
|
+
│ ├── <ComponentA>.tsx # Smart component (PascalCase-named)
|
|
120
|
+
│ ├── <component-b>.tsx # Dumb component (kebab-case-named)
|
|
121
|
+
│ ├── <component-part>.tsx # Dumb component — shared by A and B, so it stays flat too
|
|
122
|
+
│ ├── <NestedComponent>/ # Smart (PascalCase-named) — or kebab-case-named for dumb; nested only once it gains exclusive children
|
|
123
|
+
│ │ ├── index.ts # Named-re-exports only the nested parent
|
|
124
|
+
│ │ ├── <NestedComponent>.tsx # Smart (PascalCase-named) — or kebab-case-named for dumb
|
|
125
|
+
│ │ ├── <nested-part>.tsx # Exclusive child — not in index.ts
|
|
126
|
+
│ │ ├── hooks/ # Nested-scoped support folders
|
|
127
|
+
│ │ ├── types/
|
|
128
|
+
│ │ ├── schemas/
|
|
129
|
+
│ │ ├── constants/
|
|
130
|
+
│ │ └── utils/
|
|
131
|
+
│ ├── hooks/ # Shared-scoped custom hooks
|
|
132
|
+
│ ├── types/ # Shared-scoped TypeScript types
|
|
133
|
+
│ ├── schemas/ # Shared-scoped validation schemas
|
|
134
|
+
│ ├── constants/ # Shared-scoped constants
|
|
135
|
+
│ └── utils/ # Shared-scoped pure functions
|
|
136
|
+
├── hooks/ # App-wide custom hooks
|
|
137
|
+
├── types/ # App-wide TypeScript types
|
|
138
|
+
├── schemas/ # App-wide validation schemas
|
|
139
|
+
├── constants/ # App-wide constants
|
|
140
|
+
├── utils/ # App-wide pure functions
|
|
141
|
+
├── config/ # Configuration modules that centralize application behavior
|
|
142
|
+
│ └── <config-name>/ # One folder per configuration module
|
|
143
|
+
│ ├── index.ts # Configuration-module entry point
|
|
144
|
+
│ ├── types/ # Config-only TypeScript types
|
|
145
|
+
│ ├── schemas/ # Config-only validation schemas
|
|
146
|
+
│ └── utils/ # Config-only pure functions
|
|
147
|
+
└── locales/ # App-wide locale strings
|
|
148
|
+
└── index.ts # Single locale-registration file
|
|
149
|
+
```
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Component Placement Rule
|
|
2
|
+
|
|
3
|
+
Without clear placement, it is hard to tell where a component belongs and reuse can create tangled dependencies. This rule gives each component a specific place in the application structure.
|
|
4
|
+
|
|
5
|
+
- A component that imports from two or more feature folders MUST live in `src/compositions/` and is, by definition, a composition.
|
|
6
|
+
- A component with no consumers outside `src/app/` or configuration modules, and that does not import from two or more feature folders, MUST live in the feature folder it represents or supports. If no existing feature applies, it MUST introduce a new feature folder.
|
|
7
|
+
- A component with at least one consumer outside `src/app/` and configuration modules MUST live in its CCF, calculated without imports from `src/app/` or configuration modules.
|
|
8
|
+
- When calculating a component's CCF, consumers under `src/compositions/` MUST count only when no consumer is outside `src/compositions/`.
|
|
9
|
+
- A component whose CCF is `src/features/` MUST live in `src/shared/`.
|
|
10
|
+
- `src/app/` MUST contain [Next.js App Router framework-convention files and assets](https://nextjs.org/docs/app/getting-started/project-structure#routing-files), plus styles required by routing files, but MUST NOT contain ordinary components or support folders.
|
|
11
|
+
## Incorrect — Cross-Feature Component Duplicated
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
src/features/
|
|
15
|
+
├── donation/
|
|
16
|
+
│ ├── DonationCard.tsx
|
|
17
|
+
│ └── status-badge.tsx
|
|
18
|
+
└── stream/
|
|
19
|
+
├── StreamCard.tsx
|
|
20
|
+
└── status-badge.tsx
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Why: two feature folders contain duplicate copies because neither feature may import from the other.
|
|
24
|
+
|
|
25
|
+
## Correct — Cross-Feature Component in `src/shared/`
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
src/
|
|
29
|
+
├── features/
|
|
30
|
+
│ ├── donation/
|
|
31
|
+
│ │ └── DonationCard.tsx
|
|
32
|
+
│ └── stream/
|
|
33
|
+
│ └── StreamCard.tsx
|
|
34
|
+
└── shared/
|
|
35
|
+
└── status-badge.tsx
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Why: the component's CCF is `src/features/`, so it lives in `src/shared/`.
|
|
39
|
+
|
|
40
|
+
## Incorrect — Multi-Feature Composition Inside One Feature
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// src/features/billing/billing-aggregate-view.tsx
|
|
44
|
+
import { BillingPanel } from "./BillingPanel";
|
|
45
|
+
import { HomeBanner } from "@/features/home/HomeBanner";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Why: the component imports from two feature folders, so it cannot live in either feature folder.
|
|
49
|
+
|
|
50
|
+
## Correct — Multi-Feature Component in `src/compositions/`
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// src/compositions/billing-aggregate-view.tsx
|
|
54
|
+
import { BillingPanel } from "@/features/billing/BillingPanel";
|
|
55
|
+
import { HomeBanner } from "@/features/home/HomeBanner";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Why: the component imports from two feature folders, so it lives in `src/compositions/`.
|
|
59
|
+
|
|
60
|
+
## Incorrect — Ordinary Component Under `src/app/`
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
src/app/contact/
|
|
64
|
+
├── _components/
|
|
65
|
+
│ └── contact-page-content.tsx
|
|
66
|
+
└── page.tsx
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Why: the route folder contains ordinary component structure even though `src/app/` is reserved for framework-convention files, assets, and styles.
|
|
70
|
+
|
|
71
|
+
## Correct — `src/app/` Imports a Page Composition
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
src/
|
|
75
|
+
├── app/contact/
|
|
76
|
+
│ └── page.tsx
|
|
77
|
+
├── compositions/
|
|
78
|
+
│ └── contact-page-content.tsx
|
|
79
|
+
└── features/
|
|
80
|
+
├── contact/
|
|
81
|
+
│ └── contact-information.tsx
|
|
82
|
+
└── location/
|
|
83
|
+
└── office-location.tsx
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// src/compositions/contact-page-content.tsx
|
|
88
|
+
import { ContactInformation } from "@/features/contact/contact-information";
|
|
89
|
+
import { OfficeLocation } from "@/features/location/office-location";
|
|
90
|
+
import { locales } from "@/locales";
|
|
91
|
+
|
|
92
|
+
export function ContactPageContent(): React.JSX.Element {
|
|
93
|
+
return (
|
|
94
|
+
<main>
|
|
95
|
+
<header>
|
|
96
|
+
<h1>{locales.contactPageTitle}</h1>
|
|
97
|
+
<p>{locales.contactPageDescription}</p>
|
|
98
|
+
</header>
|
|
99
|
+
<section>
|
|
100
|
+
<h2>{locales.contactInformationHeading}</h2>
|
|
101
|
+
<ContactInformation />
|
|
102
|
+
</section>
|
|
103
|
+
<section>
|
|
104
|
+
<h2>{locales.officeLocationHeading}</h2>
|
|
105
|
+
<OfficeLocation />
|
|
106
|
+
</section>
|
|
107
|
+
</main>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/app/contact/page.tsx
|
|
112
|
+
import { ContactPageContent } from "@/compositions/contact-page-content";
|
|
113
|
+
|
|
114
|
+
export default function Page(): React.JSX.Element {
|
|
115
|
+
return <ContactPageContent />;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Why: `src/app/` contains the routing file while the composition provides the contact page layout and combines components from two feature folders.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Configuration Rule
|
|
2
|
+
|
|
3
|
+
Configuration modules centralize values that control application behavior. This rule keeps each module and the files that only support it together in `src/config/`.
|
|
4
|
+
|
|
5
|
+
- A configuration module is an app-wide module that selects or parameterizes application behavior.
|
|
6
|
+
- All configuration modules MUST live in `src/config/`.
|
|
7
|
+
- A configuration module MUST be one `src/config/<config-name>/` folder with `index.ts` as its entry point.
|
|
8
|
+
- A type, schema, or utility used only to implement one configuration module MUST be extracted even with one consumer.
|
|
9
|
+
- An extracted configuration type, schema, or utility MUST live in its matching dedicated folder under `src/config/<config-name>/`.
|
|
10
|
+
- An extracted configuration type, schema, or utility MUST move to its matching root support folder when a consumer outside its configuration module imports it.
|
|
11
|
+
|
|
12
|
+
## Incorrect — Supporting Schema Outside Its Config Folder
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
src/config/
|
|
16
|
+
├── home-feed/
|
|
17
|
+
│ └── index.ts
|
|
18
|
+
└── home-feed-schema.ts
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Why: the schema sits outside the `home-feed/` configuration folder.
|
|
22
|
+
|
|
23
|
+
## Correct — Configuration Module with Its Supporting Schema
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
src/config/
|
|
27
|
+
└── home-feed/
|
|
28
|
+
├── index.ts
|
|
29
|
+
└── schemas/
|
|
30
|
+
└── index.ts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// src/config/home-feed/index.ts
|
|
35
|
+
import { homeFeedConfigSchema } from "./schemas";
|
|
36
|
+
|
|
37
|
+
export const homeFeedConfig = homeFeedConfigSchema.parse({
|
|
38
|
+
freshVideoMaxAgeDays: 7,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Why: the configuration module and its supporting schema are grouped in the same configuration folder.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Constants Rule
|
|
2
|
+
|
|
3
|
+
Duplicated constants are hard to keep in sync, while extracting every single-use value creates unnecessary files. This rule keeps reused constants in one file and leaves single-use values close to their consumer.
|
|
4
|
+
|
|
5
|
+
- A value MUST remain in its declaring component or file until another file imports it independently; it MUST then be extracted as a constant.
|
|
6
|
+
- Extracted constants MUST live in a `constants/` folder at the closest common folder (CCF) of their consumers.
|
|
7
|
+
- Consumers MUST import an extracted constant through the `index.ts` in that constant's `constants/` folder.
|
|
8
|
+
- A `constants/` folder MUST either define its constants directly in `index.ts` or group related constants in kebab-case files that `index.ts` named-re-exports.
|
|
9
|
+
- When a constant's CCF is `src/features/`, it MUST move to `src/constants/`.
|
|
10
|
+
- A constant MAY live in `src/config/<module>/` instead of a `constants/` folder when a developer determines that it configures application behavior and is best understood alongside the configuration that parameterizes it, even when consumers exist outside the config module.
|
|
11
|
+
|
|
12
|
+
## Incorrect — Constant Imported Without `constants/index.ts`
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// src/features/billing/constants/max-retries.ts
|
|
16
|
+
export const maxRetries = 3;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// src/features/billing/hooks/use-retry-payment.ts
|
|
21
|
+
import { maxRetries } from "../constants/max-retries";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Why: the consumer bypasses the feature's `constants/index.ts`.
|
|
25
|
+
|
|
26
|
+
## Correct — Grouped Constants Re-Exported from `constants/index.ts`
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// src/features/billing/constants/retry.ts
|
|
30
|
+
export const maxRetries = 3;
|
|
31
|
+
export const retryDelayMs = 1_000;
|
|
32
|
+
|
|
33
|
+
// src/features/billing/constants/index.ts
|
|
34
|
+
export { maxRetries, retryDelayMs } from "./retry";
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// src/features/billing/hooks/use-retry-payment.ts
|
|
39
|
+
import { maxRetries } from "../constants";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Why: the constants are available through the feature's `constants/index.ts`.
|
|
43
|
+
|
|
44
|
+
## Incorrect — Feature and Composition Constant Kept in a Feature
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// src/features/billing/constants/index.ts
|
|
48
|
+
export const paymentRetryDelayMs = 1_000;
|
|
49
|
+
|
|
50
|
+
// src/features/billing/hooks/use-retry-payment.ts
|
|
51
|
+
import { paymentRetryDelayMs } from "../constants";
|
|
52
|
+
|
|
53
|
+
// src/compositions/BillingDashboard.tsx
|
|
54
|
+
import { paymentRetryDelayMs } from "@/features/billing/constants";
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Why: the feature and composition have `src/` as their CCF, but the constant remains in the billing feature.
|
|
58
|
+
|
|
59
|
+
## Correct — Feature and Composition Constant in `src/constants/`
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// src/constants/index.ts
|
|
63
|
+
export const paymentRetryDelayMs = 1_000;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// src/features/billing/hooks/use-retry-payment.ts
|
|
68
|
+
import { paymentRetryDelayMs } from "@/constants";
|
|
69
|
+
|
|
70
|
+
// src/compositions/BillingDashboard.tsx
|
|
71
|
+
import { paymentRetryDelayMs } from "@/constants";
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Why: the feature and composition have `src/` as their CCF, so the constant lives in `src/constants/`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Exports and Imports Rule
|
|
2
|
+
|
|
3
|
+
Without consistent exports, import paths, and layer boundaries, it is harder to tell what a file contains and which files may depend on it. This rule gives each file a predictable export style and keeps imports consistent with the application structure.
|
|
4
|
+
|
|
5
|
+
- A file that exports values MUST use named exports unless a framework or third-party package requires a different export style for that file.
|
|
6
|
+
- Imports MUST use relative paths for the same folder, a direct subfolder, or one folder up.
|
|
7
|
+
- Imports MUST use the `@/*` alias for anything beyond one folder up.
|
|
8
|
+
- A file under `src/compositions/` MUST NOT import from `src/app/`.
|
|
9
|
+
- A file in a feature folder MUST NOT import from another feature folder, `src/compositions/`, or `src/app/`.
|
|
10
|
+
- A file under `src/shared/` MUST NOT import from `src/app/`, `src/compositions/`, or a feature folder.
|
|
11
|
+
- A file in the `root` layer MUST NOT import from `src/app/`, `src/compositions/`, a feature folder, or `src/shared/`.
|
|
12
|
+
- A configuration module MUST import only from root support folders and its own files.
|
|
13
|
+
## Incorrect — Single-Export Utility Uses a Default Export
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// src/utils/format-duration.ts
|
|
17
|
+
export default function formatDuration(seconds: number): string {
|
|
18
|
+
const minutes = Math.floor(seconds / 60);
|
|
19
|
+
const remainder = seconds % 60;
|
|
20
|
+
return `${minutes}:${String(remainder).padStart(2, "0")}`;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Why: files use named exports even when they expose one item.
|
|
25
|
+
|
|
26
|
+
## Correct — Single-Export Utility Uses a Named Export
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// src/utils/format-duration.ts
|
|
30
|
+
export function formatDuration(seconds: number): string {
|
|
31
|
+
const minutes = Math.floor(seconds / 60);
|
|
32
|
+
const remainder = seconds % 60;
|
|
33
|
+
return `${minutes}:${String(remainder).padStart(2, "0")}`;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Why: the utility uses the same named-export form as every other file that does not have a package-required export contract.
|
|
38
|
+
|
|
39
|
+
## Incorrect — Deep Relative Path
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// src/features/stream/StreamBoard/schedule.ts
|
|
43
|
+
import { debounce } from "../../../utils/debounce";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Why: reaching more than one folder up hides the cross-scope dependency in relative path traversal.
|
|
47
|
+
|
|
48
|
+
## Correct — Alias Across Scopes and Relative Paths Nearby
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// src/features/stream/StreamBoard/schedule.ts
|
|
52
|
+
import { debounce } from "@/utils/debounce";
|
|
53
|
+
import { formatSlot } from "./utils/format-slot";
|
|
54
|
+
import { type StreamSlot } from "./types";
|
|
55
|
+
import { buildSchedule } from "../schedule-builder";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Why: the alias identifies the distant dependency while nearby files keep short relative paths.
|
|
59
|
+
|
|
60
|
+
## Incorrect — Next.js Page Uses a Named Export
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// src/app/contact/page.tsx
|
|
64
|
+
import { locales } from "@/locales";
|
|
65
|
+
|
|
66
|
+
export function Page(): React.JSX.Element {
|
|
67
|
+
return <main>{locales.contactUs}</main>;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Why: Next.js requires a `page.tsx` file to default-export its page component.
|
|
72
|
+
|
|
73
|
+
## Correct — Next.js Page Uses a Default Export
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// src/app/contact/page.tsx
|
|
77
|
+
import { locales } from "@/locales";
|
|
78
|
+
|
|
79
|
+
export default function Page(): React.JSX.Element {
|
|
80
|
+
return <main>{locales.contactUs}</main>;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Why: the page follows the export contract Next.js requires.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Folder Nesting Rule
|
|
2
|
+
|
|
3
|
+
Without nesting, exclusive children can look reusable and their relationship to the parent is easy to miss in review. This rule groups them with their parent and keeps them out of the folder's public API.
|
|
4
|
+
|
|
5
|
+
- An exclusive child component is imported only by its parent component. A component MUST stay flat until it has one or more exclusive child components, then MUST be nested in a folder with the same name.
|
|
6
|
+
- A component MUST NOT be nested only because it has support files.
|
|
7
|
+
- A nested component's support files MUST live in its folder.
|
|
8
|
+
- The nested folder's `index.ts` MUST named-re-export the nested component and MUST NOT re-export its exclusive children.
|
|
9
|
+
|
|
10
|
+
## Incorrect — Exclusive Children Kept Flat
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
src/features/blog/
|
|
14
|
+
BlogPage.tsx # owns exclusive children
|
|
15
|
+
blog-header.tsx # exclusive child — no sibling needs it
|
|
16
|
+
blog-footer.tsx # exclusive child — no sibling needs it
|
|
17
|
+
hooks/
|
|
18
|
+
use-blog-filter.ts # used only by BlogPage
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Why: `BlogPage` owns children that no sibling uses, but all three files sit as flat siblings. Nothing in the tree shows that `blog-header.tsx` and `blog-footer.tsx` belong to `BlogPage` rather than to any other component in the folder.
|
|
22
|
+
|
|
23
|
+
## Correct — Exclusive Children Nested
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
src/features/blog/
|
|
27
|
+
BlogPage/
|
|
28
|
+
index.ts # re-exports only BlogPage.tsx
|
|
29
|
+
BlogPage.tsx
|
|
30
|
+
blog-header.tsx # not re-exported from index.ts
|
|
31
|
+
blog-footer.tsx # not re-exported from index.ts
|
|
32
|
+
hooks/
|
|
33
|
+
use-blog-filter.ts
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Why: nesting `BlogPage` into `BlogPage/` makes the parent–child relationship visible in the filesystem, lets the children stay scoped to their concrete consumer, and gives the folder a barrel that re-exports only the nested component.
|
|
37
|
+
|
|
38
|
+
## Incorrect — Support Files Cause Unnecessary Nesting
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
src/features/blog/
|
|
42
|
+
BlogPage/
|
|
43
|
+
index.ts
|
|
44
|
+
BlogPage.tsx
|
|
45
|
+
hooks/
|
|
46
|
+
use-blog-filter.ts
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Why: support files alone do not make `BlogPage` a nested component.
|
|
50
|
+
|
|
51
|
+
## Correct — Support Files Keep a Component Flat
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
src/features/blog/
|
|
55
|
+
BlogPage.tsx
|
|
56
|
+
hooks/
|
|
57
|
+
use-blog-filter.ts
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Why: without exclusive children, `BlogPage` stays flat and its support files remain at the feature scope.
|
|
61
|
+
|
|
62
|
+
## Incorrect — Exclusive Child Re-Exported
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// src/features/blog/BlogPage/index.ts
|
|
66
|
+
export { BlogPage } from "./BlogPage";
|
|
67
|
+
export { BlogHeader } from "./blog-header";
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Why: the barrel re-exports the child as well as the nested component, so outside consumers can import `blog-header.tsx` through `index.ts` and the child stops being exclusive to `BlogPage`.
|
|
71
|
+
|
|
72
|
+
## Correct — Only the Nested Component Re-Exported
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// src/features/blog/BlogPage/index.ts
|
|
76
|
+
export { BlogPage } from "./BlogPage";
|
|
77
|
+
|
|
78
|
+
// src/features/blog/BlogPage/BlogPage.tsx
|
|
79
|
+
import { BlogHeader } from "./blog-header";
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Why: the barrel exposes only the nested component, so it does not expose the child.
|