@synergyerp/frontend-standards 1.0.2 → 1.1.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/CICD_STANDARDS.md +1888 -0
- package/FRONTEND_STANDARDS.md +2942 -0
- package/package.json +4 -2
|
@@ -0,0 +1,2942 @@
|
|
|
1
|
+
# Frontend Standardization & Deployment Guide
|
|
2
|
+
|
|
3
|
+
> **Purpose**: Company-wide standards for frontend project architecture, code quality, CI/CD, and code reviews.
|
|
4
|
+
> **Target Audience**: All frontend engineers, QA engineers, and DevOps teams.
|
|
5
|
+
> **Date**: June 2026
|
|
6
|
+
> **Status**: v1.0 -- Standard
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
1. [Philosophy & Guiding Principles](#1-philosophy--guiding-principles)
|
|
13
|
+
2. [Recommended Tech Stack](#2-recommended-tech-stack)
|
|
14
|
+
3. [Project Scaffolding & Directory Structure](#3-project-scaffolding--directory-structure)
|
|
15
|
+
4. [TypeScript Configuration Standards](#4-typescript-configuration-standards)
|
|
16
|
+
5. [Code Quality & Linting Standards](#5-code-quality--linting-standards)
|
|
17
|
+
6. [Formatting Standards](#6-formatting-standards)
|
|
18
|
+
7. [UI Component Standards (Lovable Integration)](#7-ui-component-standards-lovable-integration)
|
|
19
|
+
8. [Testing Standards](#8-testing-standards)
|
|
20
|
+
9. [Git Workflow & Commit Standards](#9-git-workflow--commit-standards)
|
|
21
|
+
10. [Code Review Standards](#10-code-review-standards)
|
|
22
|
+
11. [CI/CD Pipeline Standards](#11-cicd-pipeline-standards)
|
|
23
|
+
12. [Deployment Standards](#12-deployment-standards)
|
|
24
|
+
13. [Security Standards](#13-security-standards)
|
|
25
|
+
14. [Monitoring & Observability](#14-monitoring--observability)
|
|
26
|
+
15. [Documentation Standards](#15-documentation-standards)
|
|
27
|
+
16. [Appendix: Quick References](#16-appendix-quick-references)
|
|
28
|
+
17. [API Service Layer Standards](#17-api-service-layer-standards)
|
|
29
|
+
18. [React Hooks Best Practices](#18-react-hooks-best-practices)
|
|
30
|
+
19. [Enforcement: Husky, Git Hooks & Automated Validation](#19-enforcement-husky-git-hooks--automated-validation)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1. Philosophy & Guiding Principles
|
|
35
|
+
|
|
36
|
+
### 1.1 Core Tenets
|
|
37
|
+
|
|
38
|
+
1. **Consistency Over Configuration**: Every project should look, feel, and behave the same way so engineers can move between projects seamlessly.
|
|
39
|
+
2. **Automation Over Manual Processes**: Linting, formatting, testing, and deployments must be automated. Human judgement is reserved for architecture and code reviews.
|
|
40
|
+
3. **Security By Default**: Security checks, input validation, and dependency auditing are non-negotiable gates in every pipeline.
|
|
41
|
+
4. **Documentation As Code**: Key decisions live as close to the code as possible (READMEs, ADRs, inline JSDoc).
|
|
42
|
+
5. **Accessibility First**: Every UI component must be accessible (WCAG 2.1 AA minimum).
|
|
43
|
+
|
|
44
|
+
### 1.2 Separation Of Concerns
|
|
45
|
+
|
|
46
|
+
This document is split into two distinct parts:
|
|
47
|
+
|
|
48
|
+
| Part | Focus | Audience |
|
|
49
|
+
|------|-------|----------|
|
|
50
|
+
| **Frontend Standards** (Sections 2-9) | Code structure, linting, UI components, testing, git | Developers, Tech Leads |
|
|
51
|
+
| **CI/CD & Deployment** (Sections 10-14) | Pipelines, code review, deployment, monitoring, security | DevOps, QA, Tech Leads |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 2. Recommended Tech Stack
|
|
56
|
+
|
|
57
|
+
### 2.1 Framework & Runtime
|
|
58
|
+
|
|
59
|
+
| Layer | Recommended | Alternative | Notes |
|
|
60
|
+
|-------|------------|-------------|-------|
|
|
61
|
+
| **Framework** | React 19 + Vite | Next.js(if SSR/SEO needed) | Choose based on project needs. Lovable generates React components natively. |
|
|
62
|
+
| **Language** | TypeScript 5.5+ (strict mode) | -- | Non-negotiable. No plain JavaScript in new projects. |
|
|
63
|
+
| **Build Tool** | Vite 6 | Turbopack (Next.js only) | Fastest build times. |
|
|
64
|
+
| **Package Manager** | Any (pnpm 9+ preferred) | npm 10+, yarn, bun | Developers may use any manager locally; however, **pnpm** is preferred for CI/CD and deployment for performance and consistency. |
|
|
65
|
+
|
|
66
|
+
### 2.2 UI & Styling
|
|
67
|
+
|
|
68
|
+
| Layer | Recommendation | Notes |
|
|
69
|
+
|-------|---------------|-------|
|
|
70
|
+
| **UI Components** | Lovable.ai generated + Radix UI primitives | Lovable generates the initial templates and components. Hand-roll custom logic on top. |
|
|
71
|
+
| **Styling** | Tailwind CSS v4 | Utility-first. Compatible with Lovable output. |
|
|
72
|
+
| **Component Library** | Shadcn UI (or custom) | Copy-pasteable components, fully customizable. |
|
|
73
|
+
| **Icons** | Lucide React | Consistent, tree-shakeable. |
|
|
74
|
+
| **Animations** | Framer Motion / tw-animate-css | For complex animations vs simple transitions. |
|
|
75
|
+
|
|
76
|
+
### 2.3 State & Data
|
|
77
|
+
|
|
78
|
+
| Layer | Recommendation | Notes |
|
|
79
|
+
|-------|---------------|-------|
|
|
80
|
+
| **Server State** | TanStack React Query (v5) | Caching, refetching, optimistic updates. |
|
|
81
|
+
| **Client State** | Zustand | Lightweight, no boilerplate. |
|
|
82
|
+
| **Forms** | React Hook Form + Zod | Validation schema-driven. |
|
|
83
|
+
| **Routing** | React Router v7 / Next.js App Router | Depends on framework choice. |
|
|
84
|
+
|
|
85
|
+
### 2.4 Testing
|
|
86
|
+
|
|
87
|
+
| Layer | Tool | Notes |
|
|
88
|
+
|-------|------|-------|
|
|
89
|
+
| **Unit / Component** | Vitest (preferred) or Jest 29 | Vitest is faster, native ESM, Vite-compatible. |
|
|
90
|
+
| **Component Testing** | Testing Library + jsdom | Test behavior, not implementation. |
|
|
91
|
+
| **E2E** | Playwright | Cross-browser, reliable, parallel. |
|
|
92
|
+
| **Visual Regression** | Playwright / Chromatic | For UI component libraries. |
|
|
93
|
+
| **Coverage** | c8 / istanbul | Minimum 80% line/branch. |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 3. Project Scaffolding & Directory Structure
|
|
98
|
+
|
|
99
|
+
### 3.1 Standard Project Layout
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
project-name/
|
|
103
|
+
src/
|
|
104
|
+
app/ # Pages / Routes
|
|
105
|
+
pages/ # Pages
|
|
106
|
+
routes/ # Route definitions
|
|
107
|
+
layouts/ # Layout components
|
|
108
|
+
components/ # All components
|
|
109
|
+
ui/ # Base UI primitives (Lovable/Shadcn)
|
|
110
|
+
forms/ # Form-specific components
|
|
111
|
+
layout/ # App layout components
|
|
112
|
+
shared/ # Shared business components
|
|
113
|
+
hooks/ # Custom React hooks
|
|
114
|
+
lib/ # Utilities, API clients, helpers
|
|
115
|
+
store/ # Zustand stores
|
|
116
|
+
types/ # TypeScript type definitions
|
|
117
|
+
services/ # API service calls (domain-modularized)
|
|
118
|
+
employee/ # Employee domain services
|
|
119
|
+
employee-bio-detail.service.ts
|
|
120
|
+
employee-bank-detail.service.ts
|
|
121
|
+
auth/ # Auth domain services
|
|
122
|
+
auth.service.ts
|
|
123
|
+
users/ # User domain services
|
|
124
|
+
users.service.ts
|
|
125
|
+
api-client.ts # Shared API client (ONLY shared file at this level)
|
|
126
|
+
styles/ # Global styles (Tailwind entry)
|
|
127
|
+
test/ # Test setup, mocks, fixtures
|
|
128
|
+
public/ # Static assets
|
|
129
|
+
scripts/ # Build & automation scripts
|
|
130
|
+
.github/workflows/ # CI/CD pipeline definitions
|
|
131
|
+
.husky/ # Git hooks
|
|
132
|
+
docs/ # Project documentation
|
|
133
|
+
-- root config files --
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3.2 Monorepo Layout (For Multiple Projects)
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
project-name/
|
|
140
|
+
apps/
|
|
141
|
+
admin/ # Admin dashboard
|
|
142
|
+
web/ # Public website
|
|
143
|
+
packages/
|
|
144
|
+
config/eslint/ # Shared ESLint config
|
|
145
|
+
config/prettier/ # Shared Prettier config
|
|
146
|
+
config/tsconfig/ # Shared TypeScript config
|
|
147
|
+
ui/ # Shared component library
|
|
148
|
+
utils/ # Shared utilities
|
|
149
|
+
types/ # Shared type definitions
|
|
150
|
+
scripts/
|
|
151
|
+
-- root config files --
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 3.3 Standards
|
|
155
|
+
|
|
156
|
+
- Every project must have a README.md at the root
|
|
157
|
+
- Environment files: .env.example (committed), .env (gitignored)
|
|
158
|
+
- Config files: Use flat config where possible
|
|
159
|
+
- Directory naming: kebab-case for all directories
|
|
160
|
+
- File naming: kebab-case for all files, except PascalCase for React components (e.g., `UserProfile.tsx`)
|
|
161
|
+
|
|
162
|
+
#### 3.3.1 Domain-Based Modularization (Strictly Enforced)
|
|
163
|
+
|
|
164
|
+
All domain-specific files across **every** directory (`services/`, `hooks/`, `store/`, `components/`, `lib/`) must be organized into feature subdirectories. Flat/orphaned files at the root of any modularized directory are **prohibited** unless they are truly shared across all domains.
|
|
165
|
+
|
|
166
|
+
Every domain subdirectory **must** expose its contents via an `index.ts` barrel file. Parent code imports from the barrel, never from individual files within a domain.
|
|
167
|
+
|
|
168
|
+
| Directory | Modularization Rule | Barrel Required |
|
|
169
|
+
|-----------|---------------------|:---:|
|
|
170
|
+
| `services/` | One subdirectory per domain (e.g., `services/employee/`, `services/auth/`). Only `api-client.ts` may live at the root. | Yes |
|
|
171
|
+
| `hooks/` | Domain-specific hooks in `hooks/<domain>/`. Shared utility hooks (`useDebounce`) may live at root. | Yes |
|
|
172
|
+
| `store/` | Domain-specific Zustand stores in `store/<domain>/`. Cross-cutting stores only at root. | Yes |
|
|
173
|
+
| `components/` | Domain-specific components in `components/<domain>/`. Shared UI primitives in `components/ui/`. | Yes |
|
|
174
|
+
| `lib/` | Domain-specific utilities in `lib/<domain>/`. Cross-domain utilities may live at root. | Yes |
|
|
175
|
+
| `types/` | Domain-specific types in `types/<domain>/`. Shared types at root (`types/user.ts`). | Yes |
|
|
176
|
+
|
|
177
|
+
**Barrel File (`index.ts`) Convention**:
|
|
178
|
+
|
|
179
|
+
Every domain subdirectory must contain an `index.ts` that re-exports all public members. Parent-level code imports exclusively from the barrel:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
services/employee/
|
|
183
|
+
index.ts # Barrel: re-exports everything
|
|
184
|
+
employee-bio-detail.service.ts # Implementation file
|
|
185
|
+
employee-bank-detail.service.ts # Implementation file
|
|
186
|
+
employee-emergency-contact.service.ts # Implementation file
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// services/employee/index.ts (BARREL FILE -- REQUIRED)
|
|
191
|
+
export { getEmployeeBio } from './employee-bio-detail.service';
|
|
192
|
+
export { getEmployeeBankDetails, updateEmployeeBankDetails } from './employee-bank-detail.service';
|
|
193
|
+
export { getEmergencyContacts } from './employee-emergency-contact.service';
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// pages/EmployeeDashboard.tsx (PARENT CONSUMER -- imports from barrel only)
|
|
198
|
+
import { getEmployeeBio, getEmployeeBankDetails } from '@/services/employee';
|
|
199
|
+
// NOT: import { getEmployeeBio } from '@/services/employee/employee-bio-detail.service'; // BLOCKED
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Violation examples** (these will be blocked by ESLint and CI):
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
# BAD -- Flat files at directory root (BLOCKED)
|
|
206
|
+
services/employee-bio-detail.service.ts
|
|
207
|
+
hooks/useEmployeeLeave.ts
|
|
208
|
+
store/employeeLeaveStore.ts
|
|
209
|
+
|
|
210
|
+
# BAD -- Missing barrel file (BLOCKED)
|
|
211
|
+
services/employee/employee-bio-detail.service.ts # No index.ts in employee/
|
|
212
|
+
|
|
213
|
+
# BAD -- Deep import bypassing barrel (BLOCKED)
|
|
214
|
+
import { getEmployeeBio } from '@/services/employee/employee-bio-detail.service';
|
|
215
|
+
|
|
216
|
+
# GOOD -- Domain-modularized with barrel (REQUIRED)
|
|
217
|
+
services/employee/index.ts # Barrel exports
|
|
218
|
+
services/employee/employee-bio-detail.service.ts
|
|
219
|
+
services/employee/employee-bank-detail.service.ts
|
|
220
|
+
import { getEmployeeBio } from '@/services/employee';
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This rule is enforced via `eslint-plugin-boundaries` combined with `eslint-plugin-import` (see Section 5.2.1 for configuration).
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 4. TypeScript Configuration Standards
|
|
228
|
+
|
|
229
|
+
### 4.1 Required Compiler Options
|
|
230
|
+
|
|
231
|
+
These options ensure a modern, strict, and performant TypeScript environment:
|
|
232
|
+
|
|
233
|
+
- **target: ES2022**: Specifies the JavaScript language version for the emitted output.
|
|
234
|
+
- **module: ESNext**: Sets the module system for the program to the latest ECMAScript standard.
|
|
235
|
+
- **moduleResolution: bundler**: Optimized module resolution for modern bundlers like Vite.
|
|
236
|
+
- **strict: true**: Enables all strict type-checking options for maximum safety.
|
|
237
|
+
- **esModuleInterop: true**: Ensures compatibility when importing CommonJS modules.
|
|
238
|
+
- **skipLibCheck: true**: Skips type checking of declaration files to speed up builds.
|
|
239
|
+
- **forceConsistentCasingInFileNames: true**: Prevents issues between case-sensitive and case-insensitive filesystems.
|
|
240
|
+
- **resolveJsonModule: true**: Allows importing .json files as modules.
|
|
241
|
+
- **isolatedModules: true**: Ensures each file can be safely transpiled independently.
|
|
242
|
+
- **allowJs: false**: Disallows plain JavaScript to enforce TypeScript usage.
|
|
243
|
+
- **noEmit: true**: Relies on the bundler (Vite) for emission, using tsc only for type checking.
|
|
244
|
+
- **jsx: react-jsx**: Enables the modern React JSX transform.
|
|
245
|
+
- **baseUrl: .**: Sets the root for non-relative module resolution.
|
|
246
|
+
- **paths**: Configures path aliases (e.g., @/*) for cleaner imports.
|
|
247
|
+
|
|
248
|
+
```jsonc
|
|
249
|
+
{
|
|
250
|
+
"compilerOptions": {
|
|
251
|
+
"target": "ES2022",
|
|
252
|
+
"module": "ESNext",
|
|
253
|
+
"moduleResolution": "bundler",
|
|
254
|
+
"strict": true,
|
|
255
|
+
"esModuleInterop": true,
|
|
256
|
+
"skipLibCheck": true,
|
|
257
|
+
"forceConsistentCasingInFileNames": true,
|
|
258
|
+
"resolveJsonModule": true,
|
|
259
|
+
"isolatedModules": true,
|
|
260
|
+
"allowJs": false,
|
|
261
|
+
"noEmit": true,
|
|
262
|
+
"jsx": "react-jsx",
|
|
263
|
+
"baseUrl": ".",
|
|
264
|
+
"paths": { "@/*": ["src/*"] }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 4.2 Strict Mode Checklist
|
|
270
|
+
|
|
271
|
+
- strict: true (enables all strict flags)
|
|
272
|
+
- noUncheckedIndexedAccess: true (prevent undefined access)
|
|
273
|
+
- exactOptionalPropertyTypes: true
|
|
274
|
+
- noPropertyAccessFromIndexSignature: true
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 5. Code Quality & Linting Standards
|
|
279
|
+
|
|
280
|
+
### 5.1 ESLint (v9 Flat Config) - Required Plugins
|
|
281
|
+
|
|
282
|
+
| Plugin | Purpose |
|
|
283
|
+
|--------|---------|
|
|
284
|
+
| @eslint/js | ESLint recommended rules |
|
|
285
|
+
| typescript-eslint | TypeScript-specific rules |
|
|
286
|
+
| eslint-plugin-react | React-specific rules |
|
|
287
|
+
| eslint-plugin-react-hooks | React Hooks rules |
|
|
288
|
+
| eslint-plugin-import | Import ordering |
|
|
289
|
+
| eslint-plugin-unicorn | Modern JS best practices |
|
|
290
|
+
| eslint-plugin-prettier | Prettier as ESLint rule |
|
|
291
|
+
| eslint-plugin-jsx-a11y | Accessibility checks |
|
|
292
|
+
| eslint-plugin-boundaries | Enforce domain-based modularization and folder dependency rules |
|
|
293
|
+
|
|
294
|
+
### 5.2 Naming Conventions -- Complete Reference
|
|
295
|
+
|
|
296
|
+
#### Core Principle: Names Must Be Self-Documenting
|
|
297
|
+
|
|
298
|
+
A variable name should tell you what it contains, not how it's implemented. If you need a comment to explain what a variable holds, the name is wrong.
|
|
299
|
+
|
|
300
|
+
**Single-letter variables are strictly prohibited** except:
|
|
301
|
+
- Loop index i, j, k inside a for loop
|
|
302
|
+
- Array callback _ for unused parameters
|
|
303
|
+
- Math coordinates x, y, z (must be documented)
|
|
304
|
+
|
|
305
|
+
#### Naming Reference Table
|
|
306
|
+
|
|
307
|
+
| Entity | Convention | Good Example | Bad Example |
|
|
308
|
+
|--------|-----------|-------------|-------------|
|
|
309
|
+
| Files (General) | kebab-case | user-service.ts | UserService.ts |
|
|
310
|
+
| Files (React Components) | PascalCase | UserProfile.tsx | user-profile.tsx |
|
|
311
|
+
| Directories | kebab-case | user-profile/ | UserProfile/ |
|
|
312
|
+
| Variables (general) | camelCase | userCount, isLoading | uc, l, data |
|
|
313
|
+
| Booleans | camelCase + prefix | isLoading, hasError, canEdit | loading, error |
|
|
314
|
+
| Functions | camelCase (verb+noun) | getUserById(), handleSubmit() | get(), doStuff() |
|
|
315
|
+
| Constants (module) | UPPER_SNAKE_CASE | MAX_RETRY_COUNT | maxRetryCount |
|
|
316
|
+
| Types/Interfaces | PascalCase | UserProfileProps | userProfile |
|
|
317
|
+
| Enums | PascalCase + UPPER values | Role.ADMIN | role.admin |
|
|
318
|
+
| Event handlers | handle + Event | handleSubmit | submitHandler |
|
|
319
|
+
| Handler props | on + Event | onSubmit, onClick | submit, clicked |
|
|
320
|
+
| Env variables | UPPER_SNAKE + prefix | VITE_API_URL | apiUrl |
|
|
321
|
+
| Mock data | mock + Entity | mockUser, mockResponse | testData |
|
|
322
|
+
|
|
323
|
+
#### Banned Variable Names
|
|
324
|
+
|
|
325
|
+
| Banned | Why | Use Instead |
|
|
326
|
+
|--------|-----|-------------|
|
|
327
|
+
| const data | Means anything | const users, const response |
|
|
328
|
+
| const temp | Temporary to whom? | const formattedDate |
|
|
329
|
+
| const arr | Hungarian + meaningless | const permissions |
|
|
330
|
+
| const obj | Everything is an object | const config |
|
|
331
|
+
| const str | Hungarian notation | const message |
|
|
332
|
+
| const result | All functions return a result | const validationResult |
|
|
333
|
+
| const myVar | my prefix adds nothing | const username |
|
|
334
|
+
| function handle() | Handle what? | function handleFormSubmit() |
|
|
335
|
+
|
|
336
|
+
#### Boolean Naming
|
|
337
|
+
|
|
338
|
+
Booleans must use a prefix that makes true/false obvious:
|
|
339
|
+
```
|
|
340
|
+
GOOD: isLoading, hasPermission, canSubmit, shouldRender, isVisible, wasProcessed
|
|
341
|
+
BAD: loading, permission, submit, render, visible, processed
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### ESLint Enforcement Rules
|
|
345
|
+
|
|
346
|
+
```javascript
|
|
347
|
+
rules: {
|
|
348
|
+
// Ban single-letter variables (minimum 2 characters)
|
|
349
|
+
'id-length': ['error', {
|
|
350
|
+
min: 2, max: 50,
|
|
351
|
+
exceptions: ['i', 'j', 'k', '_', 'x', 'y', 'z'],
|
|
352
|
+
properties: 'never',
|
|
353
|
+
}],
|
|
354
|
+
// Enforce camelCase
|
|
355
|
+
camelcase: ['error', { properties: 'never', ignoreDestructuring: false, allow: ['^UNSAFE_'] }],
|
|
356
|
+
// Enforce kebab-case file names, except for PascalCase React components
|
|
357
|
+
'unicorn/filename-case': ['error', {
|
|
358
|
+
case: 'kebabCase',
|
|
359
|
+
ignore: ['^[A-Z].*\\.tsx$'],
|
|
360
|
+
}],
|
|
361
|
+
// Enforce PascalCase for React components
|
|
362
|
+
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
#### 5.2.1 Domain-Based Modularization Enforcement (eslint-plugin-boundaries + eslint-plugin-import)
|
|
367
|
+
|
|
368
|
+
Install the required plugins:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
pnpm add -D eslint-plugin-boundaries eslint-plugin-import
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Configuration for enforcing domain-based folder modularization across **all** directories
|
|
375
|
+
(`services/`, `hooks/`, `store/`, `components/`, `lib/`, `types/`), including barrel file (`index.ts`) enforcement:
|
|
376
|
+
|
|
377
|
+
```javascript
|
|
378
|
+
// eslint.config.js (flat config)
|
|
379
|
+
import boundaries from "eslint-plugin-boundaries";
|
|
380
|
+
import importPlugin from "eslint-plugin-import";
|
|
381
|
+
|
|
382
|
+
export default [
|
|
383
|
+
// ... other configs
|
|
384
|
+
{
|
|
385
|
+
plugins: { boundaries, import: importPlugin },
|
|
386
|
+
settings: {
|
|
387
|
+
"boundaries/include": ["src/**/*"],
|
|
388
|
+
"boundaries/elements": [
|
|
389
|
+
// Shared/global files (can be imported by any domain)
|
|
390
|
+
{
|
|
391
|
+
mode: "full",
|
|
392
|
+
type: "shared",
|
|
393
|
+
pattern: [
|
|
394
|
+
"src/services/api-client.ts",
|
|
395
|
+
"src/hooks/use-debounce.ts",
|
|
396
|
+
"src/hooks/use-media-query.ts",
|
|
397
|
+
"src/components/ui/**",
|
|
398
|
+
"src/lib/*.ts",
|
|
399
|
+
"src/lib/*.tsx",
|
|
400
|
+
"src/types/!(*/**).ts",
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
// Employee domain -- all directories
|
|
404
|
+
{
|
|
405
|
+
mode: "full",
|
|
406
|
+
type: "employee",
|
|
407
|
+
pattern: [
|
|
408
|
+
"src/services/employee/**",
|
|
409
|
+
"src/hooks/employee/**",
|
|
410
|
+
"src/store/employee/**",
|
|
411
|
+
"src/components/employee/**",
|
|
412
|
+
"src/lib/employee/**",
|
|
413
|
+
"src/types/employee/**",
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
// Auth domain -- all directories
|
|
417
|
+
{
|
|
418
|
+
mode: "full",
|
|
419
|
+
type: "auth",
|
|
420
|
+
pattern: [
|
|
421
|
+
"src/services/auth/**",
|
|
422
|
+
"src/hooks/auth/**",
|
|
423
|
+
"src/store/auth/**",
|
|
424
|
+
"src/components/auth/**",
|
|
425
|
+
"src/lib/auth/**",
|
|
426
|
+
"src/types/auth/**",
|
|
427
|
+
],
|
|
428
|
+
},
|
|
429
|
+
// Users domain -- all directories
|
|
430
|
+
{
|
|
431
|
+
mode: "full",
|
|
432
|
+
type: "users",
|
|
433
|
+
pattern: [
|
|
434
|
+
"src/services/users/**",
|
|
435
|
+
"src/hooks/users/**",
|
|
436
|
+
"src/store/users/**",
|
|
437
|
+
"src/components/users/**",
|
|
438
|
+
"src/lib/users/**",
|
|
439
|
+
"src/types/users/**",
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
rules: {
|
|
445
|
+
// === BOUNDARIES RULES ===
|
|
446
|
+
|
|
447
|
+
// BLOCK: flat/orphaned domain files at directory roots (enforces subdirectory pattern)
|
|
448
|
+
"boundaries/entry-point": [
|
|
449
|
+
"error",
|
|
450
|
+
{
|
|
451
|
+
default: "disallow",
|
|
452
|
+
rules: [
|
|
453
|
+
// Allow ONLY api-client.ts at the services/ root level
|
|
454
|
+
{
|
|
455
|
+
target: ["src/services/**/*.ts"],
|
|
456
|
+
allow: "src/services/api-client.ts",
|
|
457
|
+
},
|
|
458
|
+
// Allow shared utility hooks at root but NOT domain-specific ones
|
|
459
|
+
{
|
|
460
|
+
target: ["src/hooks/use-employee*.ts", "src/hooks/use-auth*.ts", "src/hooks/use-user*.ts"],
|
|
461
|
+
disallow: "src/hooks/",
|
|
462
|
+
},
|
|
463
|
+
// No domain-specific files at store/ root
|
|
464
|
+
{
|
|
465
|
+
target: ["src/store/**/*.ts"],
|
|
466
|
+
allow: "src/store/index.ts",
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
|
|
472
|
+
// BLOCK: cross-domain imports (e.g., employee component cannot import from auth services)
|
|
473
|
+
"boundaries/element-types": [
|
|
474
|
+
"error",
|
|
475
|
+
{
|
|
476
|
+
default: "disallow",
|
|
477
|
+
rules: [
|
|
478
|
+
// Shared files can be imported by any domain
|
|
479
|
+
{ from: ["employee", "auth", "users"], allow: ["shared"] },
|
|
480
|
+
// Domains cannot import from other domains
|
|
481
|
+
{ from: ["employee"], disallow: ["auth", "users"] },
|
|
482
|
+
{ from: ["auth"], disallow: ["employee", "users"] },
|
|
483
|
+
{ from: ["users"], disallow: ["employee", "auth"] },
|
|
484
|
+
// Shared can import from shared
|
|
485
|
+
{ from: ["shared"], allow: ["shared"] },
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
|
|
490
|
+
// === IMPORT RULES: BLOCK deep imports bypassing barrel files ===
|
|
491
|
+
|
|
492
|
+
// BLOCK: importing from a non-index file inside a domain subdirectory
|
|
493
|
+
"import/no-internal-modules": [
|
|
494
|
+
"error",
|
|
495
|
+
{
|
|
496
|
+
allow: [
|
|
497
|
+
// Only allow importing the barrel (index.ts) from each domain directory
|
|
498
|
+
"src/services/*/index",
|
|
499
|
+
"src/hooks/*/index",
|
|
500
|
+
"src/store/*/index",
|
|
501
|
+
"src/components/*/index",
|
|
502
|
+
"src/lib/*/index",
|
|
503
|
+
"src/types/*/index",
|
|
504
|
+
// Allow shared/utility files
|
|
505
|
+
"src/services/api-client",
|
|
506
|
+
"src/hooks/use-debounce",
|
|
507
|
+
"src/hooks/use-media-query",
|
|
508
|
+
"src/components/ui/**",
|
|
509
|
+
"src/lib/*",
|
|
510
|
+
"src/types/*",
|
|
511
|
+
// Allow root-level globals
|
|
512
|
+
"src/app/**",
|
|
513
|
+
"src/styles/**",
|
|
514
|
+
"src/test/**",
|
|
515
|
+
],
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
|
|
519
|
+
// BLOCK: importing from index files across domain boundaries (backup for boundaries plugin)
|
|
520
|
+
"import/no-restricted-paths": [
|
|
521
|
+
"error",
|
|
522
|
+
{
|
|
523
|
+
zones: [
|
|
524
|
+
// Employee domain may not import from other domains
|
|
525
|
+
{
|
|
526
|
+
target: "./src/**/employee/**",
|
|
527
|
+
from: "./src/**/auth/**",
|
|
528
|
+
message: "Employee domain must not import from Auth domain. Use shared interfaces instead.",
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
target: "./src/**/employee/**",
|
|
532
|
+
from: "./src/**/users/**",
|
|
533
|
+
message: "Employee domain must not import from Users domain. Use shared interfaces instead.",
|
|
534
|
+
},
|
|
535
|
+
// (repeat for each domain pair)
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
];
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**Enforcement Summary**:
|
|
545
|
+
|
|
546
|
+
| Rule | What It Catches | Severity |
|
|
547
|
+
|------|----------------|----------|
|
|
548
|
+
| `boundaries/entry-point` | Flat domain files at directory roots (e.g., `services/employeeService.ts` instead of `services/employee/`) | `error` |
|
|
549
|
+
| `boundaries/element-types` | Cross-domain imports (e.g., `employee` component importing from `auth` services) | `error` |
|
|
550
|
+
| `import/no-internal-modules` | **Deep imports bypassing barrel files** (e.g., `from '@/services/employee/employee-bio-detail.service'` instead of `from '@/services/employee'`) | `error` |
|
|
551
|
+
| `import/no-restricted-paths` | Backup enforcement of cross-domain import restrictions | `error` |
|
|
552
|
+
|
|
553
|
+
**Barrel File Enforcement Flow**:
|
|
554
|
+
|
|
555
|
+
```
|
|
556
|
+
✅ ALLOWED: import { getBio } from '@/services/employee';
|
|
557
|
+
// Resolves to services/employee/index.ts (barrel)
|
|
558
|
+
|
|
559
|
+
❌ BLOCKED: import { getBio } from '@/services/employee/employee-bio-detail.service';
|
|
560
|
+
// Deep import bypassing barrel -- `import/no-internal-modules` catches this
|
|
561
|
+
|
|
562
|
+
✅ ALLOWED: import { useEmployeeBio } from '@/hooks/employee';
|
|
563
|
+
// Resolves to hooks/employee/index.ts (barrel)
|
|
564
|
+
|
|
565
|
+
❌ BLOCKED: import { useEmployeeBio } from '@/hooks/employee/useEmployeeBio';
|
|
566
|
+
// Deep import bypassing barrel
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**Adding a New Domain**:
|
|
570
|
+
1. Create the domain subdirectories across all folders:
|
|
571
|
+
```
|
|
572
|
+
services/<domain>/
|
|
573
|
+
hooks/<domain>/
|
|
574
|
+
store/<domain>/
|
|
575
|
+
components/<domain>/
|
|
576
|
+
lib/<domain>/
|
|
577
|
+
types/<domain>/
|
|
578
|
+
```
|
|
579
|
+
2. Create an `index.ts` barrel file in each subdirectory
|
|
580
|
+
3. Add the domain element type to `boundaries/elements`
|
|
581
|
+
4. Add the `from`/`disallow` rules to `boundaries/element-types`
|
|
582
|
+
5. Add the domain barrel paths to `import/no-internal-modules` allow list
|
|
583
|
+
6. Add cross-domain restrictions to `import/no-restricted-paths`
|
|
584
|
+
7. The domain starts isolated; update rules if cross-domain communication is needed (via shared interfaces only)
|
|
585
|
+
|
|
586
|
+
#### Naming Decision Tree
|
|
587
|
+
|
|
588
|
+
```
|
|
589
|
+
Is it a boolean? --> is/has/can/should/did/was prefix
|
|
590
|
+
Is it a count? --> count/total/num/index suffix
|
|
591
|
+
Is it a list? --> Plural noun or List suffix
|
|
592
|
+
Is it an entity? --> Specific singular noun
|
|
593
|
+
Is it a getter? --> get/fetch prefix
|
|
594
|
+
Is it a transformer?--> format/convert prefix
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
#### Good vs Bad Examples
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
// BAD -- Would fail code review
|
|
601
|
+
const d = new Date();
|
|
602
|
+
const r = await fetch('/api/users');
|
|
603
|
+
const arr = [1, 2, 3];
|
|
604
|
+
const temp = users.filter(u => u.active);
|
|
605
|
+
function get() { }
|
|
606
|
+
function handle() { }
|
|
607
|
+
|
|
608
|
+
// GOOD -- Self-documenting
|
|
609
|
+
const currentDate = new Date();
|
|
610
|
+
const response = await fetch('/api/users');
|
|
611
|
+
const userIds = [1, 2, 3];
|
|
612
|
+
const activeUsers = users.filter(user => user.isActive);
|
|
613
|
+
function getActiveUsers() { }
|
|
614
|
+
function handleFormSubmit() { }
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### 5.3 Import Order Standard
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
// 1. Built-in Node modules
|
|
621
|
+
import fs from 'fs';
|
|
622
|
+
// 2. External dependencies
|
|
623
|
+
import { useState } from 'react';
|
|
624
|
+
// 3. Internal modules (using @/ alias)
|
|
625
|
+
import { Button } from '@/components/ui/button';
|
|
626
|
+
// 4. Parent imports
|
|
627
|
+
import { formatDate } from '../utils/date';
|
|
628
|
+
// 5. Sibling imports
|
|
629
|
+
import { UserCard } from './user-card';
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### 5.4 Lovable-Generated Code Handling
|
|
633
|
+
|
|
634
|
+
When Lovable generates UI templates, always:
|
|
635
|
+
1. Run generated code through ESLint + Prettier before committing.
|
|
636
|
+
2. Extract reusable components into components/shared/ directory.
|
|
637
|
+
3. Add proper TypeScript types to any untyped props.
|
|
638
|
+
4. Ensure generated components pass accessibility checks.
|
|
639
|
+
5. Add JSDoc comments to any complex generated logic.
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## 6. Formatting Standards
|
|
644
|
+
|
|
645
|
+
### 6.1 Prettier Configuration
|
|
646
|
+
|
|
647
|
+
```javascript
|
|
648
|
+
// .prettierrc.js
|
|
649
|
+
export default {
|
|
650
|
+
semi: true,
|
|
651
|
+
singleQuote: true,
|
|
652
|
+
tabWidth: 2,
|
|
653
|
+
useTabs: false,
|
|
654
|
+
trailingComma: 'es5',
|
|
655
|
+
printWidth: 100,
|
|
656
|
+
bracketSpacing: true,
|
|
657
|
+
arrowParens: 'always',
|
|
658
|
+
endOfLine: 'lf',
|
|
659
|
+
plugins: ['prettier-plugin-organize-imports'],
|
|
660
|
+
};
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### 6.2 Format-On-Save (VS Code)
|
|
664
|
+
|
|
665
|
+
Every project must have .vscode/settings.json:
|
|
666
|
+
```json
|
|
667
|
+
{
|
|
668
|
+
"editor.formatOnSave": true,
|
|
669
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
670
|
+
"editor.codeActionsOnSave": {
|
|
671
|
+
"source.fixAll.eslint": "explicit"
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## 7. UI Component Standards (Lovable Integration)
|
|
679
|
+
|
|
680
|
+
### 7.1 Lovable Workflow
|
|
681
|
+
|
|
682
|
+
1. **Design in Lovable**: Product designer creates UI mockups/templates.
|
|
683
|
+
2. **Export clean components**: Export generated React components with Tailwind styling.
|
|
684
|
+
3. **Integrate into project**: Place components in src/components/ui/ or shared/.
|
|
685
|
+
4. **Add business logic**: Connect components to stores, API calls, and routing.
|
|
686
|
+
5. **Test**: Write unit tests for logic, visual regression for UI.
|
|
687
|
+
6. **Review**: Code review must verify generated code meets project standards.
|
|
688
|
+
|
|
689
|
+
### 7.2 Component Architecture
|
|
690
|
+
|
|
691
|
+
```
|
|
692
|
+
src/components/
|
|
693
|
+
ui/ # Base UI primitives (from Lovable/Shadcn)
|
|
694
|
+
Button.tsx
|
|
695
|
+
Input.tsx
|
|
696
|
+
Dialog.tsx
|
|
697
|
+
Table.tsx
|
|
698
|
+
forms/ # Form-specific compositions
|
|
699
|
+
LoginForm.tsx
|
|
700
|
+
SignupForm.tsx
|
|
701
|
+
layout/ # Layout components
|
|
702
|
+
Header.tsx
|
|
703
|
+
Sidebar.tsx
|
|
704
|
+
shared/ # Domain-specific shared components
|
|
705
|
+
UserAvatar.tsx
|
|
706
|
+
DataTable.tsx
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### 7.3 Component Best Practices
|
|
710
|
+
|
|
711
|
+
- One component per file (except small utility components)
|
|
712
|
+
- Props interface defined and exported
|
|
713
|
+
- Default export for pages, named export for reusable components
|
|
714
|
+
- Composition over configuration: prefer children and slots over boolean props
|
|
715
|
+
- Lovable components should be presentational -- business logic in hooks and stores
|
|
716
|
+
|
|
717
|
+
### 7.3.1 Error Boundaries (Required)
|
|
718
|
+
|
|
719
|
+
Every route-level component must be wrapped in an Error Boundary. Implement a standard `<ErrorBoundary>` component:
|
|
720
|
+
|
|
721
|
+
```tsx
|
|
722
|
+
import { Component, type ReactNode } from 'react';
|
|
723
|
+
import { logger } from '@/lib/logger';
|
|
724
|
+
|
|
725
|
+
interface ErrorBoundaryProps {
|
|
726
|
+
fallback?: ReactNode;
|
|
727
|
+
children: ReactNode;
|
|
728
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
interface ErrorBoundaryState {
|
|
732
|
+
hasError: boolean;
|
|
733
|
+
error: Error | null;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
737
|
+
state: ErrorBoundaryState = { hasError: false, error: null };
|
|
738
|
+
|
|
739
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
740
|
+
return { hasError: true, error };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
744
|
+
logger.error('ErrorBoundary caught an error', { error, componentStack: errorInfo.componentStack });
|
|
745
|
+
this.props.onError?.(error, errorInfo);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
handleReset = () => this.setState({ hasError: false, error: null });
|
|
749
|
+
|
|
750
|
+
render() {
|
|
751
|
+
if (this.state.hasError) {
|
|
752
|
+
if (this.props.fallback) return this.props.fallback;
|
|
753
|
+
return (
|
|
754
|
+
<div role="alert" className="flex flex-col items-center justify-center min-h-[400px] p-8">
|
|
755
|
+
<h2 className="text-xl font-semibold mb-2">Something went wrong</h2>
|
|
756
|
+
<p className="text-muted-foreground mb-4">An unexpected error occurred.</p>
|
|
757
|
+
<button onClick={this.handleReset} className="px-4 py-2 bg-primary text-primary-foreground rounded-md">
|
|
758
|
+
Try Again
|
|
759
|
+
</button>
|
|
760
|
+
</div>
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
return this.props.children;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
**Usage**:
|
|
769
|
+
```tsx
|
|
770
|
+
// Route-level wrapping
|
|
771
|
+
<ErrorBoundary onError={(err) => captureException(err)}>
|
|
772
|
+
<EmployeeDashboard />
|
|
773
|
+
</ErrorBoundary>
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
**Rules**:
|
|
777
|
+
- One ErrorBoundary per route (not per component) for granularity
|
|
778
|
+
- Log errors to Sentry via `onError` callback
|
|
779
|
+
- Never wrap individual small components (<50 lines) in their own ErrorBoundary
|
|
780
|
+
- Fallback UI must include a "Try Again" reset action
|
|
781
|
+
|
|
782
|
+
### 7.3.2 Code Splitting & Lazy Loading (Required for Routes)
|
|
783
|
+
|
|
784
|
+
All page-level components must be lazy-loaded to reduce initial bundle size.
|
|
785
|
+
|
|
786
|
+
```tsx
|
|
787
|
+
import { lazy, Suspense } from 'react';
|
|
788
|
+
import { ErrorBoundary } from '@/components/shared/error-boundary';
|
|
789
|
+
import { PageSkeleton } from '@/components/ui/page-skeleton';
|
|
790
|
+
|
|
791
|
+
const EmployeeDashboard = lazy(() => import('@/pages/employee/employee-dashboard'));
|
|
792
|
+
const EmployeeProfile = lazy(() => import('@/pages/employee/employee-profile'));
|
|
793
|
+
|
|
794
|
+
export function EmployeeRoutes() {
|
|
795
|
+
return (
|
|
796
|
+
<ErrorBoundary>
|
|
797
|
+
<Suspense fallback={<PageSkeleton />}>
|
|
798
|
+
<Routes>
|
|
799
|
+
<Route path="dashboard" element={<EmployeeDashboard />} />
|
|
800
|
+
<Route path="profile/:id" element={<EmployeeProfile />} />
|
|
801
|
+
</Routes>
|
|
802
|
+
</Suspense>
|
|
803
|
+
</ErrorBoundary>
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
**Rules**:
|
|
809
|
+
- Every page/route component must use `React.lazy()` + dynamic `import()`
|
|
810
|
+
- Every lazy route must have a `<Suspense>` wrapper with a skeleton fallback
|
|
811
|
+
- Prefetch critical routes (dashboard, home) on link hover or visibility
|
|
812
|
+
- Named exports are required for lazy-loaded modules: `export function EmployeeDashboard()`
|
|
813
|
+
- Chunk naming: use `webpackChunkName: 'employee'` comment for Vite/webpack
|
|
814
|
+
|
|
815
|
+
### 7.4.1 Loading & Error State Patterns
|
|
816
|
+
|
|
817
|
+
**Loading States**:
|
|
818
|
+
|
|
819
|
+
| Context | Pattern | When |
|
|
820
|
+
|---------|---------|------|
|
|
821
|
+
| Page load | Skeleton screen | Initial page navigation |
|
|
822
|
+
| Table/list data | Skeleton rows or spinner overlay | Data fetching |
|
|
823
|
+
| Button action | Spinner inside button + disabled state | Form submit, save, delete |
|
|
824
|
+
| File upload | Progress bar | Large uploads |
|
|
825
|
+
|
|
826
|
+
Debounce loader appearance: do not show any loading indicator for fetches that resolve in < 300ms (avoid flash).
|
|
827
|
+
|
|
828
|
+
```tsx
|
|
829
|
+
// Standard page pattern
|
|
830
|
+
function EmployeePage() {
|
|
831
|
+
const { data, isLoading, isError, error, refetch } = useEmployee(id);
|
|
832
|
+
|
|
833
|
+
if (isLoading) return <PageSkeleton />;
|
|
834
|
+
if (isError) return <ErrorState error={error} onRetry={refetch} />;
|
|
835
|
+
if (!data) return <EmptyState message="No employee data found" action={<CreateEmployeeButton />} />;
|
|
836
|
+
|
|
837
|
+
return <EmployeeProfile employee={data} />;
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
**Error States** — use a standard `<ErrorState>` component:
|
|
842
|
+
|
|
843
|
+
```tsx
|
|
844
|
+
interface ErrorStateProps {
|
|
845
|
+
error?: Error | string;
|
|
846
|
+
onRetry?: () => void;
|
|
847
|
+
title?: string;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export function ErrorState({ error, onRetry, title = 'Something went wrong' }: ErrorStateProps) {
|
|
851
|
+
return (
|
|
852
|
+
<div role="alert" className="flex flex-col items-center justify-center min-h-[300px] p-6 text-center">
|
|
853
|
+
<AlertTriangle className="w-12 h-12 text-destructive mb-4" />
|
|
854
|
+
<h3 className="text-lg font-semibold mb-1">{title}</h3>
|
|
855
|
+
<p className="text-muted-foreground mb-4 max-w-md">
|
|
856
|
+
{typeof error === 'string' ? error : error?.message || 'An unexpected error occurred. Please try again.'}
|
|
857
|
+
</p>
|
|
858
|
+
{onRetry && (
|
|
859
|
+
<button onClick={onRetry} className="px-4 py-2 bg-primary text-primary-foreground rounded-md">
|
|
860
|
+
Try Again
|
|
861
|
+
</button>
|
|
862
|
+
)}
|
|
863
|
+
</div>
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### 7.4.2 Accessibility Standards (WCAG 2.1 AA — Mandatory)
|
|
869
|
+
|
|
870
|
+
Every UI component must meet WCAG 2.1 AA standards. The following are **non-negotiable**:
|
|
871
|
+
|
|
872
|
+
| Requirement | Implementation |
|
|
873
|
+
|-------------|---------------|
|
|
874
|
+
| Color contrast | Minimum 4.5:1 for text, 3:1 for large text. Verify with axe DevTools. |
|
|
875
|
+
| Keyboard navigation | All interactive elements reachable via Tab. Logical tab order. Visible focus ring. |
|
|
876
|
+
| Skip link | Skip-to-content link as first focusable element on every page. |
|
|
877
|
+
| Screen reader | `aria-label` on icon-only buttons. `aria-describedby` linking inputs to error messages. |
|
|
878
|
+
| Form accessibility | Every `<input>` must have an associated `<label>`. Error messages linked via `aria-describedby`. |
|
|
879
|
+
| Modals/Dialogs | Focus trap inside modal. Escape to close. `aria-modal="true"`. |
|
|
880
|
+
| Images | All `<img>` must have `alt` attribute (descriptive or empty for decorative). |
|
|
881
|
+
| Landmarks | Use semantic HTML: `<header>`, `<main>`, `<nav>`, `<footer>`. |
|
|
882
|
+
|
|
883
|
+
**Required tools**: axe DevTools browser extension, eslint-plugin-jsx-a11y (already enforced via ESLint).
|
|
884
|
+
|
|
885
|
+
---
|
|
886
|
+
|
|
887
|
+
### 7.5 Table, Navigation, and Branding Standards
|
|
888
|
+
|
|
889
|
+
#### Filterable and Sortable Tables (Required)
|
|
890
|
+
|
|
891
|
+
Every data table MUST support sortable column headers, global and per-column filtering, debounced search, persistent sort preference, visible active filters, and a no-results state.
|
|
892
|
+
|
|
893
|
+
Required table features:
|
|
894
|
+
- Sort on click for all non-action columns with sort direction indicator
|
|
895
|
+
- Filter input above table with 300-400ms debounce
|
|
896
|
+
- Column visibility toggle
|
|
897
|
+
- Row selection with selected count and bulk actions
|
|
898
|
+
- Server-side pagination with page numbers, prev/next, and page size selector
|
|
899
|
+
- Empty state with friendly text and a primary action
|
|
900
|
+
- Loading state using table skeleton or row spinners
|
|
901
|
+
- Error state with retry button
|
|
902
|
+
|
|
903
|
+
#### Jump-to-Top Button (Required on Long Pages)
|
|
904
|
+
|
|
905
|
+
Pages taller than 2x viewport MUST include a fixed back-to-top button:
|
|
906
|
+
- Appears after scrolling 500px
|
|
907
|
+
- Smooth scroll to top
|
|
908
|
+
- Hidden when at top
|
|
909
|
+
- Accessible: keyboard navigable with aria-label
|
|
910
|
+
- Use an up chevron or arrow icon
|
|
911
|
+
|
|
912
|
+
#### Go Back / Go to Previous (Context-Aware Navigation)
|
|
913
|
+
|
|
914
|
+
Every page except the home/landing page MUST provide navigation aids:
|
|
915
|
+
- Breadcrumbs showing full path: Home > Reports > Financial > Q1 2026
|
|
916
|
+
- Back button returning to parent list or previous page
|
|
917
|
+
- Context-aware return preserving list filters and pagination via query params
|
|
918
|
+
|
|
919
|
+
#### Corporate Branding (Required)
|
|
920
|
+
|
|
921
|
+
Every application MUST implement consistent corporate branding:
|
|
922
|
+
- Primary logo in header/sidebar, clickable to home, with favicon and Apple touch icon
|
|
923
|
+
- Brand colors defined in Tailwind config with minimum 4.5:1 contrast ratio
|
|
924
|
+
- Typography: Inter or system fonts, defined heading scale, 1rem base body text
|
|
925
|
+
- Consistent spacing: 4px base unit, standard page padding, consistent gaps
|
|
926
|
+
- Approved loading and empty states using corporate colors
|
|
927
|
+
- 404 and error pages with corporate logo, friendly message, and primary action
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
---
|
|
933
|
+
|
|
934
|
+
## 8. Testing Standards
|
|
935
|
+
|
|
936
|
+
### 8.1 Testing Pyramid
|
|
937
|
+
|
|
938
|
+
```
|
|
939
|
+
/\
|
|
940
|
+
/E2E\ Playwright (critical user flows)
|
|
941
|
+
/------\
|
|
942
|
+
/Integr.\ API + Component integration tests
|
|
943
|
+
/----------\
|
|
944
|
+
/ Unit Tests \ Vitest + Testing Library (fast, isolated)
|
|
945
|
+
/--------------\
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
### 8.2 Coverage Requirements
|
|
949
|
+
|
|
950
|
+
| Level | Minimum Coverage | Notes |
|
|
951
|
+
|-------|-----------------|-------|
|
|
952
|
+
| Line | 80% | Enforced in CI pipeline |
|
|
953
|
+
| Branch | 75% | Enforced in CI pipeline |
|
|
954
|
+
| New Code | 90% | PR checks must pass |
|
|
955
|
+
| Critical Paths | 100% | Auth, payments, data export |
|
|
956
|
+
|
|
957
|
+
### 8.3 Test Structure
|
|
958
|
+
|
|
959
|
+
```typescript
|
|
960
|
+
describe('UserProfile', () => {
|
|
961
|
+
describe('rendering', () => {
|
|
962
|
+
it('should display user name when data is loaded', () => {
|
|
963
|
+
const user = { name: 'John Doe', email: 'john@example.com' };
|
|
964
|
+
render(<UserProfile user={user} />);
|
|
965
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it('should show loading state when data is fetching', () => {
|
|
969
|
+
render(<UserProfile isLoading={true} />);
|
|
970
|
+
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
describe('error handling', () => {
|
|
975
|
+
it('should display error message when API fails', () => {
|
|
976
|
+
render(<UserProfile error="Failed to load" />);
|
|
977
|
+
expect(screen.getByText(/failed/i)).toBeInTheDocument();
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### 8.4 E2E Standards (Playwright)
|
|
984
|
+
|
|
985
|
+
- One spec file per critical user journey
|
|
986
|
+
- Use data-testid attributes (not CSS classes) for selectors
|
|
987
|
+
- Run against production-like environment (staging)
|
|
988
|
+
- Include: login flow, CRUD operations, error states
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
992
|
+
## 9. Git Workflow & Commit Standards
|
|
993
|
+
|
|
994
|
+
### 9.1 Branch Strategy
|
|
995
|
+
|
|
996
|
+
```
|
|
997
|
+
main # Production-ready code
|
|
998
|
+
develop # Integration branch
|
|
999
|
+
feature/* # New features (branch from develop)
|
|
1000
|
+
bugfix/* # Bug fixes (branch from develop)
|
|
1001
|
+
hotfix/* # Critical fixes (branch from main, merge back)
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### 9.2 Commit Message Format
|
|
1005
|
+
|
|
1006
|
+
```
|
|
1007
|
+
<type>(<scope>): <description>
|
|
1008
|
+
[optional body]
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
**Types**: feat, fix, security, perf, docs, style, refactor, test, chore
|
|
1012
|
+
|
|
1013
|
+
**Examples**:
|
|
1014
|
+
```
|
|
1015
|
+
feat(auth): add JWT refresh token flow
|
|
1016
|
+
fix(profile): resolve avatar upload timeout
|
|
1017
|
+
security(api): implement rate limiting on login
|
|
1018
|
+
chore(deps): upgrade React to v19
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
### 9.3 Git Hooks (Husky)
|
|
1022
|
+
|
|
1023
|
+
| Hook | Action | Tool |
|
|
1024
|
+
|------|--------|------|
|
|
1025
|
+
| pre-commit | Lint staged files | lint-staged |
|
|
1026
|
+
| commit-msg | Validate commit message | commitlint |
|
|
1027
|
+
| pre-push | Run lint + modularization + type check + tests | eslint + check-modularization + tsc + vitest |
|
|
1028
|
+
|
|
1029
|
+
**lint-staged config**:
|
|
1030
|
+
```json
|
|
1031
|
+
{
|
|
1032
|
+
"src/**/*.{ts,tsx}": ["eslint --fix", "prettier --write"],
|
|
1033
|
+
"*.{json,md,yaml,yml}": ["prettier --write"]
|
|
1034
|
+
}
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
---
|
|
1038
|
+
|
|
1039
|
+
## 10. Code Review Standards
|
|
1040
|
+
|
|
1041
|
+
### 10.1 PR Pre-Submission Checklist
|
|
1042
|
+
|
|
1043
|
+
> **PR Template Required**: Every repository must contain `.github/PULL_REQUEST_TEMPLATE.md`. The canonical templates are provided by `@aoholdings/frontend-standards` (frontend) and `@aoholdings/backend-standards` (backend). They are auto-copied into `.github/` on `pnpm install`. All PRs must follow the template structure. CI validates this via the PR checklist gates.
|
|
1044
|
+
|
|
1045
|
+
- [ ] Code compiles without errors (tsc --noEmit)
|
|
1046
|
+
- [ ] All lint rules pass
|
|
1047
|
+
- [ ] All tests pass
|
|
1048
|
+
- [ ] New code has >=90% test coverage
|
|
1049
|
+
- [ ] No .only in test files
|
|
1050
|
+
- [ ] No console.log or debugger statements
|
|
1051
|
+
- [ ] No hardcoded secrets, API keys, or URLs
|
|
1052
|
+
- [ ] Branch is up to date with develop/main
|
|
1053
|
+
- [ ] PR description follows the template (`.github/PULL_REQUEST_TEMPLATE.md`)
|
|
1054
|
+
|
|
1055
|
+
### 10.2 PR Description Template
|
|
1056
|
+
|
|
1057
|
+
> **Enforcement**: Every repository must contain `.github/PULL_REQUEST_TEMPLATE.md`. The canonical PR template is provided by `@aoholdings/frontend-standards` and auto-copied on `pnpm install`. CI will flag PRs that do not follow the template structure. See [Section 19.10](#1910-enforcement-checklist-for-new-projects) for the enforcement checklist.
|
|
1058
|
+
|
|
1059
|
+
```markdown
|
|
1060
|
+
## Description
|
|
1061
|
+
[Brief description of the change]
|
|
1062
|
+
|
|
1063
|
+
## Type of Change
|
|
1064
|
+
- [ ] feat: New feature
|
|
1065
|
+
- [ ] fix: Bug fix
|
|
1066
|
+
- [ ] refactor: Code restructure
|
|
1067
|
+
- [ ] test: Test changes
|
|
1068
|
+
- [ ] docs: Documentation
|
|
1069
|
+
- [ ] chore: Build/tooling
|
|
1070
|
+
|
|
1071
|
+
## Testing Done
|
|
1072
|
+
- [ ] Unit tests added/updated
|
|
1073
|
+
- [ ] Manual testing performed
|
|
1074
|
+
- [ ] E2E tests (if applicable)
|
|
1075
|
+
|
|
1076
|
+
## Screenshots (if UI change)
|
|
1077
|
+
|
|
1078
|
+
## Checklist
|
|
1079
|
+
- [ ] Code follows project style guidelines
|
|
1080
|
+
- [ ] Self-review completed
|
|
1081
|
+
- [ ] No breaking changes (or documented)
|
|
1082
|
+
- [ ] Security implications considered
|
|
1083
|
+
- [ ] Accessibility checked (if UI change)
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
### 10.3 Review Checklist
|
|
1087
|
+
|
|
1088
|
+
**Functional**:
|
|
1089
|
+
- Does the code do what its supposed to?
|
|
1090
|
+
- Are edge cases handled (loading, empty, error states)?
|
|
1091
|
+
- Are there appropriate tests?
|
|
1092
|
+
|
|
1093
|
+
**Architecture**:
|
|
1094
|
+
- Is the component properly decomposed?
|
|
1095
|
+
- Is business logic separated from UI?
|
|
1096
|
+
- Are state management decisions appropriate?
|
|
1097
|
+
|
|
1098
|
+
**Security**:
|
|
1099
|
+
- Are user inputs validated and sanitized?
|
|
1100
|
+
- Are secrets exposed anywhere?
|
|
1101
|
+
- Is authentication/authorization enforced?
|
|
1102
|
+
|
|
1103
|
+
**Performance**:
|
|
1104
|
+
- Are there unnecessary re-renders?
|
|
1105
|
+
- Are large lists properly virtualized?
|
|
1106
|
+
- Are images optimized?
|
|
1107
|
+
|
|
1108
|
+
**Accessibility**:
|
|
1109
|
+
- Are all interactive elements keyboard-accessible?
|
|
1110
|
+
- Do images have alt text?
|
|
1111
|
+
- Is color contrast sufficient?
|
|
1112
|
+
|
|
1113
|
+
**Lovable Code**:
|
|
1114
|
+
- Has the generated code been cleaned up (no unused imports, proper types)?
|
|
1115
|
+
- Are generated components properly integrated?
|
|
1116
|
+
|
|
1117
|
+
### 10.4 Review Process Flow
|
|
1118
|
+
|
|
1119
|
+
```
|
|
1120
|
+
PR Created
|
|
1121
|
+
|
|
|
1122
|
+
Auto Checks (CI): Lint, Types, Tests, Coverage
|
|
1123
|
+
|
|
|
1124
|
+
|-- FAIL --> Author fixes --> PR updated
|
|
1125
|
+
|
|
|
1126
|
+
v PASS
|
|
1127
|
+
Peer Review (at least 1 senior dev)
|
|
1128
|
+
|
|
|
1129
|
+
|-- CHANGES REQUESTED --> Author addresses --> Re-review
|
|
1130
|
+
|
|
|
1131
|
+
v APPROVED
|
|
1132
|
+
Second Approval (for main branch)
|
|
1133
|
+
|
|
|
1134
|
+
v
|
|
1135
|
+
Merge (Squash commit to maintain clean history)
|
|
1136
|
+
|
|
|
1137
|
+
v
|
|
1138
|
+
Auto-deploy to staging (if main/develop)
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
---
|
|
1142
|
+
|
|
1143
|
+
## 11. CI/CD Pipeline Standards
|
|
1144
|
+
|
|
1145
|
+
### 11.1 Pipeline Stages
|
|
1146
|
+
|
|
1147
|
+
```
|
|
1148
|
+
CI PIPELINE (Runs on every PR and push to develop/main):
|
|
1149
|
+
1. Checkout + Setup Node.js + pnpm
|
|
1150
|
+
2. Cache dependencies
|
|
1151
|
+
3. pnpm install --frozen-lockfile
|
|
1152
|
+
4. pnpm lint (must pass)
|
|
1153
|
+
5. pnpm check-types (must pass)
|
|
1154
|
+
6. pnpm test:ci (>=80% coverage)
|
|
1155
|
+
7. Security audit (pnpm audit / npm audit)
|
|
1156
|
+
8. Build application
|
|
1157
|
+
9. Upload test results + coverage
|
|
1158
|
+
|
|
1159
|
+
CD PIPELINE (Push to develop):
|
|
1160
|
+
1. Build Docker image
|
|
1161
|
+
2. Push to container registry
|
|
1162
|
+
3. Deploy to Dev (auto)
|
|
1163
|
+
4. Run smoke tests
|
|
1164
|
+
|
|
1165
|
+
CD PIPELINE (Push to main + manual approval):
|
|
1166
|
+
1. Build Docker image
|
|
1167
|
+
2. Push to container registry
|
|
1168
|
+
3. Deploy to Staging (auto)
|
|
1169
|
+
4. E2E tests
|
|
1170
|
+
5. Manual approval gate
|
|
1171
|
+
6. Deploy to Production (rolling update)
|
|
1172
|
+
7. Health check + smoke tests
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
### 11.2 CI Configuration (GitHub Actions Example)
|
|
1176
|
+
|
|
1177
|
+
```yaml
|
|
1178
|
+
name: CI Pipeline
|
|
1179
|
+
on:
|
|
1180
|
+
push: { branches: [main, develop] }
|
|
1181
|
+
pull_request: { branches: [main, develop] }
|
|
1182
|
+
|
|
1183
|
+
jobs:
|
|
1184
|
+
quality:
|
|
1185
|
+
runs-on: ubuntu-latest
|
|
1186
|
+
steps:
|
|
1187
|
+
- uses: actions/checkout@v4
|
|
1188
|
+
- uses: actions/setup-node@v4
|
|
1189
|
+
with: { node-version: "20" }
|
|
1190
|
+
- uses: pnpm/action-setup@v2
|
|
1191
|
+
with: { version: 9 }
|
|
1192
|
+
|
|
1193
|
+
- run: pnpm install --frozen-lockfile
|
|
1194
|
+
- run: pnpm lint
|
|
1195
|
+
- run: pnpm check-types
|
|
1196
|
+
- run: pnpm test:ci
|
|
1197
|
+
- run: pnpm audit --audit-level=high
|
|
1198
|
+
- run: pnpm build
|
|
1199
|
+
|
|
1200
|
+
- uses: codecov/codecov-action@v4
|
|
1201
|
+
with:
|
|
1202
|
+
files: ./coverage/cobertura-coverage.xml
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
### 11.3 Non-Negotiables
|
|
1206
|
+
|
|
1207
|
+
1. Lockfile must be committed: Reproducible builds depend on it. **pnpm-lock.yaml** is the standard for CI/CD.
|
|
1208
|
+
2. Pipeline must fail on lint or test failures. No continueOnError.
|
|
1209
|
+
3. Security scanning required: npm audit or Snyk/Trivy.
|
|
1210
|
+
4. Docker images must be tagged with commit SHA.
|
|
1211
|
+
5. Production deployment requires manual approval.
|
|
1212
|
+
6. Database migrations must run before the app starts (if applicable).
|
|
1213
|
+
|
|
1214
|
+
---
|
|
1215
|
+
|
|
1216
|
+
## 12. Deployment Standards
|
|
1217
|
+
|
|
1218
|
+
### 12.1 Multi-Stage Dockerfile
|
|
1219
|
+
|
|
1220
|
+
```dockerfile
|
|
1221
|
+
FROM node:20-alpine AS deps
|
|
1222
|
+
WORKDIR /app
|
|
1223
|
+
COPY package.json pnpm-lock.yaml ./
|
|
1224
|
+
RUN pnpm install --frozen-lockfile
|
|
1225
|
+
|
|
1226
|
+
FROM node:20-alpine AS builder
|
|
1227
|
+
WORKDIR /app
|
|
1228
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
1229
|
+
COPY . .
|
|
1230
|
+
RUN pnpm build
|
|
1231
|
+
|
|
1232
|
+
FROM node:20-alpine AS runner
|
|
1233
|
+
WORKDIR /app
|
|
1234
|
+
ENV NODE_ENV=production
|
|
1235
|
+
RUN addgroup --system --gid 1001 nodejs
|
|
1236
|
+
RUN adduser --system --uid 1001 appuser
|
|
1237
|
+
COPY --from=builder /app/dist ./dist
|
|
1238
|
+
COPY --from=builder /app/package.json ./
|
|
1239
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
1240
|
+
USER appuser
|
|
1241
|
+
EXPOSE 3000
|
|
1242
|
+
CMD ["node", "dist/server.js"]
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
### 12.2 Environment Strategy
|
|
1246
|
+
|
|
1247
|
+
| Environment | Purpose | Deploy Trigger | Replicas |
|
|
1248
|
+
|-------------|---------|---------------|----------|
|
|
1249
|
+
| Dev | Developer testing | Push to develop | 1 |
|
|
1250
|
+
| Staging | QA + UAT | Push to main | 2 |
|
|
1251
|
+
| Production | Live | Manual approval | 3+ (auto-scale) |
|
|
1252
|
+
|
|
1253
|
+
### 12.3 Deployment Checklist
|
|
1254
|
+
|
|
1255
|
+
- [ ] Environment variables configured in secret manager (not committed)
|
|
1256
|
+
- [ ] Health check endpoint implemented (/health)
|
|
1257
|
+
- [ ] Readiness probe configured (Kubernetes)
|
|
1258
|
+
- [ ] Resource limits set (CPU/Memory)
|
|
1259
|
+
- [ ] Database migration plan prepared (if applicable)
|
|
1260
|
+
- [ ] Rollback plan documented
|
|
1261
|
+
- [ ] Monitoring alerts configured
|
|
1262
|
+
- [ ] Smoke tests passing
|
|
1263
|
+
|
|
1264
|
+
---
|
|
1265
|
+
|
|
1266
|
+
## 13. Security Standards
|
|
1267
|
+
|
|
1268
|
+
### 13.1 Mandatory Security Measures
|
|
1269
|
+
|
|
1270
|
+
| Measure | Implementation | Priority |
|
|
1271
|
+
|---------|---------------|----------|
|
|
1272
|
+
| HTTPS | Enforced at ingress level | Critical |
|
|
1273
|
+
| CSP Headers | Content Security Policy via server headers | Critical |
|
|
1274
|
+
| Input Validation | Zod schemas for all user inputs | Critical |
|
|
1275
|
+
| XSS Protection | React escaping + DOMPurify for raw HTML | Critical |
|
|
1276
|
+
| CSRF Protection | SameSite cookies + CSRF tokens | High |
|
|
1277
|
+
| Rate Limiting | API endpoints (especially auth) | High |
|
|
1278
|
+
| Dependency Scanning | npm audit / Snyk in CI | High |
|
|
1279
|
+
| Secret Scanning | GitLeaks / TruffleHog in CI | Medium |
|
|
1280
|
+
| CORS | Whitelist allowed origins | High |
|
|
1281
|
+
|
|
1282
|
+
### 13.2 Secrets NEVER Committed
|
|
1283
|
+
|
|
1284
|
+
- API keys, tokens, passwords
|
|
1285
|
+
- JWT signing secrets
|
|
1286
|
+
- Database connection strings
|
|
1287
|
+
- Cloud provider credentials
|
|
1288
|
+
- SSL/TLS private keys
|
|
1289
|
+
- Any .pem file
|
|
1290
|
+
|
|
1291
|
+
### 13.3 .env.example Template
|
|
1292
|
+
|
|
1293
|
+
```bash
|
|
1294
|
+
# App
|
|
1295
|
+
VITE_APP_TITLE=My App
|
|
1296
|
+
VITE_API_URL=http://localhost:3000/api/v1
|
|
1297
|
+
|
|
1298
|
+
# Auth (if applicable)
|
|
1299
|
+
VITE_AUTH_DOMAIN=your-domain.auth0.com
|
|
1300
|
+
VITE_AUTH_CLIENT_ID=your-client-id
|
|
1301
|
+
|
|
1302
|
+
# Monitoring
|
|
1303
|
+
VITE_SENTRY_DSN=https://xxx@sentry.io/xxx
|
|
1304
|
+
|
|
1305
|
+
# Feature Flags
|
|
1306
|
+
VITE_ENABLE_DARK_MODE=true
|
|
1307
|
+
VITE_ENABLE_NEW_DASHBOARD=false
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
---
|
|
1311
|
+
|
|
1312
|
+
### 13.4 Console.Log & Debugger Enforcement
|
|
1313
|
+
|
|
1314
|
+
Console and debugger rules: `console.log`, `console.debug`, `console.info`, `console.trace`, `console.table`, and `debugger` are banned in committed code.
|
|
1315
|
+
|
|
1316
|
+
Allowed only: `console.warn()` and `console.error()`. Use a structured logger instead.
|
|
1317
|
+
|
|
1318
|
+
ESLint rules:
|
|
1319
|
+
```
|
|
1320
|
+
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
1321
|
+
'no-debugger': 'error',
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
Structured logger replacement:
|
|
1325
|
+
```typescript
|
|
1326
|
+
const isDev = import.meta.env.DEV;
|
|
1327
|
+
|
|
1328
|
+
export const logger = {
|
|
1329
|
+
debug: (...args: unknown[]) => { if (isDev) console.debug('[DEBUG]', ...args); },
|
|
1330
|
+
warn: (...args: unknown[]) => console.warn('[WARN]', ...args),
|
|
1331
|
+
error: (...args: unknown[]) => console.error('[ERROR]', ...args),
|
|
1332
|
+
};
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
Production stripping (Vite/Terser):
|
|
1336
|
+
```typescript
|
|
1337
|
+
build: {
|
|
1338
|
+
minify: 'terser',
|
|
1339
|
+
terserOptions: {
|
|
1340
|
+
compress: {
|
|
1341
|
+
drop_console: true,
|
|
1342
|
+
drop_debugger: true,
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
}
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
---
|
|
1349
|
+
|
|
1350
|
+
---
|
|
1351
|
+
|
|
1352
|
+
### 13.5 API Response Masking (Network Tab Security)
|
|
1353
|
+
|
|
1354
|
+
Sensitive fields must NEVER appear in network responses visible in the browser Network tab.
|
|
1355
|
+
|
|
1356
|
+
Mask these data types:
|
|
1357
|
+
- Auth tokens (JWT, session IDs)
|
|
1358
|
+
- Passwords, password hashes, salts
|
|
1359
|
+
- PII (SSN, phone, address, email)
|
|
1360
|
+
- Financial data (card numbers, salaries)
|
|
1361
|
+
- Internal IDs (database IDs, internal references)
|
|
1362
|
+
- API keys, secrets, private keys
|
|
1363
|
+
|
|
1364
|
+
Strategy 1: Use public DTOs in service layer so the backend only returns safe fields.
|
|
1365
|
+
|
|
1366
|
+
Strategy 2: Global response interceptor masking sensitive fields.
|
|
1367
|
+
```typescript
|
|
1368
|
+
class ApiClient {
|
|
1369
|
+
private SENSITIVE_PATTERNS = [
|
|
1370
|
+
'password', 'passwordhash', 'salt',
|
|
1371
|
+
'ssn', 'socialsecurity', 'nationalid',
|
|
1372
|
+
'internalid', 'databaseid', 'legacyid',
|
|
1373
|
+
'creditcardnumber', 'cvv', 'bankaccount',
|
|
1374
|
+
'apikey', 'privatekey', 'token', 'sessionid',
|
|
1375
|
+
'salary', 'income', 'debt', 'revenue',
|
|
1376
|
+
];
|
|
1377
|
+
|
|
1378
|
+
constructor() {
|
|
1379
|
+
this.client.interceptors.response.use((response) => ({
|
|
1380
|
+
...response,
|
|
1381
|
+
data: this.maskSensitiveData(response.data),
|
|
1382
|
+
}));
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
private maskSensitiveData(data: unknown): unknown {
|
|
1386
|
+
if (typeof data !== 'object' || data === null) return data;
|
|
1387
|
+
if (Array.isArray(data)) return data.map(item => this.maskSensitiveData(item));
|
|
1388
|
+
const masked: Record<string, unknown> = {};
|
|
1389
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1390
|
+
const lowerKey = key.toLowerCase();
|
|
1391
|
+
if (this.SENSITIVE_PATTERNS.some(p => lowerKey.includes(p))) {
|
|
1392
|
+
masked[key] = '[REDACTED]';
|
|
1393
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
1394
|
+
masked[key] = this.maskSensitiveData(value);
|
|
1395
|
+
} else {
|
|
1396
|
+
masked[key] = value;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return masked;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
Strategy 3: Disable caching for sensitive endpoints.
|
|
1405
|
+
```typescript
|
|
1406
|
+
export const sensitiveService = {
|
|
1407
|
+
getFinancialData: () =>
|
|
1408
|
+
apiClient.get('/reports/financial', {
|
|
1409
|
+
headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' },
|
|
1410
|
+
}),
|
|
1411
|
+
};
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
Production verification checklist:
|
|
1415
|
+
- Open DevTools > Network tab
|
|
1416
|
+
- Verify no Authorization header visible (use httpOnly cookies)
|
|
1417
|
+
- Verify no passwords, SSNs, or internal IDs in request bodies
|
|
1418
|
+
- Verify masked responses show [REDACTED] for sensitive fields
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
### 13.6 Authorization: RBAC, ABAC, Guards, and DevMode Safety
|
|
1422
|
+
|
|
1423
|
+
Required authorization model: RBAC baseline, ABAC for policy restrictions. Enforced in route guards, component guards, and data/service guards. Backend must also enforce policy independently.
|
|
1424
|
+
|
|
1425
|
+
Core rules:
|
|
1426
|
+
- No feature, page, action, button, table row, or API call may be rendered or executed without an authorization check
|
|
1427
|
+
- Authorized state MUST NOT be derivable from the UI alone in developer mode
|
|
1428
|
+
- Authorization checks MUST be centralized
|
|
1429
|
+
|
|
1430
|
+
RBAC structure:
|
|
1431
|
+
```tsx
|
|
1432
|
+
export enum Role {
|
|
1433
|
+
SUPER_ADMIN = 'super_admin',
|
|
1434
|
+
TENANT_ADMIN = 'tenant_admin',
|
|
1435
|
+
MANAGER = 'manager',
|
|
1436
|
+
ANALYST = 'analyst',
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
export enum Permission {
|
|
1440
|
+
USERS_READ = 'users:read',
|
|
1441
|
+
USERS_WRITE = 'users:write',
|
|
1442
|
+
USERS_DELETE = 'users:delete',
|
|
1443
|
+
REPORTS_READ = 'reports:read',
|
|
1444
|
+
BILLING_READ = 'billing:read',
|
|
1445
|
+
BILLING_WRITE = 'billing:write',
|
|
1446
|
+
SETTINGS_WRITE = 'settings:write',
|
|
1447
|
+
}
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
ABAC structure:
|
|
1451
|
+
```tsx
|
|
1452
|
+
export type AbacContext = {
|
|
1453
|
+
role: Role;
|
|
1454
|
+
tenantId: string;
|
|
1455
|
+
userId: string;
|
|
1456
|
+
ownershipIds: string[];
|
|
1457
|
+
region: string;
|
|
1458
|
+
clearanceLevel: number;
|
|
1459
|
+
isImpersonating: boolean;
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
export type AbacPolicy = (
|
|
1463
|
+
ctx: AbacContext,
|
|
1464
|
+
action: string,
|
|
1465
|
+
resource: { type: string; id?: string; ownerId?: string; tenantId?: string }
|
|
1466
|
+
) => boolean;
|
|
1467
|
+
|
|
1468
|
+
export const canAccessResource: AbacPolicy = (ctx, action, resource) => {
|
|
1469
|
+
if (ctx.isImpersonating) return false;
|
|
1470
|
+
if (resource.tenantId && resource.tenantId !== ctx.tenantId) return false;
|
|
1471
|
+
if (resource.ownerId && !ctx.ownershipIds.includes(resource.ownerId)) return false;
|
|
1472
|
+
if (action.startsWith('admin:') && ctx.role !== Role.SUPER_ADMIN) return false;
|
|
1473
|
+
return true;
|
|
1474
|
+
};
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
Route guards:
|
|
1478
|
+
```tsx
|
|
1479
|
+
export function RequireAuth({ roles, permissions, abacCheck, children }: RequireAuthProps) {
|
|
1480
|
+
const auth = useAuth();
|
|
1481
|
+
const location = useLocation();
|
|
1482
|
+
if (!auth.isAuthenticated) return <Navigate to='/login' state={{ from: location }} replace />;
|
|
1483
|
+
const hasRole = roles ? roles.includes(auth.role) : true;
|
|
1484
|
+
const hasPermission = permissions ? permissions.every(p => auth.permissions.includes(p)) : true;
|
|
1485
|
+
const abacPass = abacCheck ? abacCheck(auth.abacContext) : true;
|
|
1486
|
+
if (!hasRole || !hasPermission || !abacPass) return <Forbidden />;
|
|
1487
|
+
return <>{children}</>;
|
|
1488
|
+
}
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
Component guards:
|
|
1492
|
+
```tsx
|
|
1493
|
+
export function Can({ roles, permissions, abacCheck, fallback = null, children }: CanProps) {
|
|
1494
|
+
const auth = useAuth();
|
|
1495
|
+
const hasRole = roles ? roles.includes(auth.role) : true;
|
|
1496
|
+
const hasPermission = permissions ? permissions.some(p => auth.permissions.includes(p)) : true;
|
|
1497
|
+
const abacPass = abacCheck ? abacCheck(auth.abacContext) : true;
|
|
1498
|
+
if (!hasRole || !hasPermission || !abacPass) return <>{fallback}</>;
|
|
1499
|
+
return <>{children}</>;
|
|
1500
|
+
}
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
Data guards:
|
|
1504
|
+
```tsx
|
|
1505
|
+
export function useAuthorizedUser(id: string) {
|
|
1506
|
+
const auth = useAuth();
|
|
1507
|
+
return useQuery({
|
|
1508
|
+
queryKey: ['user', id],
|
|
1509
|
+
queryFn: async () => {
|
|
1510
|
+
const data = await usersService.getById(id);
|
|
1511
|
+
if (!canAccessResource(auth.abacContext, 'users:read', { type: 'user', id, tenantId: data.tenantId })) {
|
|
1512
|
+
throw new Error('Forbidden');
|
|
1513
|
+
}
|
|
1514
|
+
return data;
|
|
1515
|
+
},
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
DevMode safety:
|
|
1521
|
+
- Dev-only endpoints, hidden buttons, debug flags MUST NOT bypass authorization
|
|
1522
|
+
- Hardcoded ADMIN=true query params, secret URL paths MUST be treated as security bugs
|
|
1523
|
+
- Impersonation and elevated roles MUST be logged and auditable
|
|
1524
|
+
|
|
1525
|
+
ESLint enforcement:
|
|
1526
|
+
```
|
|
1527
|
+
no-restricted-syntax: ['error', {
|
|
1528
|
+
selector: "MemberExpression[property.name='role']",
|
|
1529
|
+
message: 'Use <Can> or useAuthorized* guards instead of inline role checks.',
|
|
1530
|
+
}]
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
Authorization code review checklist:
|
|
1534
|
+
- Every protected route uses RequireAuth
|
|
1535
|
+
- Every sensitive action uses Can
|
|
1536
|
+
- ABAC policies for tenant, ownership, region, or time restrictions
|
|
1537
|
+
- No inline role/permission checks in JSX
|
|
1538
|
+
- No hidden dev paths bypassing authorization
|
|
1539
|
+
- No mock authorization used outside tests
|
|
1540
|
+
## 14. Monitoring & Observability
|
|
1541
|
+
|
|
1542
|
+
### 14.1 Required Instrumentation
|
|
1543
|
+
|
|
1544
|
+
| Tool | Purpose | Implementation |
|
|
1545
|
+
|------|---------|---------------|
|
|
1546
|
+
| Sentry | Error tracking (frontend) | @sentry/react |
|
|
1547
|
+
| Lighthouse CI | Performance budget | GitHub Action |
|
|
1548
|
+
| Console Logging | Structured logging | No console.log in production |
|
|
1549
|
+
|
|
1550
|
+
### 14.2 Performance Budget
|
|
1551
|
+
|
|
1552
|
+
| Metric | Budget | Enforcement |
|
|
1553
|
+
|--------|--------|-------------|
|
|
1554
|
+
| First Contentful Paint (FCP) | < 1.5s | Lighthouse CI |
|
|
1555
|
+
| Largest Contentful Paint (LCP) | < 2.5s | Lighthouse CI |
|
|
1556
|
+
| Time to Interactive (TTI) | < 3.5s | Lighthouse CI |
|
|
1557
|
+
| Bundle Size (initial) | < 200KB gzipped | size-limit package |
|
|
1558
|
+
|
|
1559
|
+
---
|
|
1560
|
+
|
|
1561
|
+
## 15. Documentation Standards
|
|
1562
|
+
|
|
1563
|
+
### 15.1 Minimum Required Docs
|
|
1564
|
+
|
|
1565
|
+
| File | Purpose | Required |
|
|
1566
|
+
|------|---------|----------|
|
|
1567
|
+
| README.md | Project overview, setup, commands | Yes |
|
|
1568
|
+
| CONTRIBUTING.md | How to contribute, coding standards | Yes |
|
|
1569
|
+
| .env.example | All required environment variables | Yes |
|
|
1570
|
+
| docs/architecture.md | High-level architecture decisions | For complex projects |
|
|
1571
|
+
| docs/onboarding.md | Setup checklist for new developers | For monorepos |
|
|
1572
|
+
|
|
1573
|
+
### 15.2 README Template
|
|
1574
|
+
|
|
1575
|
+
```markdown
|
|
1576
|
+
# Project Name
|
|
1577
|
+
[Brief description]
|
|
1578
|
+
|
|
1579
|
+
## Quick Start
|
|
1580
|
+
```bash
|
|
1581
|
+
git clone <repo>
|
|
1582
|
+
cd project
|
|
1583
|
+
pnpm install
|
|
1584
|
+
cp .env.example .env
|
|
1585
|
+
pnpm dev
|
|
1586
|
+
```
|
|
1587
|
+
|
|
1588
|
+
## Available Scripts
|
|
1589
|
+
| Command | Description |
|
|
1590
|
+
| pnpm dev | Start development server |
|
|
1591
|
+
| pnpm build | Build for production |
|
|
1592
|
+
| pnpm test | Run tests |
|
|
1593
|
+
| pnpm lint | Lint code |
|
|
1594
|
+
|
|
1595
|
+
## Environment Variables
|
|
1596
|
+
[Table of required env vars]
|
|
1597
|
+
```
|
|
1598
|
+
|
|
1599
|
+
---
|
|
1600
|
+
|
|
1601
|
+
## 16. Appendix: Quick References
|
|
1602
|
+
|
|
1603
|
+
### 16.1 Essential Scripts
|
|
1604
|
+
|
|
1605
|
+
```bash
|
|
1606
|
+
pnpm dev # Start dev server with HMR
|
|
1607
|
+
pnpm build # Production build
|
|
1608
|
+
pnpm test # Run unit tests
|
|
1609
|
+
pnpm test:e2e # Run E2E tests
|
|
1610
|
+
pnpm lint # Lint all files
|
|
1611
|
+
pnpm format # Format with Prettier
|
|
1612
|
+
pnpm check-types # TypeScript type checking
|
|
1613
|
+
pnpm audit # Security audit
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
### 16.2 File Naming Reference
|
|
1617
|
+
|
|
1618
|
+
```
|
|
1619
|
+
UserProfile.tsx # React component (PascalCase)
|
|
1620
|
+
use-user-data.ts # Custom hook (kebab-case)
|
|
1621
|
+
user-service.ts # Service/utility (kebab-case)
|
|
1622
|
+
UserProfile.test.tsx # Test file (matches component)
|
|
1623
|
+
UserProfile.stories.tsx # Storybook file (matches component)
|
|
1624
|
+
types/user.ts # Type definitions
|
|
1625
|
+
constants/auth.ts # Constants
|
|
1626
|
+
```
|
|
1627
|
+
|
|
1628
|
+
### 16.3 Key Config Files
|
|
1629
|
+
|
|
1630
|
+
| File | Purpose |
|
|
1631
|
+
|------|---------|
|
|
1632
|
+
| tsconfig.json | TypeScript configuration |
|
|
1633
|
+
| vite.config.ts | Vite build configuration |
|
|
1634
|
+
| eslint.config.js | ESLint configuration |
|
|
1635
|
+
| .prettierrc | Prettier formatting rules |
|
|
1636
|
+
| .husky/pre-commit | Pre-commit hook (lint-staged) |
|
|
1637
|
+
| commitlint.config.js | Conventional commit rules |
|
|
1638
|
+
| .github/workflows/ci.yml | CI pipeline |
|
|
1639
|
+
| .github/workflows/cd.yml | CD pipeline |
|
|
1640
|
+
| Dockerfile | Container build |
|
|
1641
|
+
| docker-compose.yml | Local development |
|
|
1642
|
+
| .env.example | Documented env variables |
|
|
1643
|
+
|
|
1644
|
+
### 16.4 Package.json Scripts Template
|
|
1645
|
+
|
|
1646
|
+
```json
|
|
1647
|
+
{
|
|
1648
|
+
"scripts": {
|
|
1649
|
+
"dev": "vite",
|
|
1650
|
+
"build": "tsc --noEmit && vite build",
|
|
1651
|
+
"preview": "vite preview",
|
|
1652
|
+
"test": "vitest run",
|
|
1653
|
+
"test:watch": "vitest",
|
|
1654
|
+
"test:ci": "vitest run --coverage",
|
|
1655
|
+
"test:e2e": "playwright test",
|
|
1656
|
+
"lint": "eslint src/ --ext .ts,.tsx --max-warnings 0",
|
|
1657
|
+
"format": "prettier --write src/**/*.{ts,tsx,css,json}",
|
|
1658
|
+
"format:check": "prettier --check src/**/*.{ts,tsx,css,json}",
|
|
1659
|
+
"check-types": "tsc --noEmit",
|
|
1660
|
+
"prepare": "husky",
|
|
1661
|
+
"audit": "pnpm audit --audit-level=high"
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
```
|
|
1665
|
+
|
|
1666
|
+
---
|
|
1667
|
+
|
|
1668
|
+
---
|
|
1669
|
+
|
|
1670
|
+
## 17. API Service Layer Standards
|
|
1671
|
+
|
|
1672
|
+
### 17.1 Why a Services Layer?
|
|
1673
|
+
|
|
1674
|
+
A dedicated **API service layer** separates all backend communication from UI components. This is critical for:
|
|
1675
|
+
|
|
1676
|
+
- **Consistency**: All API calls go through the same error handling, auth headers, and response parsing
|
|
1677
|
+
- **Testability**: Service functions can be mocked independently of components
|
|
1678
|
+
- **Caching**: Centralized caching and deduplication of requests
|
|
1679
|
+
- **Performance**: Request batching, debouncing, and cancellation handled in one place
|
|
1680
|
+
- **Maintainability**: Backend URL changes require one file change, not hundreds
|
|
1681
|
+
|
|
1682
|
+
### 17.2 Recommended Architecture
|
|
1683
|
+
|
|
1684
|
+
```
|
|
1685
|
+
src/
|
|
1686
|
+
services/ # API service layer (domain-modularized)
|
|
1687
|
+
api-client.ts # Axios/fetch instance with interceptors (ONLY file at root)
|
|
1688
|
+
auth/ # Auth domain
|
|
1689
|
+
index.ts # Barrel: re-exports all auth services
|
|
1690
|
+
auth.service.ts # Auth-related API calls
|
|
1691
|
+
users/ # Users domain
|
|
1692
|
+
index.ts # Barrel: re-exports all user services
|
|
1693
|
+
users.service.ts # User-related API calls
|
|
1694
|
+
employee/ # Employee domain
|
|
1695
|
+
index.ts # Barrel: re-exports all employee services
|
|
1696
|
+
employee-bio-detail.service.ts # Bio detail API calls
|
|
1697
|
+
employee-bank-detail.service.ts# Bank detail API calls
|
|
1698
|
+
employee-leave.service.ts # Leave management API calls
|
|
1699
|
+
hooks/ # React hooks that consume services
|
|
1700
|
+
use-debounce.ts # Utility hook (shared, allowed at root)
|
|
1701
|
+
users/ # User domain hooks
|
|
1702
|
+
index.ts # Barrel
|
|
1703
|
+
use-users.ts # User data hook using React Query + service
|
|
1704
|
+
employee/ # Employee domain hooks
|
|
1705
|
+
index.ts # Barrel
|
|
1706
|
+
use-employee-bio.ts # Employee bio hook
|
|
1707
|
+
components/ # UI components (no direct API calls)
|
|
1708
|
+
```
|
|
1709
|
+
|
|
1710
|
+
### 17.3 API Client Template
|
|
1711
|
+
|
|
1712
|
+
```typescript
|
|
1713
|
+
import axios, { AxiosError, AxiosInstance } from 'axios';
|
|
1714
|
+
|
|
1715
|
+
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api/v1';
|
|
1716
|
+
|
|
1717
|
+
class ApiClient {
|
|
1718
|
+
private client: AxiosInstance;
|
|
1719
|
+
|
|
1720
|
+
constructor() {
|
|
1721
|
+
this.client = axios.create({
|
|
1722
|
+
baseURL: API_BASE_URL,
|
|
1723
|
+
timeout: 30000,
|
|
1724
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1725
|
+
});
|
|
1726
|
+
this.setupInterceptors();
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
private setupInterceptors() {
|
|
1730
|
+
this.client.interceptors.request.use((config) => {
|
|
1731
|
+
const token = getAccessToken();
|
|
1732
|
+
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
1733
|
+
return config;
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
this.client.interceptors.response.use(
|
|
1737
|
+
(res) => res,
|
|
1738
|
+
(error: AxiosError) => {
|
|
1739
|
+
if (error.response?.status === 401) window.location.href = '/login';
|
|
1740
|
+
return Promise.reject(this.normalizeError(error));
|
|
1741
|
+
}
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
private normalizeError(error: AxiosError) {
|
|
1746
|
+
return {
|
|
1747
|
+
message: error.response?.data?.message || error.message,
|
|
1748
|
+
status: error.response?.status || 500,
|
|
1749
|
+
code: error.response?.data?.code || 'UNKNOWN_ERROR',
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
async get<T>(url: string): Promise<T> {
|
|
1754
|
+
const res = await this.client.get<T>(url);
|
|
1755
|
+
return res.data;
|
|
1756
|
+
}
|
|
1757
|
+
async post<T>(url: string, data?: unknown): Promise<T> {
|
|
1758
|
+
const res = await this.client.post<T>(url, data);
|
|
1759
|
+
return res.data;
|
|
1760
|
+
}
|
|
1761
|
+
async put<T>(url: string, data?: unknown): Promise<T> {
|
|
1762
|
+
const res = await this.client.put<T>(url, data);
|
|
1763
|
+
return res.data;
|
|
1764
|
+
}
|
|
1765
|
+
async delete<T>(url: string): Promise<T> {
|
|
1766
|
+
const res = await this.client.delete<T>(url);
|
|
1767
|
+
return res.data;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
export const apiClient = new ApiClient();
|
|
1772
|
+
```
|
|
1773
|
+
|
|
1774
|
+
### 17.4 Service Module Template (Domain-Modularized with Barrel Export)
|
|
1775
|
+
|
|
1776
|
+
Each service file lives inside its domain subdirectory and is re-exported via the barrel `index.ts`.
|
|
1777
|
+
|
|
1778
|
+
```typescript
|
|
1779
|
+
// src/services/users/users.service.ts
|
|
1780
|
+
import { apiClient } from '@/services/api-client';
|
|
1781
|
+
import type { User, PaginatedResponse } from '@/types';
|
|
1782
|
+
|
|
1783
|
+
const ENDPOINT = '/users';
|
|
1784
|
+
|
|
1785
|
+
export const usersService = {
|
|
1786
|
+
getAll: (page = 1, limit = 20) =>
|
|
1787
|
+
apiClient.get<PaginatedResponse<User>>(`${ENDPOINT}?page=${page}&limit=${limit}`),
|
|
1788
|
+
getById: (id: string) => apiClient.get<User>(`${ENDPOINT}/${id}`),
|
|
1789
|
+
create: (data: Partial<User>) => apiClient.post<User>(ENDPOINT, data),
|
|
1790
|
+
update: (id: string, data: Partial<User>) => apiClient.put<User>(`${ENDPOINT}/${id}`, data),
|
|
1791
|
+
delete: (id: string) => apiClient.delete<void>(`${ENDPOINT}/${id}`),
|
|
1792
|
+
};
|
|
1793
|
+
```
|
|
1794
|
+
|
|
1795
|
+
```typescript
|
|
1796
|
+
// src/services/users/index.ts (BARREL FILE -- REQUIRED)
|
|
1797
|
+
export { usersService } from './users.service';
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
```typescript
|
|
1801
|
+
// src/services/employee/employee-bio-detail.service.ts
|
|
1802
|
+
import { apiClient } from '@/services/api-client';
|
|
1803
|
+
import type { EmployeeBio } from '@/types';
|
|
1804
|
+
|
|
1805
|
+
const ENDPOINT = '/employees';
|
|
1806
|
+
|
|
1807
|
+
export const employeeBioDetailService = {
|
|
1808
|
+
getBio: (employeeId: string) =>
|
|
1809
|
+
apiClient.get<EmployeeBio>(`${ENDPOINT}/${employeeId}/bio`),
|
|
1810
|
+
updateBio: (employeeId: string, data: Partial<EmployeeBio>) =>
|
|
1811
|
+
apiClient.put<EmployeeBio>(`${ENDPOINT}/${employeeId}/bio`, data),
|
|
1812
|
+
};
|
|
1813
|
+
```
|
|
1814
|
+
|
|
1815
|
+
```typescript
|
|
1816
|
+
// src/services/employee/index.ts (BARREL FILE -- REQUIRED)
|
|
1817
|
+
export { employeeBioDetailService } from './employee-bio-detail.service';
|
|
1818
|
+
export { employeeBankDetailService } from './employee-bank-detail.service';
|
|
1819
|
+
export { employeeLeaveService } from './employee-leave.service';
|
|
1820
|
+
```
|
|
1821
|
+
|
|
1822
|
+
**Consumer imports** (pages/components import from the barrel only):
|
|
1823
|
+
|
|
1824
|
+
```typescript
|
|
1825
|
+
// pages/EmployeeProfile.tsx -- GOOD (barrel import)
|
|
1826
|
+
import { employeeBioDetailService } from '@/services/employee';
|
|
1827
|
+
import { usersService } from '@/services/users';
|
|
1828
|
+
|
|
1829
|
+
// pages/EmployeeProfile.tsx -- BAD (deep import -- BLOCKED by ESLint)
|
|
1830
|
+
import { employeeBioDetailService } from '@/services/employee/employee-bio-detail.service';
|
|
1831
|
+
```
|
|
1832
|
+
|
|
1833
|
+
### 17.5 Hook That Consumes a Service (With React Query + Barrel Imports)
|
|
1834
|
+
|
|
1835
|
+
```typescript
|
|
1836
|
+
// src/hooks/users/use-users.ts
|
|
1837
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
1838
|
+
import { usersService } from '@/services/users';
|
|
1839
|
+
// NOT: import { usersService } from '@/services/users/users.service'; // BLOCKED
|
|
1840
|
+
|
|
1841
|
+
// Query key factory for consistent cache management
|
|
1842
|
+
export const userKeys = {
|
|
1843
|
+
all: ['users'] as const,
|
|
1844
|
+
lists: () => [...userKeys.all, 'list'] as const,
|
|
1845
|
+
list: (filters: object) => [...userKeys.lists(), filters] as const,
|
|
1846
|
+
details: () => [...userKeys.all, 'detail'] as const,
|
|
1847
|
+
detail: (id: string) => [...userKeys.details(), id] as const,
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
// Fetch paginated users (auto-cached + stale-while-revalidate)
|
|
1851
|
+
export function useUsers(page = 1) {
|
|
1852
|
+
return useQuery({
|
|
1853
|
+
queryKey: userKeys.list({ page }),
|
|
1854
|
+
queryFn: () => usersService.getAll(page),
|
|
1855
|
+
staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
|
|
1856
|
+
gcTime: 30 * 60 * 1000, // Keep cache for 30 minutes
|
|
1857
|
+
refetchOnWindowFocus: true,
|
|
1858
|
+
retry: 3,
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Create user with automatic cache invalidation
|
|
1863
|
+
export function useCreateUser() {
|
|
1864
|
+
const queryClient = useQueryClient();
|
|
1865
|
+
return useMutation({
|
|
1866
|
+
mutationFn: usersService.create,
|
|
1867
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: userKeys.lists() }),
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
```
|
|
1871
|
+
|
|
1872
|
+
### 17.6 Client-Side Caching Strategy
|
|
1873
|
+
|
|
1874
|
+
| Cache Layer | Technology | Use Case | Persistence |
|
|
1875
|
+
|-------------|-----------|----------|-------------|
|
|
1876
|
+
| Server State | TanStack React Query | API caching, dedup, background refetch | In-memory |
|
|
1877
|
+
| Persistent Storage | localStorage | User preferences, theme, auth tokens | Disk |
|
|
1878
|
+
| Large Data | IndexedDB via Dexie.js | Offline data, large datasets | Disk |
|
|
1879
|
+
| Route Prefetch | Router loaders | Anticipatory data loading | In-memory |
|
|
1880
|
+
| Service Worker | Workbox | Full offline support | Cache Storage API |
|
|
1881
|
+
|
|
1882
|
+
**Rules**:
|
|
1883
|
+
- Never cache sensitive data (tokens, PII) in localStorage without encryption
|
|
1884
|
+
- Always set staleTime / gcTime for API responses
|
|
1885
|
+
- Invalidate caches on mutations (create, update, delete)
|
|
1886
|
+
- Prefer React Query over manual fetch + useState for server data
|
|
1887
|
+
- Use React Query staleTime to control refetch frequency, not arbitrary timers
|
|
1888
|
+
|
|
1889
|
+
---
|
|
1890
|
+
|
|
1891
|
+
## 18. React Hooks Best Practices
|
|
1892
|
+
|
|
1893
|
+
### 18.1 The Golden Rule
|
|
1894
|
+
|
|
1895
|
+
> **Hooks are for behavior, not data.** If data comes from an API, use React Query + your service layer. Reserve hooks for:
|
|
1896
|
+
> - Connecting UI to data (React Query hooks)
|
|
1897
|
+
> - Encapsulating complex browser APIs (resize, scroll, geolocation)
|
|
1898
|
+
> - Reusable form logic
|
|
1899
|
+
> - Performance optimization (useMemo, useCallback)
|
|
1900
|
+
|
|
1901
|
+
### 18.2 useEffect -- When and How
|
|
1902
|
+
|
|
1903
|
+
**DO use useEffect for**:
|
|
1904
|
+
- Synchronizing with external systems (WebSocket, browser APIs, analytics)
|
|
1905
|
+
- Setting up subscriptions with proper cleanup
|
|
1906
|
+
- Managing non-React state (syncing to localStorage)
|
|
1907
|
+
- Imperative operations (focus management, scroll restoration)
|
|
1908
|
+
|
|
1909
|
+
**DO NOT use useEffect for**:
|
|
1910
|
+
- Fetching data on mount --> Use React Query
|
|
1911
|
+
- Deriving state from props --> Use useMemo
|
|
1912
|
+
- Responding to user actions --> Use event handlers directly
|
|
1913
|
+
- Computed values --> Use useMemo or inline computation
|
|
1914
|
+
|
|
1915
|
+
```typescript
|
|
1916
|
+
// GOOD: Clean subscription with cleanup
|
|
1917
|
+
useEffect(() => {
|
|
1918
|
+
const socket = new WebSocket(url);
|
|
1919
|
+
socket.onmessage = (event) => setMessages((prev) => [...prev, JSON.parse(event.data)]);
|
|
1920
|
+
return () => socket.close(); // Cleanup on unmount
|
|
1921
|
+
}, [url]);
|
|
1922
|
+
|
|
1923
|
+
// BAD: Fetching data manually (use React Query instead)
|
|
1924
|
+
useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers); }, []);
|
|
1925
|
+
|
|
1926
|
+
// BAD: Deriving state from props
|
|
1927
|
+
useEffect(() => { setFullName(`${firstName} ${lastName}`); }, [firstName, lastName]);
|
|
1928
|
+
// FIX: const fullName = `${firstName} ${lastName}`;
|
|
1929
|
+
```
|
|
1930
|
+
|
|
1931
|
+
### 18.3 useMemo -- When to Memoize
|
|
1932
|
+
|
|
1933
|
+
**DO use useMemo for**:
|
|
1934
|
+
- Expensive computations (filtering/sorting large lists, complex data transformation)
|
|
1935
|
+
- Creating stable object references for child component props (prevents re-renders)
|
|
1936
|
+
|
|
1937
|
+
**DO NOT use useMemo for**:
|
|
1938
|
+
- Simple calculations (firstName + lastName)
|
|
1939
|
+
- Values used once or in a single render
|
|
1940
|
+
- Premature optimization -- measure first, then memoize
|
|
1941
|
+
|
|
1942
|
+
```typescript
|
|
1943
|
+
// GOOD: Expensive computation memoized
|
|
1944
|
+
const filteredUsers = useMemo(() => {
|
|
1945
|
+
return users
|
|
1946
|
+
.filter((u) => u.role === selectedRole)
|
|
1947
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1948
|
+
}, [users, selectedRole]);
|
|
1949
|
+
|
|
1950
|
+
// GOOD: Stable object reference for child
|
|
1951
|
+
const config = useMemo(() => ({ pageSize: 20, enableSorting: true }), []);
|
|
1952
|
+
|
|
1953
|
+
// BAD: Simple operation -- adds overhead
|
|
1954
|
+
const greeting = useMemo(() => `Hello, ${name}!`, [name]);
|
|
1955
|
+
// Just write: const greeting = `Hello, ${name}!`;
|
|
1956
|
+
```
|
|
1957
|
+
|
|
1958
|
+
### 18.4 useCallback -- When to Memoize Functions
|
|
1959
|
+
|
|
1960
|
+
**DO use useCallback for**:
|
|
1961
|
+
- Callback props passed to memoized children (React.memo)
|
|
1962
|
+
- Functions in useEffect dependency arrays
|
|
1963
|
+
- Functions passed to context providers
|
|
1964
|
+
|
|
1965
|
+
**DO NOT use useCallback for**:
|
|
1966
|
+
- Event handlers on native HTML elements (<button onClick>)
|
|
1967
|
+
- Functions used in a single parent component with no memoized children
|
|
1968
|
+
- Premature optimization
|
|
1969
|
+
|
|
1970
|
+
```typescript
|
|
1971
|
+
// GOOD: Stable callback for memoized child
|
|
1972
|
+
const Table = React.memo(({ onRowClick, data }) => { ... });
|
|
1973
|
+
function Parent() {
|
|
1974
|
+
const onRowClick = useCallback((id: string) => handleSelect(id), [handleSelect]);
|
|
1975
|
+
return <Table onRowClick={onRowClick} data={data} />;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// BAD: Unnecessary -- native elements don't check prop equality
|
|
1979
|
+
const handleClick = useCallback(() => setCount((c) => c + 1), []);
|
|
1980
|
+
// <button onClick={handleClick}> -- NO BENEFIT
|
|
1981
|
+
|
|
1982
|
+
// BAD: useCallback + inline function = same as no memo
|
|
1983
|
+
const handleSubmit = useCallback(() => { submit(data); }, [data]);
|
|
1984
|
+
// Same as: const handleSubmit = () => { submit(data); };
|
|
1985
|
+
```
|
|
1986
|
+
|
|
1987
|
+
### 18.5 useTransition & useDeferredValue -- For Responsive UIs
|
|
1988
|
+
|
|
1989
|
+
These hooks prevent the UI from freezing during expensive state updates.
|
|
1990
|
+
|
|
1991
|
+
```typescript
|
|
1992
|
+
// useTransition: Mark a state update as non-urgent
|
|
1993
|
+
function SearchPage() {
|
|
1994
|
+
const [isPending, startTransition] = useTransition();
|
|
1995
|
+
const [query, setQuery] = useState('');
|
|
1996
|
+
const [results, setResults] = useState([]);
|
|
1997
|
+
|
|
1998
|
+
const handleChange = (e) => {
|
|
1999
|
+
const value = e.target.value;
|
|
2000
|
+
setQuery(value); // Urgent: input must feel responsive
|
|
2001
|
+
startTransition(() => {
|
|
2002
|
+
setResults(filterHugeList(value)); // Deferred: can wait
|
|
2003
|
+
});
|
|
2004
|
+
};
|
|
2005
|
+
|
|
2006
|
+
return (
|
|
2007
|
+
<div>
|
|
2008
|
+
<input value={query} onChange={handleChange} />
|
|
2009
|
+
{isPending ? <Spinner /> : <List items={results} />}
|
|
2010
|
+
</div>
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// useDeferredValue: Defer re-render of non-critical content
|
|
2015
|
+
function Dashboard() {
|
|
2016
|
+
const [input, setInput] = useState('');
|
|
2017
|
+
const deferredInput = useDeferredValue(input);
|
|
2018
|
+
const isStale = input !== deferredInput;
|
|
2019
|
+
|
|
2020
|
+
return (
|
|
2021
|
+
<div>
|
|
2022
|
+
<input value={input} onChange={(e) => setInput(e.target.value)} />
|
|
2023
|
+
<SlowList text={deferredInput} /> {/* Re-renders less often */}
|
|
2024
|
+
{isStale && <div>Updating...</div>}
|
|
2025
|
+
</div>
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
```
|
|
2029
|
+
|
|
2030
|
+
**When to use**:
|
|
2031
|
+
- Search fields with large result sets
|
|
2032
|
+
- Tab switching with heavy content
|
|
2033
|
+
- Chart/table rendering blocking the UI thread
|
|
2034
|
+
- Filtering/sorting lists >10,000 items
|
|
2035
|
+
|
|
2036
|
+
### 18.6 Custom Hooks for Performance
|
|
2037
|
+
|
|
2038
|
+
```typescript
|
|
2039
|
+
// hooks/use-debounce.ts -- Debounce rapidly changing values
|
|
2040
|
+
export function useDebounce<T>(value: T, delay = 300): T {
|
|
2041
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
2042
|
+
useEffect(() => {
|
|
2043
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
2044
|
+
return () => clearTimeout(timer);
|
|
2045
|
+
}, [value, delay]);
|
|
2046
|
+
return debouncedValue;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// Usage: Reduces API calls during rapid typing
|
|
2050
|
+
function SearchUsers() {
|
|
2051
|
+
const [search, setSearch] = useState('');
|
|
2052
|
+
const debouncedSearch = useDebounce(search, 400); // Wait 400ms after last keystroke
|
|
2053
|
+
|
|
2054
|
+
const { data, isLoading } = useQuery({
|
|
2055
|
+
queryKey: ['users', 'search', debouncedSearch],
|
|
2056
|
+
queryFn: () => usersService.search(debouncedSearch),
|
|
2057
|
+
enabled: debouncedSearch.length >= 2, // Only search when 2+ chars
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
return (
|
|
2061
|
+
<div>
|
|
2062
|
+
<input value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
2063
|
+
{isLoading ? <Spinner /> : <UserList users={data} />}
|
|
2064
|
+
</div>
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
```
|
|
2068
|
+
|
|
2069
|
+
### 18.7 Performance Decision Guide
|
|
2070
|
+
|
|
2071
|
+
| Situation | Solution | Why |
|
|
2072
|
+
|-----------|----------|-----|
|
|
2073
|
+
| Fetching API data | React Query (useQuery) | Built-in caching, dedup, retry, stale-while-revalidate |
|
|
2074
|
+
| Mutating server data | React Query (useMutation) | Optimistic updates, cache invalidation, rollback |
|
|
2075
|
+
| Expensive calculation on props change | useMemo | Only re-computes when dependencies change |
|
|
2076
|
+
| Stable callback for memoized child | useCallback | Prevents unnecessary child re-renders |
|
|
2077
|
+
| Debouncing user input | Custom useDebounce hook | Reduces API calls during rapid typing |
|
|
2078
|
+
| Non-urgent state update | useTransition | Keeps UI responsive during heavy renders |
|
|
2079
|
+
| Deferring slow component re-render | useDeferredValue | Shows stale content while new renders |
|
|
2080
|
+
| Subscribing to browser API | useEffect with cleanup | Proper lifecycle management |
|
|
2081
|
+
| Form state | React Hook Form | Built-in validation, minimal re-renders |
|
|
2082
|
+
| Global state | Zustand | No providers, selective subscriptions |
|
|
2083
|
+
|
|
2084
|
+
### 18.8 Common Anti-Patterns to Avoid
|
|
2085
|
+
|
|
2086
|
+
```typescript
|
|
2087
|
+
// ANTI-PATTERN: State + useEffect for derived data
|
|
2088
|
+
const [fullName, setFullName] = useState('');
|
|
2089
|
+
useEffect(() => { setFullName(`${user.firstName} ${user.lastName}`); }, [user]);
|
|
2090
|
+
// FIX: const fullName = `${user.firstName} ${user.lastName}`;
|
|
2091
|
+
|
|
2092
|
+
// ANTI-PATTERN: Fetching in useEffect without cleanup
|
|
2093
|
+
useEffect(() => { fetch(url).then(setData); }, [url]);
|
|
2094
|
+
// FIX: Use React Query or AbortController
|
|
2095
|
+
|
|
2096
|
+
// ANTI-PATTERN: Object/array in useEffect deps (causes infinite loop)
|
|
2097
|
+
useEffect(() => { doSomething(); }, [{ id: user.id }]);
|
|
2098
|
+
// FIX: Use primitive deps: [user.id]
|
|
2099
|
+
|
|
2100
|
+
// ANTI-PATTERN: useCallback everywhere
|
|
2101
|
+
const handleChange = useCallback((e) => setValue(e.target.value), []);
|
|
2102
|
+
// FIX: const handleChange = (e) => setValue(e.target.value);
|
|
2103
|
+
|
|
2104
|
+
// ANTI-PATTERN: Nested ternaries in JSX
|
|
2105
|
+
return (<div>{isLoading ? <Spin/> : isError ? <Error/> : data.length === 0 ? <Empty/> : <List data={data}/>}</div>);
|
|
2106
|
+
// FIX: Extract into variables or early returns
|
|
2107
|
+
|
|
2108
|
+
// ANTI-PATTERN: Giant useEffect doing everything
|
|
2109
|
+
useEffect(() => {
|
|
2110
|
+
fetchUsers(); setupWebSocket(); initAnalytics(); setInterval(poll, 5000);
|
|
2111
|
+
return () => { cleanupAll(); };
|
|
2112
|
+
}, []);
|
|
2113
|
+
// FIX: Split into multiple focused useEffect calls
|
|
2114
|
+
```
|
|
2115
|
+
|
|
2116
|
+
### 18.9 Decision Flowchart
|
|
2117
|
+
|
|
2118
|
+
```
|
|
2119
|
+
Is this server data (API call)?
|
|
2120
|
+
|-- YES --> React Query (useQuery / useMutation)
|
|
2121
|
+
NO
|
|
2122
|
+
|
|
|
2123
|
+
Is this a computed/derived value?
|
|
2124
|
+
|-- YES --> Is it expensive (>1ms)?
|
|
2125
|
+
| |-- YES --> useMemo
|
|
2126
|
+
| |-- NO --> Compute at render
|
|
2127
|
+
NO
|
|
2128
|
+
|
|
|
2129
|
+
Is this a function passed as prop to memoized child?
|
|
2130
|
+
|-- YES --> useCallback
|
|
2131
|
+
|-- NO --> Regular function
|
|
2132
|
+
NO
|
|
2133
|
+
|
|
|
2134
|
+
Is this a side effect (subscription, browser API)?
|
|
2135
|
+
|-- YES --> useEffect + cleanup
|
|
2136
|
+
NO
|
|
2137
|
+
|
|
|
2138
|
+
Is the UI freezing during state updates?
|
|
2139
|
+
|-- YES --> Is it urgent (input, button)?
|
|
2140
|
+
| |-- YES --> Keep urgent update, useTransition for non-urgent
|
|
2141
|
+
| |-- NO --> useDeferredValue
|
|
2142
|
+
NO
|
|
2143
|
+
|
|
|
2144
|
+
You probably don't need any hook. Write plain JS/TS.
|
|
2145
|
+
```
|
|
2146
|
+
### 18.10 Form Validation & Submission Standards
|
|
2147
|
+
|
|
2148
|
+
Every form must use React Hook Form with Zod validation. The schema must be co-located with the form component.
|
|
2149
|
+
|
|
2150
|
+
```typescript
|
|
2151
|
+
// pages/employee/employee-bio-form.tsx
|
|
2152
|
+
import { useForm } from 'react-hook-form';
|
|
2153
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
2154
|
+
import { z } from 'zod';
|
|
2155
|
+
|
|
2156
|
+
const employeeBioSchema = z.object({
|
|
2157
|
+
firstName: z.string().min(1, 'First name is required').max(100),
|
|
2158
|
+
lastName: z.string().min(1, 'Last name is required').max(100),
|
|
2159
|
+
email: z.string().email('Invalid email format'),
|
|
2160
|
+
phone: z.string().regex(/^\+?[\d\s-]{7,15}$/, 'Invalid phone number').optional(),
|
|
2161
|
+
departmentId: z.string().uuid('Invalid department'),
|
|
2162
|
+
});
|
|
2163
|
+
|
|
2164
|
+
type EmployeeBioFormData = z.infer<typeof employeeBioSchema>;
|
|
2165
|
+
|
|
2166
|
+
export function EmployeeBioForm() {
|
|
2167
|
+
const { register, handleSubmit, formState: { errors, isSubmitting, isDirty }, setError, reset } = useForm<EmployeeBioFormData>({
|
|
2168
|
+
resolver: zodResolver(employeeBioSchema),
|
|
2169
|
+
defaultValues: { firstName: '', lastName: '', email: '', phone: '', departmentId: '' },
|
|
2170
|
+
});
|
|
2171
|
+
|
|
2172
|
+
const mutation = useUpdateEmployeeBio();
|
|
2173
|
+
|
|
2174
|
+
const onSubmit = async (data: EmployeeBioFormData) => {
|
|
2175
|
+
try {
|
|
2176
|
+
await mutation.mutateAsync(data);
|
|
2177
|
+
reset(data); // Reset dirty state after successful save
|
|
2178
|
+
toast.success('Bio updated successfully');
|
|
2179
|
+
} catch (err) {
|
|
2180
|
+
// Map server errors to form fields
|
|
2181
|
+
if (err.response?.data?.errors) {
|
|
2182
|
+
for (const { field, message } of err.response.data.errors) {
|
|
2183
|
+
setError(field as keyof EmployeeBioFormData, { message });
|
|
2184
|
+
}
|
|
2185
|
+
} else {
|
|
2186
|
+
toast.error('Failed to update bio. Please try again.');
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
|
|
2191
|
+
// Ensure required: unsaved changes warning
|
|
2192
|
+
useEffect(() => {
|
|
2193
|
+
const handler = (e: BeforeUnloadEvent) => { if (isDirty) e.preventDefault(); };
|
|
2194
|
+
window.addEventListener('beforeunload', handler);
|
|
2195
|
+
return () => window.removeEventListener('beforeunload', handler);
|
|
2196
|
+
}, [isDirty]);
|
|
2197
|
+
|
|
2198
|
+
return (
|
|
2199
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
2200
|
+
<div>
|
|
2201
|
+
<label htmlFor="firstName">First Name</label>
|
|
2202
|
+
<input id="firstName" {...register('firstName')} aria-describedby="firstName-error" />
|
|
2203
|
+
{errors.firstName && <p id="firstName-error" role="alert">{errors.firstName.message}</p>}
|
|
2204
|
+
</div>
|
|
2205
|
+
{/* ... other fields ... */}
|
|
2206
|
+
<button type="submit" disabled={isSubmitting || !isDirty}>
|
|
2207
|
+
{isSubmitting ? <Spinner /> : 'Save'}
|
|
2208
|
+
</button>
|
|
2209
|
+
</form>
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
```
|
|
2213
|
+
|
|
2214
|
+
**Rules**:
|
|
2215
|
+
- Zod schema co-located with form (not in a separate `types/` file)
|
|
2216
|
+
- Server errors mapped to field-level via `setError` (not shown as generic toast)
|
|
2217
|
+
- Unsaved changes warning via `beforeunload` when `isDirty`
|
|
2218
|
+
- Submit button disabled when `isSubmitting` (loading spinner) or `!isDirty`
|
|
2219
|
+
- All inputs have `<label>` with `htmlFor` matching input `id`
|
|
2220
|
+
- Error messages linked via `aria-describedby`
|
|
2221
|
+
- Multi-step forms: each step is a separate form section with shared `useForm` + `watch`
|
|
2222
|
+
- Optimistic updates preferred over form submission where applicable
|
|
2223
|
+
|
|
2224
|
+
---
|
|
2225
|
+
|
|
2226
|
+
## 19. Enforcement: Husky, Git Hooks & Automated Validation
|
|
2227
|
+
|
|
2228
|
+
### 19.1 Core Principle: Automation Over Manual Review
|
|
2229
|
+
|
|
2230
|
+
No rule is valuable if it relies on humans remembering to check it. Every standard in this document must have automated enforcement.
|
|
2231
|
+
|
|
2232
|
+
**Note on Package Managers**: While developers are free to use any package manager (npm, yarn, bun) for local development, all automated enforcement and CI/CD pipelines are optimized for **pnpm**. Ensure `pnpm-lock.yaml` is kept up to date.
|
|
2233
|
+
|
|
2234
|
+
**Mandatory Enforcement Layers**:
|
|
2235
|
+
|
|
2236
|
+
| Layer | Tool | When | Purpose |
|
|
2237
|
+
|-------|------|------|---------|
|
|
2238
|
+
| L1: Editor | ESLint + Prettier | Every save | Immediate feedback while coding |
|
|
2239
|
+
| L2: Pre-commit Hook | Husky + lint-staged | Every `git commit` | Block bad code before it enters history |
|
|
2240
|
+
| L3: CI Pipeline | GitHub Actions | Every PR / push | Enforce before merge, across all contributors |
|
|
2241
|
+
| L4: Code Review | Human review | Every PR | Architecture, logic, edge cases automation cannot catch |
|
|
2242
|
+
|
|
2243
|
+
### 19.2 Required Husky + lint-staged Setup
|
|
2244
|
+
|
|
2245
|
+
#### Step 1: Install dependencies
|
|
2246
|
+
|
|
2247
|
+
```bash
|
|
2248
|
+
pnpm add -D husky lint-staged
|
|
2249
|
+
```
|
|
2250
|
+
|
|
2251
|
+
#### Step 2: Enable Husky
|
|
2252
|
+
|
|
2253
|
+
```json
|
|
2254
|
+
// package.json
|
|
2255
|
+
{
|
|
2256
|
+
"scripts": {
|
|
2257
|
+
"prepare": "husky"
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
```
|
|
2261
|
+
|
|
2262
|
+
Run: `pnpm prepare` -- this creates the `.husky/` directory.
|
|
2263
|
+
|
|
2264
|
+
#### Step 3: Create Pre-commit Hook
|
|
2265
|
+
|
|
2266
|
+
```bash
|
|
2267
|
+
# .husky/pre-commit
|
|
2268
|
+
#!/usr/bin/env sh
|
|
2269
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
2270
|
+
|
|
2271
|
+
echo "Running lint-staged..."
|
|
2272
|
+
pnpm exec lint-staged
|
|
2273
|
+
```
|
|
2274
|
+
|
|
2275
|
+
#### Step 4: Create Commit-msg Hook
|
|
2276
|
+
|
|
2277
|
+
```bash
|
|
2278
|
+
# .husky/commit-msg
|
|
2279
|
+
#!/usr/bin/env sh
|
|
2280
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
2281
|
+
|
|
2282
|
+
echo "Validating commit message..."
|
|
2283
|
+
pnpm exec commitlint --edit "$1"
|
|
2284
|
+
```
|
|
2285
|
+
|
|
2286
|
+
#### Step 5: Create Pre-push Hook
|
|
2287
|
+
|
|
2288
|
+
```bash
|
|
2289
|
+
# .husky/pre-push
|
|
2290
|
+
#!/usr/bin/env sh
|
|
2291
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
2292
|
+
|
|
2293
|
+
echo "Running quality checks..."
|
|
2294
|
+
pnpm lint && pnpm check-modularization && pnpm check-types && pnpm test:ci
|
|
2295
|
+
```
|
|
2296
|
+
|
|
2297
|
+
The `check-modularization` script (see Step 7) validates folder structure and barrel file compliance before code reaches CI.
|
|
2298
|
+
|
|
2299
|
+
#### Step 6: Configure lint-staged
|
|
2300
|
+
|
|
2301
|
+
```json
|
|
2302
|
+
// package.json
|
|
2303
|
+
{
|
|
2304
|
+
"lint-staged": {
|
|
2305
|
+
"src/**/*.{ts,tsx}": [
|
|
2306
|
+
"eslint --fix --max-warnings 0",
|
|
2307
|
+
"prettier --write"
|
|
2308
|
+
],
|
|
2309
|
+
"src/**/*.{css,scss}": ["prettier --write"],
|
|
2310
|
+
"*.{json,md,yaml,yml}": ["prettier --write"]
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
```
|
|
2314
|
+
|
|
2315
|
+
#### Step 7: Add Modularization Validation Script
|
|
2316
|
+
|
|
2317
|
+
Add a `check-modularization` script to `package.json` that validates domain-folder structure and barrel file compliance:
|
|
2318
|
+
|
|
2319
|
+
```json
|
|
2320
|
+
// package.json
|
|
2321
|
+
{
|
|
2322
|
+
"scripts": {
|
|
2323
|
+
"check-modularization": "node scripts/check-modularization.mjs",
|
|
2324
|
+
"lint": "eslint --ext .ts,.tsx --max-warnings 0 .",
|
|
2325
|
+
"lint:fix": "eslint --ext .ts,.tsx . --fix",
|
|
2326
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,json,css,md}\"",
|
|
2327
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,json,css,md}\"",
|
|
2328
|
+
"check-types": "tsc --noEmit",
|
|
2329
|
+
"prepare": "husky",
|
|
2330
|
+
"validate": "pnpm lint && pnpm check-modularization && pnpm check-types",
|
|
2331
|
+
"test:validate": "pnpm test:ci --coverage"
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
```
|
|
2335
|
+
|
|
2336
|
+
#### Step 8: Anti-Bypass Enforcement (`--no-verify` Protection)
|
|
2337
|
+
|
|
2338
|
+
The `git commit --no-verify` (or `-n`) flag skips pre-commit and commit-msg hooks locally. This is a **policy violation** and must be caught in CI. To ensure checks persist regardless:
|
|
2339
|
+
|
|
2340
|
+
**1. Commit Message Validation in CI** — since `--no-verify` can skip `commitlint` locally, CI validates all PR commit messages:
|
|
2341
|
+
|
|
2342
|
+
```yaml
|
|
2343
|
+
# .github/workflows/ci.yml (add this step BEFORE the lint step)
|
|
2344
|
+
- name: Validate Commit Messages (anti-bypass for --no-verify)
|
|
2345
|
+
run: |
|
|
2346
|
+
echo "Checking all commit messages in this PR for conventional commit format..."
|
|
2347
|
+
pnpm exec commitlint --from origin/${{ github.base_ref }} --to HEAD
|
|
2348
|
+
# Fails if ANY commit message in the PR violates conventional commit format
|
|
2349
|
+
```
|
|
2350
|
+
|
|
2351
|
+
**2. Pre-commit Checks Duplicated in CI** — every check that pre-commit runs locally also runs in CI with zero tolerance:
|
|
2352
|
+
|
|
2353
|
+
```yaml
|
|
2354
|
+
- name: Lint (catches --no-verify bypass)
|
|
2355
|
+
run: pnpm lint
|
|
2356
|
+
# Runs regardless of whether developer skipped hooks locally
|
|
2357
|
+
|
|
2358
|
+
- name: Check Modularization (catches --no-verify bypass)
|
|
2359
|
+
run: pnpm check-modularization
|
|
2360
|
+
# Runs regardless of whether developer skipped hooks locally
|
|
2361
|
+
```
|
|
2362
|
+
|
|
2363
|
+
**3. ESLint Rule to Detect `--no-verify` Usage Pattern** — add a `no-restricted-syntax` rule that flags common bypass patterns in CI scripts or husky config tampering:
|
|
2364
|
+
|
|
2365
|
+
```javascript
|
|
2366
|
+
// eslint.config.js
|
|
2367
|
+
rules: {
|
|
2368
|
+
// Flag any attempted husky bypass or hook removal
|
|
2369
|
+
'no-restricted-properties': ['error', {
|
|
2370
|
+
object: 'process',
|
|
2371
|
+
property: 'exit',
|
|
2372
|
+
message: 'Do not use process.exit to bypass validation hooks.',
|
|
2373
|
+
}],
|
|
2374
|
+
}
|
|
2375
|
+
```
|
|
2376
|
+
|
|
2377
|
+
**4. Branch Protection Rule** — require all PR status checks to pass before merge. Even if a developer uses `--no-verify` locally, the PR cannot be merged because CI will fail on lint/modularization/commit-message checks.
|
|
2378
|
+
|
|
2379
|
+
**The `--no-verify` policy**:
|
|
2380
|
+
|
|
2381
|
+
| Action | Consequence |
|
|
2382
|
+
|--------|-------------|
|
|
2383
|
+
| `git commit --no-verify` | Commit lands locally, but CI catches all skipped checks on push/PR |
|
|
2384
|
+
| `git push --no-verify` | Push succeeds, but CI pre-push checks run anyway; PR blocked if failing |
|
|
2385
|
+
| Tampering with `.husky/` files | Caught in code review; `.husky/` is committed and reviewed |
|
|
2386
|
+
| Removing hooks from `package.json` | CI `pnpm install` re-installs husky; hooks re-created |
|
|
2387
|
+
|
|
2388
|
+
> **TL;DR**: `--no-verify` delays validation from commit-time to CI-time. The checks still run and the PR still gets blocked. There is no path to merge that bypasses these standards.
|
|
2389
|
+
|
|
2390
|
+
Create the validation script at `scripts/check-modularization.mjs`:
|
|
2391
|
+
|
|
2392
|
+
```javascript
|
|
2393
|
+
// scripts/check-modularization.mjs
|
|
2394
|
+
import { readdirSync, existsSync, statSync } from 'fs';
|
|
2395
|
+
import { join, dirname } from 'path';
|
|
2396
|
+
import { fileURLToPath } from 'url';
|
|
2397
|
+
|
|
2398
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2399
|
+
const PROJECT_ROOT = join(__dirname, '..');
|
|
2400
|
+
const SRC = join(PROJECT_ROOT, 'src');
|
|
2401
|
+
|
|
2402
|
+
// Modularized directories that require subdirectory organization
|
|
2403
|
+
const MODULARIZED_DIRS = ['services', 'hooks', 'store', 'components', 'lib', 'types'];
|
|
2404
|
+
|
|
2405
|
+
// Files allowed at the root of each directory (everything else must be in subdirs)
|
|
2406
|
+
const ALLOWED_ROOT_FILES = {
|
|
2407
|
+
services: ['api-client.ts', 'api-client.tsx'],
|
|
2408
|
+
hooks: ['use-debounce.ts', 'use-media-query.ts', 'index.ts'],
|
|
2409
|
+
store: ['index.ts'],
|
|
2410
|
+
components: ['index.ts'],
|
|
2411
|
+
lib: ['index.ts'],
|
|
2412
|
+
types: ['index.ts'],
|
|
2413
|
+
};
|
|
2414
|
+
|
|
2415
|
+
// Required barrel file name
|
|
2416
|
+
const BARREL_FILE = 'index.ts';
|
|
2417
|
+
|
|
2418
|
+
let errors = 0;
|
|
2419
|
+
let warnings = 0;
|
|
2420
|
+
|
|
2421
|
+
function logError(msg) {
|
|
2422
|
+
console.error(` ❌ ERROR: ${msg}`);
|
|
2423
|
+
errors++;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
function logWarning(msg) {
|
|
2427
|
+
console.warn(` ⚠️ WARNING: ${msg}`);
|
|
2428
|
+
warnings++;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// Helper: check if a path is a directory
|
|
2432
|
+
function isDirectory(path) {
|
|
2433
|
+
try { return statSync(path).isDirectory(); } catch { return false; }
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// Helper: check if a file is a TypeScript file
|
|
2437
|
+
function isTsFile(filename) {
|
|
2438
|
+
return /\.(ts|tsx)$/.test(filename) && !filename.endsWith('.test.ts') && !filename.endsWith('.test.tsx') && !filename.endsWith('.spec.ts') && !filename.endsWith('.spec.tsx');
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
console.log('\n🔍 Checking modularization compliance...\n');
|
|
2442
|
+
|
|
2443
|
+
for (const dirName of MODULARIZED_DIRS) {
|
|
2444
|
+
const dirPath = join(SRC, dirName);
|
|
2445
|
+
if (!existsSync(dirPath) || !isDirectory(dirPath)) continue;
|
|
2446
|
+
|
|
2447
|
+
const allowedRoot = ALLOWED_ROOT_FILES[dirName] || [];
|
|
2448
|
+
const entries = readdirSync(dirPath);
|
|
2449
|
+
|
|
2450
|
+
for (const entry of entries) {
|
|
2451
|
+
const entryPath = join(dirPath, entry);
|
|
2452
|
+
|
|
2453
|
+
if (isDirectory(entryPath)) {
|
|
2454
|
+
// This is a domain subdirectory -- check it has a barrel file
|
|
2455
|
+
console.log(` 📁 ${dirName}/${entry}/`);
|
|
2456
|
+
|
|
2457
|
+
const barrelPath = join(entryPath, BARREL_FILE);
|
|
2458
|
+
if (!existsSync(barrelPath)) {
|
|
2459
|
+
logError(`${dirName}/${entry}/ is missing required ${BARREL_FILE} barrel file`);
|
|
2460
|
+
} else {
|
|
2461
|
+
// Verify barrel exports at least one member
|
|
2462
|
+
const barrelContent = readdirSync(entryPath);
|
|
2463
|
+
const tsFiles = barrelContent.filter(f => isTsFile(f) && f !== BARREL_FILE);
|
|
2464
|
+
if (tsFiles.length === 0) {
|
|
2465
|
+
logWarning(`${dirName}/${entry}/${BARREL_FILE} exists but no implementation files found in this domain`);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// Check no nested subdirectories (domains should be flat)
|
|
2470
|
+
const subEntries = readdirSync(entryPath);
|
|
2471
|
+
for (const subEntry of subEntries) {
|
|
2472
|
+
const subPath = join(entryPath, subEntry);
|
|
2473
|
+
if (subEntry !== 'test' && subEntry !== '__tests__' && subEntry !== '__mocks__' && isDirectory(subPath)) {
|
|
2474
|
+
logWarning(`${dirName}/${entry}/ should not contain nested subdirectory "${subEntry}/" -- keep domains flat`);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
} else if (isTsFile(entry)) {
|
|
2478
|
+
// This is a flat file at the directory root
|
|
2479
|
+
if (!allowedRoot.includes(entry)) {
|
|
2480
|
+
logError(
|
|
2481
|
+
`Flat file "${dirName}/${entry}" is not allowed at the root level. ` +
|
|
2482
|
+
`Move it to a domain subdirectory (e.g., ${dirName}/${entry.replace(/\.(ts|tsx)$/, '')}/${entry}). ` +
|
|
2483
|
+
`Allowed root files: ${allowedRoot.join(', ') || 'none'}`
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// Also run the ESLint check (catches deep imports bypassing barrels)
|
|
2491
|
+
try {
|
|
2492
|
+
const { execSync } = await import('child_process');
|
|
2493
|
+
console.log('\n 🔍 Verifying no deep imports bypass barrels...');
|
|
2494
|
+
execSync('pnpm lint', { cwd: PROJECT_ROOT, stdio: 'inherit' });
|
|
2495
|
+
} catch {
|
|
2496
|
+
logError('ESLint found violations (may include deep imports bypassing barrel files)');
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
console.log(`\n${errors === 0 ? '✅' : '❌'} Modularization check complete. Errors: ${errors}, Warnings: ${warnings}\n`);
|
|
2500
|
+
|
|
2501
|
+
if (errors > 0) {
|
|
2502
|
+
console.error('Fix the errors above and try again.\n');
|
|
2503
|
+
process.exit(1);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
if (warnings > 0) {
|
|
2507
|
+
console.warn('Warnings found. Consider addressing them.\n');
|
|
2508
|
+
}
|
|
2509
|
+
```
|
|
2510
|
+
|
|
2511
|
+
**What this enforces at commit time**:
|
|
2512
|
+
- ESLint auto-fixes what it can, then fails hard on remaining errors (`--max-warnings 0`)
|
|
2513
|
+
- Prettier formats all staged files
|
|
2514
|
+
- Commitlint rejects non-conventional commit messages
|
|
2515
|
+
- Pre-push runs the full quality suite (lint, modularization, types, tests)
|
|
2516
|
+
- Modularization script validates folder structure AND barrel file compliance
|
|
2517
|
+
|
|
2518
|
+
### 19.3 Commitlint Configuration
|
|
2519
|
+
|
|
2520
|
+
```javascript
|
|
2521
|
+
// commitlint.config.js
|
|
2522
|
+
export default {
|
|
2523
|
+
extends: ['@commitlint/config-conventional'],
|
|
2524
|
+
rules: {
|
|
2525
|
+
'type-enum': [
|
|
2526
|
+
'error',
|
|
2527
|
+
[
|
|
2528
|
+
'feat',
|
|
2529
|
+
'fix',
|
|
2530
|
+
'security',
|
|
2531
|
+
'perf',
|
|
2532
|
+
'docs',
|
|
2533
|
+
'style',
|
|
2534
|
+
'refactor',
|
|
2535
|
+
'test',
|
|
2536
|
+
'chore',
|
|
2537
|
+
],
|
|
2538
|
+
],
|
|
2539
|
+
'subject-max-length': [2, 'always', 100],
|
|
2540
|
+
'body-max-line-length': [2, 'always', 100],
|
|
2541
|
+
},
|
|
2542
|
+
};
|
|
2543
|
+
```
|
|
2544
|
+
|
|
2545
|
+
### 19.4 ESLint Rules That Must Be Enforced (with Error Severity)
|
|
2546
|
+
|
|
2547
|
+
These rules translate the standards in Section 5 into machine-enforceable lint rules:
|
|
2548
|
+
|
|
2549
|
+
```javascript
|
|
2550
|
+
// packages/config/eslint/base.mjs (add these to the rules block)
|
|
2551
|
+
rules: {
|
|
2552
|
+
// === NAMING (Section 5.2) ===
|
|
2553
|
+
'id-length': ['error', {
|
|
2554
|
+
min: 2,
|
|
2555
|
+
max: 50,
|
|
2556
|
+
exceptions: ['i', 'j', 'k', '_', 'x', 'y', 'z'],
|
|
2557
|
+
properties: 'never',
|
|
2558
|
+
}],
|
|
2559
|
+
|
|
2560
|
+
camelcase: ['error', {
|
|
2561
|
+
properties: 'never',
|
|
2562
|
+
ignoreDestructuring: false,
|
|
2563
|
+
allow: ['^UNSAFE_'],
|
|
2564
|
+
}],
|
|
2565
|
+
|
|
2566
|
+
'unicorn/filename-case': ['error', {
|
|
2567
|
+
case: 'kebabCase',
|
|
2568
|
+
ignore: ['^[A-Z].*\\.tsx$'], // PascalCase for component files
|
|
2569
|
+
}],
|
|
2570
|
+
|
|
2571
|
+
'react/jsx-pascal-case': ['error', {
|
|
2572
|
+
allowAllCaps: true, // Allow HOCs like withAuth()
|
|
2573
|
+
}],
|
|
2574
|
+
|
|
2575
|
+
// === IMPORTS (Section 5.3) ===
|
|
2576
|
+
'import/order': ['error', {
|
|
2577
|
+
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
|
2578
|
+
'newlines-between': 'always',
|
|
2579
|
+
alphabetize: { order: 'asc', caseInsensitive: true },
|
|
2580
|
+
}],
|
|
2581
|
+
|
|
2582
|
+
// === FUNCTIONS (Section 5.2) ===
|
|
2583
|
+
'prefer-arrow-callback': 'error',
|
|
2584
|
+
'func-style': ['error', 'expression'],
|
|
2585
|
+
'require-await': 'error',
|
|
2586
|
+
|
|
2587
|
+
// === NULL SAFETY ===
|
|
2588
|
+
'unicorn/no-null': 'error',
|
|
2589
|
+
|
|
2590
|
+
// === REACT HOOKS (Section 18) ===
|
|
2591
|
+
'react-hooks/rules-of-hooks': 'error',
|
|
2592
|
+
'react-hooks/exhaustive-deps': 'warn',
|
|
2593
|
+
|
|
2594
|
+
// === LOVABLE GENERATED CODE (Section 5.4) ===
|
|
2595
|
+
'no-unused-vars': ['error', {
|
|
2596
|
+
varsIgnorePattern: '^_',
|
|
2597
|
+
argsIgnorePattern: '^_',
|
|
2598
|
+
}],
|
|
2599
|
+
|
|
2600
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
2601
|
+
'@typescript-eslint/no-unsafe-assignment': 'error',
|
|
2602
|
+
'@typescript-eslint/no-unsafe-call': 'error',
|
|
2603
|
+
'@typescript-eslint/no-unsafe-member-access': 'error',
|
|
2604
|
+
|
|
2605
|
+
// === PRETTIER ===
|
|
2606
|
+
'prettier/prettier': ['error', {
|
|
2607
|
+
semi: true,
|
|
2608
|
+
singleQuote: true,
|
|
2609
|
+
tabWidth: 2,
|
|
2610
|
+
trailingComma: 'es5',
|
|
2611
|
+
printWidth: 100,
|
|
2612
|
+
}],
|
|
2613
|
+
|
|
2614
|
+
// === CONSOLE & DEBUGGER (Section 13.4) ===
|
|
2615
|
+
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
2616
|
+
'no-debugger': 'error',
|
|
2617
|
+
|
|
2618
|
+
// === MODULARIZATION & BARREL FILE ENFORCEMENT (Section 3.3.1, 5.2.1) ===
|
|
2619
|
+
'boundaries/entry-point': ['error', {
|
|
2620
|
+
default: 'disallow',
|
|
2621
|
+
rules: [
|
|
2622
|
+
{ target: ['src/services/**/*.ts'], allow: 'src/services/api-client.ts' },
|
|
2623
|
+
{ target: ['src/hooks/use-employee*.ts', 'src/hooks/use-auth*.ts', 'src/hooks/use-user*.ts'], disallow: 'src/hooks/' },
|
|
2624
|
+
{ target: ['src/store/**/*.ts'], allow: 'src/store/index.ts' },
|
|
2625
|
+
],
|
|
2626
|
+
}],
|
|
2627
|
+
|
|
2628
|
+
'boundaries/element-types': ['error', {
|
|
2629
|
+
default: 'disallow',
|
|
2630
|
+
rules: [
|
|
2631
|
+
{ from: ['employee', 'auth', 'users'], allow: ['shared'] },
|
|
2632
|
+
{ from: ['employee'], disallow: ['auth', 'users'] },
|
|
2633
|
+
{ from: ['auth'], disallow: ['employee', 'users'] },
|
|
2634
|
+
{ from: ['users'], disallow: ['employee', 'auth'] },
|
|
2635
|
+
{ from: ['shared'], allow: ['shared'] },
|
|
2636
|
+
],
|
|
2637
|
+
}],
|
|
2638
|
+
|
|
2639
|
+
// BLOCK: deep imports bypassing barrel files (imports must go through index.ts)
|
|
2640
|
+
'import/no-internal-modules': ['error', {
|
|
2641
|
+
allow: [
|
|
2642
|
+
'src/services/*/index',
|
|
2643
|
+
'src/hooks/*/index',
|
|
2644
|
+
'src/store/*/index',
|
|
2645
|
+
'src/components/*/index',
|
|
2646
|
+
'src/lib/*/index',
|
|
2647
|
+
'src/types/*/index',
|
|
2648
|
+
'src/services/api-client',
|
|
2649
|
+
'src/hooks/use-debounce',
|
|
2650
|
+
'src/hooks/use-media-query',
|
|
2651
|
+
'src/components/ui/**',
|
|
2652
|
+
'src/lib/*',
|
|
2653
|
+
'src/types/*',
|
|
2654
|
+
'src/app/**',
|
|
2655
|
+
'src/styles/**',
|
|
2656
|
+
'src/test/**',
|
|
2657
|
+
],
|
|
2658
|
+
}],
|
|
2659
|
+
}
|
|
2660
|
+
```
|
|
2661
|
+
|
|
2662
|
+
### 19.5 CI Validation Scripts (package.json)
|
|
2663
|
+
|
|
2664
|
+
Create a `scripts/` directory with reusable validation:
|
|
2665
|
+
|
|
2666
|
+
```json
|
|
2667
|
+
// package.json
|
|
2668
|
+
{
|
|
2669
|
+
"scripts": {
|
|
2670
|
+
"lint": "eslint --ext .ts,.tsx --max-warnings 0 .",
|
|
2671
|
+
"lint:fix": "eslint --ext .ts,.tsx . --fix",
|
|
2672
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,json,css,md}\"",
|
|
2673
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,json,css,md}\"",
|
|
2674
|
+
"check-types": "tsc --noEmit",
|
|
2675
|
+
"prepare": "husky",
|
|
2676
|
+
"validate": "pnpm lint && pnpm check-types",
|
|
2677
|
+
"test:validate": "pnpm test:ci --coverage"
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
```
|
|
2681
|
+
|
|
2682
|
+
### 19.6 CI Pipeline Enforcement (GitHub Actions)
|
|
2683
|
+
|
|
2684
|
+
Every CI run must check these in order. Any failure blocks the PR:
|
|
2685
|
+
|
|
2686
|
+
```yaml
|
|
2687
|
+
# .github/workflows/ci.yml (excerpt)
|
|
2688
|
+
jobs:
|
|
2689
|
+
quality:
|
|
2690
|
+
runs-on: ubuntu-latest
|
|
2691
|
+
steps:
|
|
2692
|
+
- uses: actions/checkout@v4
|
|
2693
|
+
with:
|
|
2694
|
+
fetch-depth: 0
|
|
2695
|
+
- uses: actions/setup-node@v4
|
|
2696
|
+
- uses: pnpm/action-setup@v2
|
|
2697
|
+
- run: pnpm install --frozen-lockfile
|
|
2698
|
+
|
|
2699
|
+
# LAYER 3: Automated enforcement
|
|
2700
|
+
- name: Validate Commit Messages (anti-bypass for --no-verify)
|
|
2701
|
+
if: github.event_name == 'pull_request'
|
|
2702
|
+
run: pnpm exec commitlint --from origin/${{ github.base_ref }} --to HEAD
|
|
2703
|
+
|
|
2704
|
+
- name: Lint ( ESLint + naming rules + modularization )
|
|
2705
|
+
run: pnpm lint
|
|
2706
|
+
# Fails on: naming, imports, types, prettier, no-unused-vars, cross-domain imports
|
|
2707
|
+
|
|
2708
|
+
- name: Check Modularization ( domain structure + barrels )
|
|
2709
|
+
run: pnpm check-modularization
|
|
2710
|
+
# Fails on: flat files at directory roots, missing barrel files, deep imports
|
|
2711
|
+
|
|
2712
|
+
- name: Type Check ( TypeScript strict )
|
|
2713
|
+
run: pnpm check-types
|
|
2714
|
+
# Fails on: any TS type error, missing types, implicit any
|
|
2715
|
+
|
|
2716
|
+
- name: Format Check ( Prettier )
|
|
2717
|
+
run: pnpm format:check
|
|
2718
|
+
# Fails on: any formatting deviation
|
|
2719
|
+
|
|
2720
|
+
- name: Test with Coverage ( Vitest )
|
|
2721
|
+
run: pnpm test:validate
|
|
2722
|
+
# Fails on: test failures OR coverage < 80%
|
|
2723
|
+
|
|
2724
|
+
- name: Security Audit ( pnpm audit )
|
|
2725
|
+
run: pnpm audit --audit-level=high
|
|
2726
|
+
# Fails on: high/critical vulnerabilities
|
|
2727
|
+
|
|
2728
|
+
- name: Build
|
|
2729
|
+
run: pnpm build
|
|
2730
|
+
# Fails on: TS errors missed by tsc, bundler errors
|
|
2731
|
+
```
|
|
2732
|
+
|
|
2733
|
+
### 19.7 Coverage Threshold Enforcement
|
|
2734
|
+
|
|
2735
|
+
> **Note**: Vitest v3 deprecated the top-level `lines`/`branches`/`functions`/`statements` shorthand in the `coverage` block. Use `thresholds` exclusively.
|
|
2736
|
+
|
|
2737
|
+
```javascript
|
|
2738
|
+
// vitest.config.ts
|
|
2739
|
+
import { defineConfig } from 'vitest/config';
|
|
2740
|
+
|
|
2741
|
+
export default defineConfig({
|
|
2742
|
+
test: {
|
|
2743
|
+
coverage: {
|
|
2744
|
+
provider: 'v8',
|
|
2745
|
+
reporter: ['text', 'json', 'html', 'cobertura'],
|
|
2746
|
+
thresholds: {
|
|
2747
|
+
lines: 80,
|
|
2748
|
+
branches: 75,
|
|
2749
|
+
functions: 80,
|
|
2750
|
+
statements: 80,
|
|
2751
|
+
// Each new file must have at least 90%
|
|
2752
|
+
perFile: true,
|
|
2753
|
+
},
|
|
2754
|
+
},
|
|
2755
|
+
},
|
|
2756
|
+
});
|
|
2757
|
+
```
|
|
2758
|
+
|
|
2759
|
+
### 19.8 Pre-commit Validation Script Template
|
|
2760
|
+
|
|
2761
|
+
```javascript
|
|
2762
|
+
// scripts/validate-pre-commit.js
|
|
2763
|
+
import { execSync } from 'child_process';
|
|
2764
|
+
import chalk from 'chalk';
|
|
2765
|
+
|
|
2766
|
+
const checks = [
|
|
2767
|
+
{ name: 'Lint', command: 'pnpm lint' },
|
|
2768
|
+
{ name: 'Type Check', command: 'pnpm check-types' },
|
|
2769
|
+
];
|
|
2770
|
+
|
|
2771
|
+
let failed = false;
|
|
2772
|
+
|
|
2773
|
+
for (const check of checks) {
|
|
2774
|
+
try {
|
|
2775
|
+
console.log(chalk.blue(`Running ${check.name}...`));
|
|
2776
|
+
execSync(check.command, { stdio: 'inherit' });
|
|
2777
|
+
console.log(chalk.green(`${check.name} passed`));
|
|
2778
|
+
} catch (err) {
|
|
2779
|
+
console.error(chalk.red(`${check.name} FAILED`));
|
|
2780
|
+
failed = true;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
if (failed) {
|
|
2785
|
+
console.error(chalk.red('\nPre-commit validation failed. Fix the above errors and try again.'));
|
|
2786
|
+
process.exit(1);
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
console.log(chalk.green('\nAll pre-commit checks passed!'));
|
|
2790
|
+
```
|
|
2791
|
+
|
|
2792
|
+
### 19.9 What Each Layer Catches
|
|
2793
|
+
|
|
2794
|
+
| Layer | Catches | Cannot Catch |
|
|
2795
|
+
|-------|---------|--------------|
|
|
2796
|
+
| L1 Editor | Typos, bad names, bad formatting | Logic bugs, architecture flaws |
|
|
2797
|
+
| L2 Pre-commit | Same as L1 + blocks commit | Business logic errors |
|
|
2798
|
+
| L3 CI Pipeline | Everything automated can check | Business logic correctness |
|
|
2799
|
+
| L4 Code Review | Architecture, security, edge cases | Naming that already passed ESLint |
|
|
2800
|
+
|
|
2801
|
+
### 19.10 Enforcement Checklist for New Projects
|
|
2802
|
+
|
|
2803
|
+
When setting up a new project, verify:
|
|
2804
|
+
|
|
2805
|
+
- [ ] Husky installed: `pnpm prepare` creates `.husky/`
|
|
2806
|
+
- [ ] Pre-commit hook exists and runs: `git commit --dry-run`
|
|
2807
|
+
- [ ] Commit-msg hook exists: Bad message is rejected
|
|
2808
|
+
- [ ] Pre-push hook exists: Tests run on `git push`
|
|
2809
|
+
- [ ] lint-staged config in package.json (not in separate config file)
|
|
2810
|
+
- [ ] `pnpm lint` exits non-zero on any ESLint error (including boundaries + `no-internal-modules`)
|
|
2811
|
+
- [ ] `pnpm check-modularization` exits non-zero on folder structure / barrel violations
|
|
2812
|
+
- [ ] `pnpm check-types` exits non-zero on any TS error
|
|
2813
|
+
- [ ] `pnpm format:check` exits non-zero on unformatted files
|
|
2814
|
+
- [ ] `pnpm test:ci` enforces coverage thresholds (80% lines, 75% branches)
|
|
2815
|
+
- [ ] `pnpm audit --audit-level=high` runs in CI
|
|
2816
|
+
- [ ] GitHub Actions CI blocked on lint/modularization/type/test failures (not `continue-on-error`)
|
|
2817
|
+
- [ ] Branch protection requires CI to pass before merge
|
|
2818
|
+
- [ ] Every domain subdirectory has an `index.ts` barrel file
|
|
2819
|
+
- [ ] No flat domain files at `services/`, `hooks/`, `store/`, `components/`, `lib/` roots
|
|
2820
|
+
- [ ] `.github/PULL_REQUEST_TEMPLATE.md` exists (copied from `@aoholdings/frontend-standards`)
|
|
2821
|
+
- [ ] All PRs follow the template format
|
|
2822
|
+
- [ ] Reviewer knows the standards in this document
|
|
2823
|
+
|
|
2824
|
+
---
|
|
2825
|
+
|
|
2826
|
+
> **This is a living document.** It should be reviewed and updated quarterly by the frontend chapter lead and architecture team. Any deviation from these standards must be documented in an Architecture Decision Record (ADR).
|
|
2827
|
+
|
|
2828
|
+
---
|
|
2829
|
+
## Appendix C. Frontend Cost & Pricing Reference (June 2026)
|
|
2830
|
+
|
|
2831
|
+
> Use this to estimate implementation and operational costs when proposing a new project or staffing decisions. Prices are approximations for mid-market commercial engagements and can vary by vendor, region, and contract length.
|
|
2832
|
+
|
|
2833
|
+
### Partner Benefits & Azure Cost Relief
|
|
2834
|
+
|
|
2835
|
+
As a **Microsoft Partner**, your organization may qualify for:
|
|
2836
|
+
|
|
2837
|
+
- **Microsoft Action Pack / Solutions Partner** benefits: included Azure credits, free/dev CI-CD minutes, and co-selling/support access.
|
|
2838
|
+
- **Azure Hybrid Benefit / Reserved Instances**: reduces VM and managed-service compute cost by **20%–40%**.
|
|
2839
|
+
- **Visual Studio Enterprise / GitHub Enterprise** bundled licensing: avoids duplicate license spend and typically lowers total cost of ownership.
|
|
2840
|
+
- **Azure for Startups / ISV Pied Piper**: early-stage credits and advisory hours to reduce ramp-up cost.
|
|
2841
|
+
- **FastTrack / Partner Support**: faster migrations and production-readiness reviews reduce delivery risk and project overruns.
|
|
2842
|
+
|
|
2843
|
+
Use this table to compare partner-inclusive pricing vs. standard list pricing when planning workloads.
|
|
2844
|
+
|
|
2845
|
+
### C.1 Estimated Annual Costs by Project Size
|
|
2846
|
+
|
|
2847
|
+
| Project Size | Frontend Devs | Backend Devs | DevOps | QA | Tools & Licenses | Infrastructure | Total Est. Annual |
|
|
2848
|
+
|---|---|---|---|---|---|---|---|
|
|
2849
|
+
| Small (MVP/closed beta) | 1 | 1 | 0.25 | 0.5 | ~USD 2,000 | ~USD 1,500 | ~USD 160,000 |
|
|
2850
|
+
| Medium (production SaaS) | 2 | 2 | 0.5 | 1 | ~USD 5,000 | ~USD 3,500 | ~USD 350,000 |
|
|
2851
|
+
| Large (enterprise/multi-tenant) | 4 | 4 | 1 | 2 | ~USD 12,000 | ~USD 8,000 | ~USD 800,000 |
|
|
2852
|
+
| Enterprise (regulated/high-scale) | 6+ | 6+ | 2+ | 3+ | ~USD 25,000+ | ~USD 20,000+ | ~USD 1,600,000+ |
|
|
2853
|
+
|
|
2854
|
+
### C.2 Tooling & License Costs (Annual)
|
|
2855
|
+
|
|
2856
|
+
| Tool / Service | Purpose | Typical Tier | Est. Cost | Notes |
|
|
2857
|
+
|---|---|---|---|---|
|
|
2858
|
+
| GitHub Enterprise / Teams | Repo hosting, Actions minutes, GHCR, Environments | Team: USD 4/user/mo; Enterprise: USD 21/user/mo | USD 2,400 – USD 25,000/yr | Public repos have free Actions minutes; private minutes are billed beyond the free tier (2,000 min/mo). |
|
|
2859
|
+
| Sentry (Error Tracking) | Frontend & backend error monitoring | Team: ~USD 29/app/mo | USD 350+/app/yr | Volume-based; consider self-hosted at scale. |
|
|
2860
|
+
| Datadog / New Relic / Azure Monitor | APM, logs, traces | Host-based or ingestion-based | USD 1,500 – USD 10,000+/yr | Strong for microservices; may overlap with Prometheus/Grafana. |
|
|
2861
|
+
| SonarCloud / SonarQube | Static analysis, quality gate | Developer: ~USD 25/dev/mo; Enterprise billed annually | USD 2,000 – USD 15,000/yr | SonarCloud is SaaS; SonarQube can be self-hosted with infra cost. |
|
|
2862
|
+
| Snyk / Trivy / Dependabot | Dependency & container vulnerability scanning | Snyk Team: ~USD 19/dev/mo; Trivy is free (OSS) | USD 0 – USD 5,000/yr | Trivy + GitHub Dependabot cover most needs at no extra cost. |
|
|
2863
|
+
| Vercel / Netlify / Azure Static Web Apps | Frontend preview deploys and hosting | Pro: ~USD 20/user/mo | USD 2,400 – USD 8,000/yr | GHCR + K8s can replace this; preview environments add cost. |
|
|
2864
|
+
| Azure Container Registry (ACR) or AWS ECR | Container image registry | Basic: USD 0.167/day + storage/egress | USD 100 – USD 1,000/yr | Often cheaper than GHCR at high throughput; pay for storage + egress. |
|
|
2865
|
+
| Auth0 / WorkOS / Clerk | Managed auth (if not custom) | Free to USD 23+/mo | USD 0 – USD 2,000/yr | Custom NestJS auth saves license cost but needs more engineering time. |
|
|
2866
|
+
| Storybook | Component library documentation + review | OSS free; Cloud from USD 20/mo | USD 0 – USD 5,000/yr | Includes collaboration add-ons in paid tiers. |
|
|
2867
|
+
|
|
2868
|
+
### C.3 Infrastructure Costs (Monthly)
|
|
2869
|
+
|
|
2870
|
+
| Environment | Typical Specs | Est. Monthly | Annual Est. |
|
|
2871
|
+
|---|---|---|---|
|
|
2872
|
+
| Dev (1 replica, no HA) | 1 vCPU / 2 GB RAM | USD 40 – USD 120 | USD 500 – USD 1,500 |
|
|
2873
|
+
| Staging (2 replicas) | 2 vCPU / 4 GB RAM each | USD 120 – USD 300 | USD 1,500 – USD 3,600 |
|
|
2874
|
+
| Production (min 3 replicas, autoscale) | 2 vCPU / 4 GB RAM each + load balancer | USD 300 – USD 900 | USD 3,600 – USD 10,800 |
|
|
2875
|
+
| Managed PostgreSQL (e.g., Azure Database, RDS, Cloud SQL) | 2 vCPU / 8 GB + storage | USD 150 – USD 500 | USD 1,800 – USD 6,000 |
|
|
2876
|
+
| Managed Redis / Valkey | 1 vCPU / 2 GB | USD 15 – USD 50 | USD 180 – USD 600 |
|
|
2877
|
+
| Object Storage (S3 / Blob) | 1 TB + egress | USD 20 – USD 80 | USD 240 – USD 960 |
|
|
2878
|
+
| SSL (managed by ingress or ACM) | -- | Often free | USD 0 |
|
|
2879
|
+
| DNS / domain | -- | USD 10 – USD 30 | USD 120 – USD 360 |
|
|
2880
|
+
|
|
2881
|
+
> These are mid-2026 market reference prices. Cloud providers frequently discount reserved/commitment terms.
|
|
2882
|
+
|
|
2883
|
+
### C.4 CDN & Hosting Notes
|
|
2884
|
+
|
|
2885
|
+
- For multi-region low-latency asset delivery, expect an additional USD 10 – USD 80/month depending on cache hit ratio.
|
|
2886
|
+
- Pre-rendering, caching strategies, and service workers reduce bandwidth and infrastructure cost more than most premature optimizations.
|
|
2887
|
+
|
|
2888
|
+
---
|
|
2889
|
+
|
|
2890
|
+
### C.5 Open-Source vs Commercial Alternatives (2026)
|
|
2891
|
+
|
|
2892
|
+
| Need | Commercial Option | OSS Alternative | Est. Savings | Trade-offs / Notes |
|
|
2893
|
+
|---|---|---|---|---|
|
|
2894
|
+
| CI/CD | GitHub Actions (private) | **Woodpecker**, **Gitea Actions**, Jenkins, GitLab CI (self-hosted runners) | Up to ~USD 10,000/yr on high volume | Self-hosted runners need infra + maintenance. Woodpecker/Gitea are easiest to adopt if you want to stay off cloud minutes. |
|
|
2895
|
+
| Error Tracking | Sentry Team | **Sentry Self-Hosted**, GlitchTip | ~USD 350 – USD 3,000+/yr saved | Self-hosted Sentry needs ~1–2 vCPU + 2–4 GB RAM per app. GlitchTip is lighter. |
|
|
2896
|
+
| Monitoring/APM | Datadog / New Relic | **Prometheus + Grafana**, SigNoz, Uptime Kuma | ~USD 1,500 – USD 10,000+/yr saved | OSS has higher setup cost. Best for teams with in-house SRE knowledge. |
|
|
2897
|
+
| Static Analysis | SonarCloud | **SonarQube Community**, ESLint + TypeScript strict + Coverage, CodeQL | ~USD 2,000 – USD 15,000/yr saved | SonarQube CE is capable. GitHub CodeQL is free for public and private repos. |
|
|
2898
|
+
| Dependency Scanning | Snyk | **Trivy**, OWASP Dependency-Check, GitHub Dependabot, Renovate | ~USD 0 – USD 5,000/yr saved | Dependabot + Trivy cover the majority of needs at zero license cost. |
|
|
2899
|
+
| Auth (if not custom) | Auth0 / Clerk | **Keycloak**, Authentik, SuperTokens | ~USD 0 – USD 2,000/yr saved | Keycloak is the most mature but heavier. SuperTokens is lighter and developer-friendly. |
|
|
2900
|
+
| Component Review | Storybook Cloud | **Storybook OSS**, Chromatic (paid) | ~USD 0 – USD 5,000/yr saved | Storybook OSS is fully free. Chromatic adds hosted review at ~USD 20/app/mo. |
|
|
2901
|
+
| Frontend Hosting | Vercel / Netlify / SWA | **Nginx + Cloudflare**, Caddy, self-hosted K8s + GHCR | ~USD 2,400 – USD 8,000/yr saved | GHCR + K8s is usually cheaper at scale; preview environments cost more effort. |
|
|
2902
|
+
| Container Registry | ACR / ECR | **GHCR**, Harbor (self-hosted) | ~USD 100 – USD 1,000/yr saved | GHCR is free for public/private with GitHub Actions. Harbor is good for air-gapped/on-prem. |
|
|
2903
|
+
|
|
2904
|
+
### C.6 OSS Cost-Scoring Cheat Sheet
|
|
2905
|
+
|
|
2906
|
+
| Solution | Setup Effort | Maintenance | When It Wins | When to Buy Commercial |
|
|
2907
|
+
|---|---|---|---|---|
|
|
2908
|
+
| Prometheus + Grafana | Medium | Medium | Metrics-heavy infra, in-house SRE skill | When you want hosted dashboards, alerting, and support SLAs |
|
|
2909
|
+
| Sentry Self-Hosted | Medium | Medium | Apps with many errors and sensitive logs | When uptime and incident response are more important than license cost |
|
|
2910
|
+
| Keycloak | High | High | Enterprise SSO, internal identity, B2B | When identity is not your core competency |
|
|
2911
|
+
| Woodpecker / Gitea | Medium | Medium | Heavy CI usage, private repo cost control | When you want zero per-minute billing and full data control |
|
|
2912
|
+
| Jenkins | High | High | Legacy enterprise estates, Windows workloads | When your team already knows Jenkins and plugin ecosystem |
|
|
2913
|
+
| Harbor | Medium | Medium | Air-gapped/on-prem registries, large images | When egress or compliance prevents cloud registries |
|
|
2914
|
+
|
|
2915
|
+
### C.7 Backend-Oriented Cost Notes (Frontend Impact)
|
|
2916
|
+
|
|
2917
|
+
- **Serverless / Edge**: If the backend is serverless (Lambda/Cloud Functions), frontend should aim for fewer, batched API calls to reduce invocation costs.
|
|
2918
|
+
- **Caching**: Client-side caching (React Query, SW) and CDN caching reduce backend and bandwidth spend.
|
|
2919
|
+
- **Monitoring**: OSS frontend monitoring stack (Sentry self-hosted + Grafana) can replace Datadog RUM for ~USD 1,500 – USD 3,000/yr with some ops effort.
|
|
2920
|
+
- **Rate Limiting**: Implemented correctly in the frontend (debounce/cancel stale requests), it reduces backend compute and queuing costs.
|
|
2921
|
+
|
|
2922
|
+
### C.8 Sources & Methodology
|
|
2923
|
+
|
|
2924
|
+
- Pricing is based on **public list prices** from vendor pricing pages:
|
|
2925
|
+
- Azure pricing: https://azure.microsoft.com/en-us/pricing/
|
|
2926
|
+
- Azure Static Web Apps: https://azure.microsoft.com/en-us/pricing/details/app-service/static/
|
|
2927
|
+
- GitHub: https://github.com/pricing
|
|
2928
|
+
- Sentry: https://sentry.io/pricing/
|
|
2929
|
+
- Datadog: https://www.datadoghq.com/pricing/
|
|
2930
|
+
- SonarCloud: https://www.sonarsource.com/pricing/
|
|
2931
|
+
- Snyk: https://snyk.io/pricing/
|
|
2932
|
+
- Vercel: https://vercel.com/pricing
|
|
2933
|
+
- Netlify: https://www.netlify.com/pricing/
|
|
2934
|
+
- Storybook: https://storybook.js.org/docs/configure/overview#tooling-options
|
|
2935
|
+
- Microsoft Partner benefits sourced from:
|
|
2936
|
+
- Microsoft Partner Network: https://partner.microsoft.com/
|
|
2937
|
+
- Microsoft for Startups / ISV Pied Piper: https://startups.microsoft.com/
|
|
2938
|
+
- Azure Hybrid Benefit: https://azure.microsoft.com/en-us/pricing/benefits/azure-hybrid-benefit/
|
|
2939
|
+
- Visual Studio subscriptions: https://visualstudio.microsoft.com/subscriptions/
|
|
2940
|
+
- FastTrack: https://azure.microsoft.com/en-us/programs/azure-fasttrack/
|
|
2941
|
+
Actual entitlement depends on partner tier, agreement, and region; treat these as **items to explore** with your account team.
|
|
2942
|
+
- Cloud infrastructure ranges are derived from standard managed VM, DB, and storage pricing pages; costs can be lower with reserved instances, savings plans, or partner credits.
|