@synergyerp/frontend-standards 1.1.2 → 1.1.3
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 +355 -341
- package/FRONTEND_STANDARDS.md +496 -384
- package/package.json +1 -1
package/FRONTEND_STANDARDS.md
CHANGED
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
|
|
46
46
|
This document is split into two distinct parts:
|
|
47
47
|
|
|
48
|
-
| Part
|
|
49
|
-
|
|
50
|
-
| **Frontend Standards** (Sections 2-9)
|
|
48
|
+
| Part | Focus | Audience |
|
|
49
|
+
| --------------------------------------- | -------------------------------------------------------- | ---------------------- |
|
|
50
|
+
| **Frontend Standards** (Sections 2-9) | Code structure, linting, UI components, testing, git | Developers, Tech Leads |
|
|
51
51
|
| **CI/CD & Deployment** (Sections 10-14) | Pipelines, code review, deployment, monitoring, security | DevOps, QA, Tech Leads |
|
|
52
52
|
|
|
53
53
|
---
|
|
@@ -56,41 +56,41 @@ This document is split into two distinct parts:
|
|
|
56
56
|
|
|
57
57
|
### 2.1 Framework & Runtime
|
|
58
58
|
|
|
59
|
-
| Layer
|
|
60
|
-
|
|
61
|
-
| **Framework**
|
|
62
|
-
| **Language**
|
|
63
|
-
| **Build Tool**
|
|
64
|
-
| **Package Manager** | Any (pnpm 9+ preferred)
|
|
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
65
|
|
|
66
66
|
### 2.2 UI & Styling
|
|
67
67
|
|
|
68
|
-
| Layer
|
|
69
|
-
|
|
70
|
-
| **UI Components**
|
|
71
|
-
| **Styling**
|
|
72
|
-
| **Component Library** | Shadcn UI (or custom)
|
|
73
|
-
| **Icons**
|
|
74
|
-
| **Animations**
|
|
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
75
|
|
|
76
76
|
### 2.3 State & Data
|
|
77
77
|
|
|
78
|
-
| Layer
|
|
79
|
-
|
|
80
|
-
| **Server State** | TanStack React Query (v5)
|
|
81
|
-
| **Client State** | Zustand
|
|
82
|
-
| **Forms**
|
|
83
|
-
| **Routing**
|
|
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
84
|
|
|
85
85
|
### 2.4 Testing
|
|
86
86
|
|
|
87
|
-
| Layer
|
|
88
|
-
|
|
89
|
-
| **Unit / Component**
|
|
90
|
-
| **Component Testing** | Testing Library + jsdom
|
|
91
|
-
| **E2E**
|
|
92
|
-
| **Visual Regression** | Playwright / Chromatic
|
|
93
|
-
| **Coverage**
|
|
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
94
|
|
|
95
95
|
---
|
|
96
96
|
|
|
@@ -165,14 +165,14 @@ All domain-specific files across **every** directory (`services/`, `hooks/`, `st
|
|
|
165
165
|
|
|
166
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
167
|
|
|
168
|
-
| Directory
|
|
169
|
-
|
|
170
|
-
| `services/`
|
|
171
|
-
| `hooks/`
|
|
172
|
-
| `store/`
|
|
173
|
-
| `components/` | Domain-specific components in `components/<domain>/`. Shared UI primitives in `components/ui/`.
|
|
174
|
-
| `lib/`
|
|
175
|
-
| `types/`
|
|
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
176
|
|
|
177
177
|
**Barrel File (`index.ts`) Convention**:
|
|
178
178
|
|
|
@@ -243,7 +243,7 @@ These options ensure a modern, strict, and performant TypeScript environment:
|
|
|
243
243
|
- **noEmit: true**: Relies on the bundler (Vite) for emission, using tsc only for type checking.
|
|
244
244
|
- **jsx: react-jsx**: Enables the modern React JSX transform.
|
|
245
245
|
- **baseUrl: .**: Sets the root for non-relative module resolution.
|
|
246
|
-
- **paths**: Configures path aliases (e.g.,
|
|
246
|
+
- **paths**: Configures path aliases (e.g., @/\*) for cleaner imports.
|
|
247
247
|
|
|
248
248
|
```jsonc
|
|
249
249
|
{
|
|
@@ -261,8 +261,8 @@ These options ensure a modern, strict, and performant TypeScript environment:
|
|
|
261
261
|
"noEmit": true,
|
|
262
262
|
"jsx": "react-jsx",
|
|
263
263
|
"baseUrl": ".",
|
|
264
|
-
"paths": { "@/*": ["src/*"] }
|
|
265
|
-
}
|
|
264
|
+
"paths": { "@/*": ["src/*"] },
|
|
265
|
+
},
|
|
266
266
|
}
|
|
267
267
|
```
|
|
268
268
|
|
|
@@ -279,17 +279,17 @@ These options ensure a modern, strict, and performant TypeScript environment:
|
|
|
279
279
|
|
|
280
280
|
### 5.1 ESLint (v9 Flat Config) - Required Plugins
|
|
281
281
|
|
|
282
|
-
| Plugin
|
|
283
|
-
|
|
284
|
-
| @eslint/js
|
|
285
|
-
| typescript-eslint
|
|
286
|
-
| eslint-plugin-react
|
|
287
|
-
| eslint-plugin-react-hooks | React Hooks rules
|
|
288
|
-
| eslint-plugin-import
|
|
289
|
-
| eslint-plugin-unicorn
|
|
290
|
-
| eslint-plugin-prettier
|
|
291
|
-
| eslint-plugin-jsx-a11y
|
|
292
|
-
| eslint-plugin-boundaries
|
|
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
293
|
|
|
294
294
|
### 5.2 Naming Conventions -- Complete Reference
|
|
295
295
|
|
|
@@ -298,44 +298,46 @@ These options ensure a modern, strict, and performant TypeScript environment:
|
|
|
298
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
299
|
|
|
300
300
|
**Single-letter variables are strictly prohibited** except:
|
|
301
|
+
|
|
301
302
|
- Loop index i, j, k inside a for loop
|
|
302
|
-
- Array callback _ for unused parameters
|
|
303
|
+
- Array callback \_ for unused parameters
|
|
303
304
|
- Math coordinates x, y, z (must be documented)
|
|
304
305
|
|
|
305
306
|
#### Naming Reference Table
|
|
306
307
|
|
|
307
|
-
| Entity
|
|
308
|
-
|
|
309
|
-
| Files (General)
|
|
310
|
-
| Files (React Components) | PascalCase
|
|
311
|
-
| Directories
|
|
312
|
-
| Variables (general)
|
|
313
|
-
| Booleans
|
|
314
|
-
| Functions
|
|
315
|
-
| Constants (module)
|
|
316
|
-
| Types/Interfaces
|
|
317
|
-
| Enums
|
|
318
|
-
| Event handlers
|
|
319
|
-
| Handler props
|
|
320
|
-
| Env variables
|
|
321
|
-
| Mock data
|
|
308
|
+
| Entity | Convention | Good Example | Bad Example |
|
|
309
|
+
| ------------------------ | ------------------------- | ----------------------------- | ---------------- |
|
|
310
|
+
| Files (General) | kebab-case | user-service.ts | UserService.ts |
|
|
311
|
+
| Files (React Components) | PascalCase | UserProfile.tsx | user-profile.tsx |
|
|
312
|
+
| Directories | kebab-case | user-profile/ | UserProfile/ |
|
|
313
|
+
| Variables (general) | camelCase | userCount, isLoading | uc, l, data |
|
|
314
|
+
| Booleans | camelCase + prefix | isLoading, hasError, canEdit | loading, error |
|
|
315
|
+
| Functions | camelCase (verb+noun) | getUserById(), handleSubmit() | get(), doStuff() |
|
|
316
|
+
| Constants (module) | UPPER_SNAKE_CASE | MAX_RETRY_COUNT | maxRetryCount |
|
|
317
|
+
| Types/Interfaces | PascalCase | UserProfileProps | userProfile |
|
|
318
|
+
| Enums | PascalCase + UPPER values | Role.ADMIN | role.admin |
|
|
319
|
+
| Event handlers | handle + Event | handleSubmit | submitHandler |
|
|
320
|
+
| Handler props | on + Event | onSubmit, onClick | submit, clicked |
|
|
321
|
+
| Env variables | UPPER_SNAKE + prefix | VITE_API_URL | apiUrl |
|
|
322
|
+
| Mock data | mock + Entity | mockUser, mockResponse | testData |
|
|
322
323
|
|
|
323
324
|
#### Banned Variable Names
|
|
324
325
|
|
|
325
|
-
| Banned
|
|
326
|
-
|
|
327
|
-
| const data
|
|
328
|
-
| const temp
|
|
329
|
-
| const arr
|
|
330
|
-
| const obj
|
|
331
|
-
| const str
|
|
332
|
-
| const result
|
|
333
|
-
| const myVar
|
|
334
|
-
| function handle() | Handle what?
|
|
326
|
+
| Banned | Why | Use Instead |
|
|
327
|
+
| ----------------- | ----------------------------- | --------------------------- |
|
|
328
|
+
| const data | Means anything | const users, const response |
|
|
329
|
+
| const temp | Temporary to whom? | const formattedDate |
|
|
330
|
+
| const arr | Hungarian + meaningless | const permissions |
|
|
331
|
+
| const obj | Everything is an object | const config |
|
|
332
|
+
| const str | Hungarian notation | const message |
|
|
333
|
+
| const result | All functions return a result | const validationResult |
|
|
334
|
+
| const myVar | my prefix adds nothing | const username |
|
|
335
|
+
| function handle() | Handle what? | function handleFormSubmit() |
|
|
335
336
|
|
|
336
337
|
#### Boolean Naming
|
|
337
338
|
|
|
338
339
|
Booleans must use a prefix that makes true/false obvious:
|
|
340
|
+
|
|
339
341
|
```
|
|
340
342
|
GOOD: isLoading, hasPermission, canSubmit, shouldRender, isVisible, wasProcessed
|
|
341
343
|
BAD: loading, permission, submit, render, visible, processed
|
|
@@ -376,67 +378,67 @@ Configuration for enforcing domain-based folder modularization across **all** di
|
|
|
376
378
|
|
|
377
379
|
```javascript
|
|
378
380
|
// eslint.config.js (flat config)
|
|
379
|
-
import boundaries from
|
|
380
|
-
import importPlugin from
|
|
381
|
+
import boundaries from 'eslint-plugin-boundaries';
|
|
382
|
+
import importPlugin from 'eslint-plugin-import';
|
|
381
383
|
|
|
382
384
|
export default [
|
|
383
385
|
// ... other configs
|
|
384
386
|
{
|
|
385
387
|
plugins: { boundaries, import: importPlugin },
|
|
386
388
|
settings: {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
+
'boundaries/include': ['src/**/*'],
|
|
390
|
+
'boundaries/elements': [
|
|
389
391
|
// Shared/global files (can be imported by any domain)
|
|
390
392
|
{
|
|
391
|
-
mode:
|
|
392
|
-
type:
|
|
393
|
+
mode: 'full',
|
|
394
|
+
type: 'shared',
|
|
393
395
|
pattern: [
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
396
|
+
'src/services/api-client.ts',
|
|
397
|
+
'src/hooks/use-debounce.ts',
|
|
398
|
+
'src/hooks/use-media-query.ts',
|
|
399
|
+
'src/components/ui/**',
|
|
400
|
+
'src/lib/*.ts',
|
|
401
|
+
'src/lib/*.tsx',
|
|
402
|
+
'src/types/!(*/**).ts',
|
|
401
403
|
],
|
|
402
404
|
},
|
|
403
405
|
// Employee domain -- all directories
|
|
404
406
|
{
|
|
405
|
-
mode:
|
|
406
|
-
type:
|
|
407
|
+
mode: 'full',
|
|
408
|
+
type: 'employee',
|
|
407
409
|
pattern: [
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
410
|
+
'src/services/employee/**',
|
|
411
|
+
'src/hooks/employee/**',
|
|
412
|
+
'src/store/employee/**',
|
|
413
|
+
'src/components/employee/**',
|
|
414
|
+
'src/lib/employee/**',
|
|
415
|
+
'src/types/employee/**',
|
|
414
416
|
],
|
|
415
417
|
},
|
|
416
418
|
// Auth domain -- all directories
|
|
417
419
|
{
|
|
418
|
-
mode:
|
|
419
|
-
type:
|
|
420
|
+
mode: 'full',
|
|
421
|
+
type: 'auth',
|
|
420
422
|
pattern: [
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
423
|
+
'src/services/auth/**',
|
|
424
|
+
'src/hooks/auth/**',
|
|
425
|
+
'src/store/auth/**',
|
|
426
|
+
'src/components/auth/**',
|
|
427
|
+
'src/lib/auth/**',
|
|
428
|
+
'src/types/auth/**',
|
|
427
429
|
],
|
|
428
430
|
},
|
|
429
431
|
// Users domain -- all directories
|
|
430
432
|
{
|
|
431
|
-
mode:
|
|
432
|
-
type:
|
|
433
|
+
mode: 'full',
|
|
434
|
+
type: 'users',
|
|
433
435
|
pattern: [
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
436
|
+
'src/services/users/**',
|
|
437
|
+
'src/hooks/users/**',
|
|
438
|
+
'src/store/users/**',
|
|
439
|
+
'src/components/users/**',
|
|
440
|
+
'src/lib/users/**',
|
|
441
|
+
'src/types/users/**',
|
|
440
442
|
],
|
|
441
443
|
},
|
|
442
444
|
],
|
|
@@ -445,44 +447,48 @@ export default [
|
|
|
445
447
|
// === BOUNDARIES RULES ===
|
|
446
448
|
|
|
447
449
|
// BLOCK: flat/orphaned domain files at directory roots (enforces subdirectory pattern)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
+
'boundaries/entry-point': [
|
|
451
|
+
'error',
|
|
450
452
|
{
|
|
451
|
-
default:
|
|
453
|
+
default: 'disallow',
|
|
452
454
|
rules: [
|
|
453
455
|
// Allow ONLY api-client.ts at the services/ root level
|
|
454
456
|
{
|
|
455
|
-
target: [
|
|
456
|
-
allow:
|
|
457
|
+
target: ['src/services/**/*.ts'],
|
|
458
|
+
allow: 'src/services/api-client.ts',
|
|
457
459
|
},
|
|
458
460
|
// Allow shared utility hooks at root but NOT domain-specific ones
|
|
459
461
|
{
|
|
460
|
-
target: [
|
|
461
|
-
|
|
462
|
+
target: [
|
|
463
|
+
'src/hooks/use-employee*.ts',
|
|
464
|
+
'src/hooks/use-auth*.ts',
|
|
465
|
+
'src/hooks/use-user*.ts',
|
|
466
|
+
],
|
|
467
|
+
disallow: 'src/hooks/',
|
|
462
468
|
},
|
|
463
469
|
// No domain-specific files at store/ root
|
|
464
470
|
{
|
|
465
|
-
target: [
|
|
466
|
-
allow:
|
|
471
|
+
target: ['src/store/**/*.ts'],
|
|
472
|
+
allow: 'src/store/index.ts',
|
|
467
473
|
},
|
|
468
474
|
],
|
|
469
475
|
},
|
|
470
476
|
],
|
|
471
477
|
|
|
472
478
|
// BLOCK: cross-domain imports (e.g., employee component cannot import from auth services)
|
|
473
|
-
|
|
474
|
-
|
|
479
|
+
'boundaries/element-types': [
|
|
480
|
+
'error',
|
|
475
481
|
{
|
|
476
|
-
default:
|
|
482
|
+
default: 'disallow',
|
|
477
483
|
rules: [
|
|
478
484
|
// Shared files can be imported by any domain
|
|
479
|
-
{ from: [
|
|
485
|
+
{ from: ['employee', 'auth', 'users'], allow: ['shared'] },
|
|
480
486
|
// Domains cannot import from other domains
|
|
481
|
-
{ from: [
|
|
482
|
-
{ from: [
|
|
483
|
-
{ from: [
|
|
487
|
+
{ from: ['employee'], disallow: ['auth', 'users'] },
|
|
488
|
+
{ from: ['auth'], disallow: ['employee', 'users'] },
|
|
489
|
+
{ from: ['users'], disallow: ['employee', 'auth'] },
|
|
484
490
|
// Shared can import from shared
|
|
485
|
-
{ from: [
|
|
491
|
+
{ from: ['shared'], allow: ['shared'] },
|
|
486
492
|
],
|
|
487
493
|
},
|
|
488
494
|
],
|
|
@@ -490,47 +496,49 @@ export default [
|
|
|
490
496
|
// === IMPORT RULES: BLOCK deep imports bypassing barrel files ===
|
|
491
497
|
|
|
492
498
|
// BLOCK: importing from a non-index file inside a domain subdirectory
|
|
493
|
-
|
|
494
|
-
|
|
499
|
+
'import/no-internal-modules': [
|
|
500
|
+
'error',
|
|
495
501
|
{
|
|
496
502
|
allow: [
|
|
497
503
|
// Only allow importing the barrel (index.ts) from each domain directory
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
+
'src/services/*/index',
|
|
505
|
+
'src/hooks/*/index',
|
|
506
|
+
'src/store/*/index',
|
|
507
|
+
'src/components/*/index',
|
|
508
|
+
'src/lib/*/index',
|
|
509
|
+
'src/types/*/index',
|
|
504
510
|
// Allow shared/utility files
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
+
'src/services/api-client',
|
|
512
|
+
'src/hooks/use-debounce',
|
|
513
|
+
'src/hooks/use-media-query',
|
|
514
|
+
'src/components/ui/**',
|
|
515
|
+
'src/lib/*',
|
|
516
|
+
'src/types/*',
|
|
511
517
|
// Allow root-level globals
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
518
|
+
'src/app/**',
|
|
519
|
+
'src/styles/**',
|
|
520
|
+
'src/test/**',
|
|
515
521
|
],
|
|
516
522
|
},
|
|
517
523
|
],
|
|
518
524
|
|
|
519
525
|
// BLOCK: importing from index files across domain boundaries (backup for boundaries plugin)
|
|
520
|
-
|
|
521
|
-
|
|
526
|
+
'import/no-restricted-paths': [
|
|
527
|
+
'error',
|
|
522
528
|
{
|
|
523
529
|
zones: [
|
|
524
530
|
// Employee domain may not import from other domains
|
|
525
531
|
{
|
|
526
|
-
target:
|
|
527
|
-
from:
|
|
528
|
-
message:
|
|
532
|
+
target: './src/**/employee/**',
|
|
533
|
+
from: './src/**/auth/**',
|
|
534
|
+
message:
|
|
535
|
+
'Employee domain must not import from Auth domain. Use shared interfaces instead.',
|
|
529
536
|
},
|
|
530
537
|
{
|
|
531
|
-
target:
|
|
532
|
-
from:
|
|
533
|
-
message:
|
|
538
|
+
target: './src/**/employee/**',
|
|
539
|
+
from: './src/**/users/**',
|
|
540
|
+
message:
|
|
541
|
+
'Employee domain must not import from Users domain. Use shared interfaces instead.',
|
|
534
542
|
},
|
|
535
543
|
// (repeat for each domain pair)
|
|
536
544
|
],
|
|
@@ -543,12 +551,12 @@ export default [
|
|
|
543
551
|
|
|
544
552
|
**Enforcement Summary**:
|
|
545
553
|
|
|
546
|
-
| Rule
|
|
547
|
-
|
|
548
|
-
| `boundaries/entry-point`
|
|
549
|
-
| `boundaries/element-types`
|
|
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
|
|
554
|
+
| Rule | What It Catches | Severity |
|
|
555
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
|
|
556
|
+
| `boundaries/entry-point` | Flat domain files at directory roots (e.g., `services/employeeService.ts` instead of `services/employee/`) | `error` |
|
|
557
|
+
| `boundaries/element-types` | Cross-domain imports (e.g., `employee` component importing from `auth` services) | `error` |
|
|
558
|
+
| `import/no-internal-modules` | **Deep imports bypassing barrel files** (e.g., `from '@/services/employee/employee-bio-detail.service'` instead of `from '@/services/employee'`) | `error` |
|
|
559
|
+
| `import/no-restricted-paths` | Backup enforcement of cross-domain import restrictions | `error` |
|
|
552
560
|
|
|
553
561
|
**Barrel File Enforcement Flow**:
|
|
554
562
|
|
|
@@ -567,6 +575,7 @@ export default [
|
|
|
567
575
|
```
|
|
568
576
|
|
|
569
577
|
**Adding a New Domain**:
|
|
578
|
+
|
|
570
579
|
1. Create the domain subdirectories across all folders:
|
|
571
580
|
```
|
|
572
581
|
services/<domain>/
|
|
@@ -601,17 +610,17 @@ Is it a transformer?--> format/convert prefix
|
|
|
601
610
|
const d = new Date();
|
|
602
611
|
const r = await fetch('/api/users');
|
|
603
612
|
const arr = [1, 2, 3];
|
|
604
|
-
const temp = users.filter(u => u.active);
|
|
605
|
-
function get() {
|
|
606
|
-
function handle() {
|
|
613
|
+
const temp = users.filter((u) => u.active);
|
|
614
|
+
function get() {}
|
|
615
|
+
function handle() {}
|
|
607
616
|
|
|
608
617
|
// GOOD -- Self-documenting
|
|
609
618
|
const currentDate = new Date();
|
|
610
619
|
const response = await fetch('/api/users');
|
|
611
620
|
const userIds = [1, 2, 3];
|
|
612
|
-
const activeUsers = users.filter(user => user.isActive);
|
|
613
|
-
function getActiveUsers() {
|
|
614
|
-
function handleFormSubmit() {
|
|
621
|
+
const activeUsers = users.filter((user) => user.isActive);
|
|
622
|
+
function getActiveUsers() {}
|
|
623
|
+
function handleFormSubmit() {}
|
|
615
624
|
```
|
|
616
625
|
|
|
617
626
|
### 5.3 Import Order Standard
|
|
@@ -632,6 +641,7 @@ import { UserCard } from './user-card';
|
|
|
632
641
|
### 5.4 Lovable-Generated Code Handling
|
|
633
642
|
|
|
634
643
|
When Lovable generates UI templates, always:
|
|
644
|
+
|
|
635
645
|
1. Run generated code through ESLint + Prettier before committing.
|
|
636
646
|
2. Extract reusable components into components/shared/ directory.
|
|
637
647
|
3. Add proper TypeScript types to any untyped props.
|
|
@@ -663,6 +673,7 @@ export default {
|
|
|
663
673
|
### 6.2 Format-On-Save (VS Code)
|
|
664
674
|
|
|
665
675
|
Every project must have .vscode/settings.json:
|
|
676
|
+
|
|
666
677
|
```json
|
|
667
678
|
{
|
|
668
679
|
"editor.formatOnSave": true,
|
|
@@ -741,7 +752,10 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
741
752
|
}
|
|
742
753
|
|
|
743
754
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
744
|
-
logger.error('ErrorBoundary caught an error', {
|
|
755
|
+
logger.error('ErrorBoundary caught an error', {
|
|
756
|
+
error,
|
|
757
|
+
componentStack: errorInfo.componentStack,
|
|
758
|
+
});
|
|
745
759
|
this.props.onError?.(error, errorInfo);
|
|
746
760
|
}
|
|
747
761
|
|
|
@@ -754,7 +768,10 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
754
768
|
<div role="alert" className="flex flex-col items-center justify-center min-h-[400px] p-8">
|
|
755
769
|
<h2 className="text-xl font-semibold mb-2">Something went wrong</h2>
|
|
756
770
|
<p className="text-muted-foreground mb-4">An unexpected error occurred.</p>
|
|
757
|
-
<button
|
|
771
|
+
<button
|
|
772
|
+
onClick={this.handleReset}
|
|
773
|
+
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
|
|
774
|
+
>
|
|
758
775
|
Try Again
|
|
759
776
|
</button>
|
|
760
777
|
</div>
|
|
@@ -766,6 +783,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
766
783
|
```
|
|
767
784
|
|
|
768
785
|
**Usage**:
|
|
786
|
+
|
|
769
787
|
```tsx
|
|
770
788
|
// Route-level wrapping
|
|
771
789
|
<ErrorBoundary onError={(err) => captureException(err)}>
|
|
@@ -774,6 +792,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
774
792
|
```
|
|
775
793
|
|
|
776
794
|
**Rules**:
|
|
795
|
+
|
|
777
796
|
- One ErrorBoundary per route (not per component) for granularity
|
|
778
797
|
- Log errors to Sentry via `onError` callback
|
|
779
798
|
- Never wrap individual small components (<50 lines) in their own ErrorBoundary
|
|
@@ -806,6 +825,7 @@ export function EmployeeRoutes() {
|
|
|
806
825
|
```
|
|
807
826
|
|
|
808
827
|
**Rules**:
|
|
828
|
+
|
|
809
829
|
- Every page/route component must use `React.lazy()` + dynamic `import()`
|
|
810
830
|
- Every lazy route must have a `<Suspense>` wrapper with a skeleton fallback
|
|
811
831
|
- Prefetch critical routes (dashboard, home) on link hover or visibility
|
|
@@ -816,12 +836,12 @@ export function EmployeeRoutes() {
|
|
|
816
836
|
|
|
817
837
|
**Loading States**:
|
|
818
838
|
|
|
819
|
-
| Context
|
|
820
|
-
|
|
821
|
-
| Page load
|
|
822
|
-
| Table/list data | Skeleton rows or spinner overlay
|
|
823
|
-
| Button action
|
|
824
|
-
| File upload
|
|
839
|
+
| Context | Pattern | When |
|
|
840
|
+
| --------------- | -------------------------------------- | ------------------------- |
|
|
841
|
+
| Page load | Skeleton screen | Initial page navigation |
|
|
842
|
+
| Table/list data | Skeleton rows or spinner overlay | Data fetching |
|
|
843
|
+
| Button action | Spinner inside button + disabled state | Form submit, save, delete |
|
|
844
|
+
| File upload | Progress bar | Large uploads |
|
|
825
845
|
|
|
826
846
|
Debounce loader appearance: do not show any loading indicator for fetches that resolve in < 300ms (avoid flash).
|
|
827
847
|
|
|
@@ -832,7 +852,8 @@ function EmployeePage() {
|
|
|
832
852
|
|
|
833
853
|
if (isLoading) return <PageSkeleton />;
|
|
834
854
|
if (isError) return <ErrorState error={error} onRetry={refetch} />;
|
|
835
|
-
if (!data)
|
|
855
|
+
if (!data)
|
|
856
|
+
return <EmptyState message="No employee data found" action={<CreateEmployeeButton />} />;
|
|
836
857
|
|
|
837
858
|
return <EmployeeProfile employee={data} />;
|
|
838
859
|
}
|
|
@@ -849,14 +870,22 @@ interface ErrorStateProps {
|
|
|
849
870
|
|
|
850
871
|
export function ErrorState({ error, onRetry, title = 'Something went wrong' }: ErrorStateProps) {
|
|
851
872
|
return (
|
|
852
|
-
<div
|
|
873
|
+
<div
|
|
874
|
+
role="alert"
|
|
875
|
+
className="flex flex-col items-center justify-center min-h-[300px] p-6 text-center"
|
|
876
|
+
>
|
|
853
877
|
<AlertTriangle className="w-12 h-12 text-destructive mb-4" />
|
|
854
878
|
<h3 className="text-lg font-semibold mb-1">{title}</h3>
|
|
855
879
|
<p className="text-muted-foreground mb-4 max-w-md">
|
|
856
|
-
{typeof error === 'string'
|
|
880
|
+
{typeof error === 'string'
|
|
881
|
+
? error
|
|
882
|
+
: error?.message || 'An unexpected error occurred. Please try again.'}
|
|
857
883
|
</p>
|
|
858
884
|
{onRetry && (
|
|
859
|
-
<button
|
|
885
|
+
<button
|
|
886
|
+
onClick={onRetry}
|
|
887
|
+
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
|
|
888
|
+
>
|
|
860
889
|
Try Again
|
|
861
890
|
</button>
|
|
862
891
|
)}
|
|
@@ -869,16 +898,16 @@ export function ErrorState({ error, onRetry, title = 'Something went wrong' }: E
|
|
|
869
898
|
|
|
870
899
|
Every UI component must meet WCAG 2.1 AA standards. The following are **non-negotiable**:
|
|
871
900
|
|
|
872
|
-
| Requirement
|
|
873
|
-
|
|
874
|
-
| Color contrast
|
|
875
|
-
| Keyboard navigation | All interactive elements reachable via Tab. Logical tab order. Visible focus ring.
|
|
876
|
-
| Skip link
|
|
877
|
-
| Screen reader
|
|
878
|
-
| Form accessibility
|
|
879
|
-
| Modals/Dialogs
|
|
880
|
-
| Images
|
|
881
|
-
| Landmarks
|
|
901
|
+
| Requirement | Implementation |
|
|
902
|
+
| ------------------- | ------------------------------------------------------------------------------------------------ |
|
|
903
|
+
| Color contrast | Minimum 4.5:1 for text, 3:1 for large text. Verify with axe DevTools. |
|
|
904
|
+
| Keyboard navigation | All interactive elements reachable via Tab. Logical tab order. Visible focus ring. |
|
|
905
|
+
| Skip link | Skip-to-content link as first focusable element on every page. |
|
|
906
|
+
| Screen reader | `aria-label` on icon-only buttons. `aria-describedby` linking inputs to error messages. |
|
|
907
|
+
| Form accessibility | Every `<input>` must have an associated `<label>`. Error messages linked via `aria-describedby`. |
|
|
908
|
+
| Modals/Dialogs | Focus trap inside modal. Escape to close. `aria-modal="true"`. |
|
|
909
|
+
| Images | All `<img>` must have `alt` attribute (descriptive or empty for decorative). |
|
|
910
|
+
| Landmarks | Use semantic HTML: `<header>`, `<main>`, `<nav>`, `<footer>`. |
|
|
882
911
|
|
|
883
912
|
**Required tools**: axe DevTools browser extension, eslint-plugin-jsx-a11y (already enforced via ESLint).
|
|
884
913
|
|
|
@@ -891,6 +920,7 @@ Every UI component must meet WCAG 2.1 AA standards. The following are **non-nego
|
|
|
891
920
|
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
921
|
|
|
893
922
|
Required table features:
|
|
923
|
+
|
|
894
924
|
- Sort on click for all non-action columns with sort direction indicator
|
|
895
925
|
- Filter input above table with 300-400ms debounce
|
|
896
926
|
- Column visibility toggle
|
|
@@ -903,6 +933,7 @@ Required table features:
|
|
|
903
933
|
#### Jump-to-Top Button (Required on Long Pages)
|
|
904
934
|
|
|
905
935
|
Pages taller than 2x viewport MUST include a fixed back-to-top button:
|
|
936
|
+
|
|
906
937
|
- Appears after scrolling 500px
|
|
907
938
|
- Smooth scroll to top
|
|
908
939
|
- Hidden when at top
|
|
@@ -912,6 +943,7 @@ Pages taller than 2x viewport MUST include a fixed back-to-top button:
|
|
|
912
943
|
#### Go Back / Go to Previous (Context-Aware Navigation)
|
|
913
944
|
|
|
914
945
|
Every page except the home/landing page MUST provide navigation aids:
|
|
946
|
+
|
|
915
947
|
- Breadcrumbs showing full path: Home > Reports > Financial > Q1 2026
|
|
916
948
|
- Back button returning to parent list or previous page
|
|
917
949
|
- Context-aware return preserving list filters and pagination via query params
|
|
@@ -919,6 +951,7 @@ Every page except the home/landing page MUST provide navigation aids:
|
|
|
919
951
|
#### Corporate Branding (Required)
|
|
920
952
|
|
|
921
953
|
Every application MUST implement consistent corporate branding:
|
|
954
|
+
|
|
922
955
|
- Primary logo in header/sidebar, clickable to home, with favicon and Apple touch icon
|
|
923
956
|
- Brand colors defined in Tailwind config with minimum 4.5:1 contrast ratio
|
|
924
957
|
- Typography: Inter or system fonts, defined heading scale, 1rem base body text
|
|
@@ -928,7 +961,6 @@ Every application MUST implement consistent corporate branding:
|
|
|
928
961
|
|
|
929
962
|
---
|
|
930
963
|
|
|
931
|
-
|
|
932
964
|
---
|
|
933
965
|
|
|
934
966
|
## 8. Testing Standards
|
|
@@ -947,12 +979,12 @@ Every application MUST implement consistent corporate branding:
|
|
|
947
979
|
|
|
948
980
|
### 8.2 Coverage Requirements
|
|
949
981
|
|
|
950
|
-
| Level
|
|
951
|
-
|
|
952
|
-
| Line
|
|
953
|
-
| Branch
|
|
954
|
-
| New Code
|
|
955
|
-
| Critical Paths | 100%
|
|
982
|
+
| Level | Minimum Coverage | Notes |
|
|
983
|
+
| -------------- | ---------------- | --------------------------- |
|
|
984
|
+
| Line | 80% | Enforced in CI pipeline |
|
|
985
|
+
| Branch | 75% | Enforced in CI pipeline |
|
|
986
|
+
| New Code | 90% | PR checks must pass |
|
|
987
|
+
| Critical Paths | 100% | Auth, payments, data export |
|
|
956
988
|
|
|
957
989
|
### 8.3 Test Structure
|
|
958
990
|
|
|
@@ -1011,6 +1043,7 @@ main # Production-ready code
|
|
|
1011
1043
|
**Types**: feat, fix, security, perf, docs, style, refactor, test, chore
|
|
1012
1044
|
|
|
1013
1045
|
**Examples**:
|
|
1046
|
+
|
|
1014
1047
|
```
|
|
1015
1048
|
feat(auth): add JWT refresh token flow
|
|
1016
1049
|
fix(profile): resolve avatar upload timeout
|
|
@@ -1020,13 +1053,14 @@ chore(deps): upgrade React to v19
|
|
|
1020
1053
|
|
|
1021
1054
|
### 9.3 Git Hooks (Husky)
|
|
1022
1055
|
|
|
1023
|
-
| Hook
|
|
1024
|
-
|
|
1025
|
-
| pre-commit | Lint staged files
|
|
1026
|
-
| commit-msg | Validate commit message
|
|
1027
|
-
| pre-push
|
|
1056
|
+
| Hook | Action | Tool |
|
|
1057
|
+
| ---------- | ---------------------------------------------- | -------------------------------------------- |
|
|
1058
|
+
| pre-commit | Lint staged files | lint-staged |
|
|
1059
|
+
| commit-msg | Validate commit message | commitlint |
|
|
1060
|
+
| pre-push | Run lint + modularization + type check + tests | eslint + check-modularization + tsc + vitest |
|
|
1028
1061
|
|
|
1029
1062
|
**lint-staged config**:
|
|
1063
|
+
|
|
1030
1064
|
```json
|
|
1031
1065
|
{
|
|
1032
1066
|
"src/**/*.{ts,tsx}": ["eslint --fix", "prettier --write"],
|
|
@@ -1058,9 +1092,11 @@ chore(deps): upgrade React to v19
|
|
|
1058
1092
|
|
|
1059
1093
|
```markdown
|
|
1060
1094
|
## Description
|
|
1095
|
+
|
|
1061
1096
|
[Brief description of the change]
|
|
1062
1097
|
|
|
1063
1098
|
## Type of Change
|
|
1099
|
+
|
|
1064
1100
|
- [ ] feat: New feature
|
|
1065
1101
|
- [ ] fix: Bug fix
|
|
1066
1102
|
- [ ] refactor: Code restructure
|
|
@@ -1069,6 +1105,7 @@ chore(deps): upgrade React to v19
|
|
|
1069
1105
|
- [ ] chore: Build/tooling
|
|
1070
1106
|
|
|
1071
1107
|
## Testing Done
|
|
1108
|
+
|
|
1072
1109
|
- [ ] Unit tests added/updated
|
|
1073
1110
|
- [ ] Manual testing performed
|
|
1074
1111
|
- [ ] E2E tests (if applicable)
|
|
@@ -1076,6 +1113,7 @@ chore(deps): upgrade React to v19
|
|
|
1076
1113
|
## Screenshots (if UI change)
|
|
1077
1114
|
|
|
1078
1115
|
## Checklist
|
|
1116
|
+
|
|
1079
1117
|
- [ ] Code follows project style guidelines
|
|
1080
1118
|
- [ ] Self-review completed
|
|
1081
1119
|
- [ ] No breaking changes (or documented)
|
|
@@ -1086,31 +1124,37 @@ chore(deps): upgrade React to v19
|
|
|
1086
1124
|
### 10.3 Review Checklist
|
|
1087
1125
|
|
|
1088
1126
|
**Functional**:
|
|
1127
|
+
|
|
1089
1128
|
- Does the code do what its supposed to?
|
|
1090
1129
|
- Are edge cases handled (loading, empty, error states)?
|
|
1091
1130
|
- Are there appropriate tests?
|
|
1092
1131
|
|
|
1093
1132
|
**Architecture**:
|
|
1133
|
+
|
|
1094
1134
|
- Is the component properly decomposed?
|
|
1095
1135
|
- Is business logic separated from UI?
|
|
1096
1136
|
- Are state management decisions appropriate?
|
|
1097
1137
|
|
|
1098
1138
|
**Security**:
|
|
1139
|
+
|
|
1099
1140
|
- Are user inputs validated and sanitized?
|
|
1100
1141
|
- Are secrets exposed anywhere?
|
|
1101
1142
|
- Is authentication/authorization enforced?
|
|
1102
1143
|
|
|
1103
1144
|
**Performance**:
|
|
1145
|
+
|
|
1104
1146
|
- Are there unnecessary re-renders?
|
|
1105
1147
|
- Are large lists properly virtualized?
|
|
1106
1148
|
- Are images optimized?
|
|
1107
1149
|
|
|
1108
1150
|
**Accessibility**:
|
|
1151
|
+
|
|
1109
1152
|
- Are all interactive elements keyboard-accessible?
|
|
1110
1153
|
- Do images have alt text?
|
|
1111
1154
|
- Is color contrast sufficient?
|
|
1112
1155
|
|
|
1113
1156
|
**Lovable Code**:
|
|
1157
|
+
|
|
1114
1158
|
- Has the generated code been cleaned up (no unused imports, proper types)?
|
|
1115
1159
|
- Are generated components properly integrated?
|
|
1116
1160
|
|
|
@@ -1186,7 +1230,7 @@ jobs:
|
|
|
1186
1230
|
steps:
|
|
1187
1231
|
- uses: actions/checkout@v4
|
|
1188
1232
|
- uses: actions/setup-node@v4
|
|
1189
|
-
with: { node-version:
|
|
1233
|
+
with: { node-version: '20' }
|
|
1190
1234
|
- uses: pnpm/action-setup@v2
|
|
1191
1235
|
with: { version: 9 }
|
|
1192
1236
|
|
|
@@ -1244,11 +1288,11 @@ CMD ["node", "dist/server.js"]
|
|
|
1244
1288
|
|
|
1245
1289
|
### 12.2 Environment Strategy
|
|
1246
1290
|
|
|
1247
|
-
| Environment | Purpose
|
|
1248
|
-
|
|
1249
|
-
| Dev
|
|
1250
|
-
| Staging
|
|
1251
|
-
| Production
|
|
1291
|
+
| Environment | Purpose | Deploy Trigger | Replicas |
|
|
1292
|
+
| ----------- | ----------------- | --------------- | --------------- |
|
|
1293
|
+
| Dev | Developer testing | Push to develop | 1 |
|
|
1294
|
+
| Staging | QA + UAT | Push to main | 2 |
|
|
1295
|
+
| Production | Live | Manual approval | 3+ (auto-scale) |
|
|
1252
1296
|
|
|
1253
1297
|
### 12.3 Deployment Checklist
|
|
1254
1298
|
|
|
@@ -1267,17 +1311,17 @@ CMD ["node", "dist/server.js"]
|
|
|
1267
1311
|
|
|
1268
1312
|
### 13.1 Mandatory Security Measures
|
|
1269
1313
|
|
|
1270
|
-
| Measure
|
|
1271
|
-
|
|
1272
|
-
| HTTPS
|
|
1273
|
-
| CSP Headers
|
|
1274
|
-
| Input Validation
|
|
1275
|
-
| XSS Protection
|
|
1276
|
-
| CSRF Protection
|
|
1277
|
-
| Rate Limiting
|
|
1278
|
-
| Dependency Scanning | npm audit / Snyk in CI
|
|
1279
|
-
| Secret Scanning
|
|
1280
|
-
| CORS
|
|
1314
|
+
| Measure | Implementation | Priority |
|
|
1315
|
+
| ------------------- | ------------------------------------------ | -------- |
|
|
1316
|
+
| HTTPS | Enforced at ingress level | Critical |
|
|
1317
|
+
| CSP Headers | Content Security Policy via server headers | Critical |
|
|
1318
|
+
| Input Validation | Zod schemas for all user inputs | Critical |
|
|
1319
|
+
| XSS Protection | React escaping + DOMPurify for raw HTML | Critical |
|
|
1320
|
+
| CSRF Protection | SameSite cookies + CSRF tokens | High |
|
|
1321
|
+
| Rate Limiting | API endpoints (especially auth) | High |
|
|
1322
|
+
| Dependency Scanning | npm audit / Snyk in CI | High |
|
|
1323
|
+
| Secret Scanning | GitLeaks / TruffleHog in CI | Medium |
|
|
1324
|
+
| CORS | Whitelist allowed origins | High |
|
|
1281
1325
|
|
|
1282
1326
|
### 13.2 Secrets NEVER Committed
|
|
1283
1327
|
|
|
@@ -1316,23 +1360,28 @@ Console and debugger rules: `console.log`, `console.debug`, `console.info`, `con
|
|
|
1316
1360
|
Allowed only: `console.warn()` and `console.error()`. Use a structured logger instead.
|
|
1317
1361
|
|
|
1318
1362
|
ESLint rules:
|
|
1363
|
+
|
|
1319
1364
|
```
|
|
1320
1365
|
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
1321
1366
|
'no-debugger': 'error',
|
|
1322
1367
|
```
|
|
1323
1368
|
|
|
1324
1369
|
Structured logger replacement:
|
|
1370
|
+
|
|
1325
1371
|
```typescript
|
|
1326
1372
|
const isDev = import.meta.env.DEV;
|
|
1327
1373
|
|
|
1328
1374
|
export const logger = {
|
|
1329
|
-
debug: (...args: unknown[]) => {
|
|
1375
|
+
debug: (...args: unknown[]) => {
|
|
1376
|
+
if (isDev) console.debug('[DEBUG]', ...args);
|
|
1377
|
+
},
|
|
1330
1378
|
warn: (...args: unknown[]) => console.warn('[WARN]', ...args),
|
|
1331
1379
|
error: (...args: unknown[]) => console.error('[ERROR]', ...args),
|
|
1332
1380
|
};
|
|
1333
1381
|
```
|
|
1334
1382
|
|
|
1335
1383
|
Production stripping (Vite/Terser):
|
|
1384
|
+
|
|
1336
1385
|
```typescript
|
|
1337
1386
|
build: {
|
|
1338
1387
|
minify: 'terser',
|
|
@@ -1354,6 +1403,7 @@ build: {
|
|
|
1354
1403
|
Sensitive fields must NEVER appear in network responses visible in the browser Network tab.
|
|
1355
1404
|
|
|
1356
1405
|
Mask these data types:
|
|
1406
|
+
|
|
1357
1407
|
- Auth tokens (JWT, session IDs)
|
|
1358
1408
|
- Passwords, password hashes, salts
|
|
1359
1409
|
- PII (SSN, phone, address, email)
|
|
@@ -1364,15 +1414,30 @@ Mask these data types:
|
|
|
1364
1414
|
Strategy 1: Use public DTOs in service layer so the backend only returns safe fields.
|
|
1365
1415
|
|
|
1366
1416
|
Strategy 2: Global response interceptor masking sensitive fields.
|
|
1417
|
+
|
|
1367
1418
|
```typescript
|
|
1368
1419
|
class ApiClient {
|
|
1369
1420
|
private SENSITIVE_PATTERNS = [
|
|
1370
|
-
'password',
|
|
1371
|
-
'
|
|
1372
|
-
'
|
|
1373
|
-
'
|
|
1374
|
-
'
|
|
1375
|
-
'
|
|
1421
|
+
'password',
|
|
1422
|
+
'passwordhash',
|
|
1423
|
+
'salt',
|
|
1424
|
+
'ssn',
|
|
1425
|
+
'socialsecurity',
|
|
1426
|
+
'nationalid',
|
|
1427
|
+
'internalid',
|
|
1428
|
+
'databaseid',
|
|
1429
|
+
'legacyid',
|
|
1430
|
+
'creditcardnumber',
|
|
1431
|
+
'cvv',
|
|
1432
|
+
'bankaccount',
|
|
1433
|
+
'apikey',
|
|
1434
|
+
'privatekey',
|
|
1435
|
+
'token',
|
|
1436
|
+
'sessionid',
|
|
1437
|
+
'salary',
|
|
1438
|
+
'income',
|
|
1439
|
+
'debt',
|
|
1440
|
+
'revenue',
|
|
1376
1441
|
];
|
|
1377
1442
|
|
|
1378
1443
|
constructor() {
|
|
@@ -1384,11 +1449,11 @@ class ApiClient {
|
|
|
1384
1449
|
|
|
1385
1450
|
private maskSensitiveData(data: unknown): unknown {
|
|
1386
1451
|
if (typeof data !== 'object' || data === null) return data;
|
|
1387
|
-
if (Array.isArray(data)) return data.map(item => this.maskSensitiveData(item));
|
|
1452
|
+
if (Array.isArray(data)) return data.map((item) => this.maskSensitiveData(item));
|
|
1388
1453
|
const masked: Record<string, unknown> = {};
|
|
1389
1454
|
for (const [key, value] of Object.entries(data)) {
|
|
1390
1455
|
const lowerKey = key.toLowerCase();
|
|
1391
|
-
if (this.SENSITIVE_PATTERNS.some(p => lowerKey.includes(p))) {
|
|
1456
|
+
if (this.SENSITIVE_PATTERNS.some((p) => lowerKey.includes(p))) {
|
|
1392
1457
|
masked[key] = '[REDACTED]';
|
|
1393
1458
|
} else if (typeof value === 'object' && value !== null) {
|
|
1394
1459
|
masked[key] = this.maskSensitiveData(value);
|
|
@@ -1402,6 +1467,7 @@ class ApiClient {
|
|
|
1402
1467
|
```
|
|
1403
1468
|
|
|
1404
1469
|
Strategy 3: Disable caching for sensitive endpoints.
|
|
1470
|
+
|
|
1405
1471
|
```typescript
|
|
1406
1472
|
export const sensitiveService = {
|
|
1407
1473
|
getFinancialData: () =>
|
|
@@ -1412,22 +1478,24 @@ export const sensitiveService = {
|
|
|
1412
1478
|
```
|
|
1413
1479
|
|
|
1414
1480
|
Production verification checklist:
|
|
1481
|
+
|
|
1415
1482
|
- Open DevTools > Network tab
|
|
1416
1483
|
- Verify no Authorization header visible (use httpOnly cookies)
|
|
1417
1484
|
- Verify no passwords, SSNs, or internal IDs in request bodies
|
|
1418
1485
|
- Verify masked responses show [REDACTED] for sensitive fields
|
|
1419
1486
|
|
|
1420
|
-
|
|
1421
1487
|
### 13.6 Authorization: RBAC, ABAC, Guards, and DevMode Safety
|
|
1422
1488
|
|
|
1423
1489
|
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
1490
|
|
|
1425
1491
|
Core rules:
|
|
1492
|
+
|
|
1426
1493
|
- No feature, page, action, button, table row, or API call may be rendered or executed without an authorization check
|
|
1427
1494
|
- Authorized state MUST NOT be derivable from the UI alone in developer mode
|
|
1428
1495
|
- Authorization checks MUST be centralized
|
|
1429
1496
|
|
|
1430
1497
|
RBAC structure:
|
|
1498
|
+
|
|
1431
1499
|
```tsx
|
|
1432
1500
|
export enum Role {
|
|
1433
1501
|
SUPER_ADMIN = 'super_admin',
|
|
@@ -1448,6 +1516,7 @@ export enum Permission {
|
|
|
1448
1516
|
```
|
|
1449
1517
|
|
|
1450
1518
|
ABAC structure:
|
|
1519
|
+
|
|
1451
1520
|
```tsx
|
|
1452
1521
|
export type AbacContext = {
|
|
1453
1522
|
role: Role;
|
|
@@ -1475,13 +1544,14 @@ export const canAccessResource: AbacPolicy = (ctx, action, resource) => {
|
|
|
1475
1544
|
```
|
|
1476
1545
|
|
|
1477
1546
|
Route guards:
|
|
1547
|
+
|
|
1478
1548
|
```tsx
|
|
1479
1549
|
export function RequireAuth({ roles, permissions, abacCheck, children }: RequireAuthProps) {
|
|
1480
1550
|
const auth = useAuth();
|
|
1481
1551
|
const location = useLocation();
|
|
1482
|
-
if (!auth.isAuthenticated) return <Navigate to=
|
|
1552
|
+
if (!auth.isAuthenticated) return <Navigate to="/login" state={{ from: location }} replace />;
|
|
1483
1553
|
const hasRole = roles ? roles.includes(auth.role) : true;
|
|
1484
|
-
const hasPermission = permissions ? permissions.every(p => auth.permissions.includes(p)) : true;
|
|
1554
|
+
const hasPermission = permissions ? permissions.every((p) => auth.permissions.includes(p)) : true;
|
|
1485
1555
|
const abacPass = abacCheck ? abacCheck(auth.abacContext) : true;
|
|
1486
1556
|
if (!hasRole || !hasPermission || !abacPass) return <Forbidden />;
|
|
1487
1557
|
return <>{children}</>;
|
|
@@ -1489,11 +1559,12 @@ export function RequireAuth({ roles, permissions, abacCheck, children }: Require
|
|
|
1489
1559
|
```
|
|
1490
1560
|
|
|
1491
1561
|
Component guards:
|
|
1562
|
+
|
|
1492
1563
|
```tsx
|
|
1493
1564
|
export function Can({ roles, permissions, abacCheck, fallback = null, children }: CanProps) {
|
|
1494
1565
|
const auth = useAuth();
|
|
1495
1566
|
const hasRole = roles ? roles.includes(auth.role) : true;
|
|
1496
|
-
const hasPermission = permissions ? permissions.some(p => auth.permissions.includes(p)) : true;
|
|
1567
|
+
const hasPermission = permissions ? permissions.some((p) => auth.permissions.includes(p)) : true;
|
|
1497
1568
|
const abacPass = abacCheck ? abacCheck(auth.abacContext) : true;
|
|
1498
1569
|
if (!hasRole || !hasPermission || !abacPass) return <>{fallback}</>;
|
|
1499
1570
|
return <>{children}</>;
|
|
@@ -1501,6 +1572,7 @@ export function Can({ roles, permissions, abacCheck, fallback = null, children }
|
|
|
1501
1572
|
```
|
|
1502
1573
|
|
|
1503
1574
|
Data guards:
|
|
1575
|
+
|
|
1504
1576
|
```tsx
|
|
1505
1577
|
export function useAuthorizedUser(id: string) {
|
|
1506
1578
|
const auth = useAuth();
|
|
@@ -1508,7 +1580,13 @@ export function useAuthorizedUser(id: string) {
|
|
|
1508
1580
|
queryKey: ['user', id],
|
|
1509
1581
|
queryFn: async () => {
|
|
1510
1582
|
const data = await usersService.getById(id);
|
|
1511
|
-
if (
|
|
1583
|
+
if (
|
|
1584
|
+
!canAccessResource(auth.abacContext, 'users:read', {
|
|
1585
|
+
type: 'user',
|
|
1586
|
+
id,
|
|
1587
|
+
tenantId: data.tenantId,
|
|
1588
|
+
})
|
|
1589
|
+
) {
|
|
1512
1590
|
throw new Error('Forbidden');
|
|
1513
1591
|
}
|
|
1514
1592
|
return data;
|
|
@@ -1518,11 +1596,13 @@ export function useAuthorizedUser(id: string) {
|
|
|
1518
1596
|
```
|
|
1519
1597
|
|
|
1520
1598
|
DevMode safety:
|
|
1599
|
+
|
|
1521
1600
|
- Dev-only endpoints, hidden buttons, debug flags MUST NOT bypass authorization
|
|
1522
1601
|
- Hardcoded ADMIN=true query params, secret URL paths MUST be treated as security bugs
|
|
1523
1602
|
- Impersonation and elevated roles MUST be logged and auditable
|
|
1524
1603
|
|
|
1525
1604
|
ESLint enforcement:
|
|
1605
|
+
|
|
1526
1606
|
```
|
|
1527
1607
|
no-restricted-syntax: ['error', {
|
|
1528
1608
|
selector: "MemberExpression[property.name='role']",
|
|
@@ -1531,30 +1611,32 @@ no-restricted-syntax: ['error', {
|
|
|
1531
1611
|
```
|
|
1532
1612
|
|
|
1533
1613
|
Authorization code review checklist:
|
|
1614
|
+
|
|
1534
1615
|
- Every protected route uses RequireAuth
|
|
1535
1616
|
- Every sensitive action uses Can
|
|
1536
1617
|
- ABAC policies for tenant, ownership, region, or time restrictions
|
|
1537
1618
|
- No inline role/permission checks in JSX
|
|
1538
1619
|
- No hidden dev paths bypassing authorization
|
|
1539
1620
|
- No mock authorization used outside tests
|
|
1621
|
+
|
|
1540
1622
|
## 14. Monitoring & Observability
|
|
1541
1623
|
|
|
1542
1624
|
### 14.1 Required Instrumentation
|
|
1543
1625
|
|
|
1544
|
-
| Tool
|
|
1545
|
-
|
|
1546
|
-
| Sentry
|
|
1547
|
-
| Lighthouse CI
|
|
1548
|
-
| Console Logging | Structured logging
|
|
1626
|
+
| Tool | Purpose | Implementation |
|
|
1627
|
+
| --------------- | ------------------------- | ---------------------------- |
|
|
1628
|
+
| Sentry | Error tracking (frontend) | @sentry/react |
|
|
1629
|
+
| Lighthouse CI | Performance budget | GitHub Action |
|
|
1630
|
+
| Console Logging | Structured logging | No console.log in production |
|
|
1549
1631
|
|
|
1550
1632
|
### 14.2 Performance Budget
|
|
1551
1633
|
|
|
1552
|
-
| Metric
|
|
1553
|
-
|
|
1554
|
-
| First Contentful Paint (FCP)
|
|
1555
|
-
| Largest Contentful Paint (LCP) | < 2.5s
|
|
1556
|
-
| Time to Interactive (TTI)
|
|
1557
|
-
| Bundle Size (initial)
|
|
1634
|
+
| Metric | Budget | Enforcement |
|
|
1635
|
+
| ------------------------------ | --------------- | ------------------ |
|
|
1636
|
+
| First Contentful Paint (FCP) | < 1.5s | Lighthouse CI |
|
|
1637
|
+
| Largest Contentful Paint (LCP) | < 2.5s | Lighthouse CI |
|
|
1638
|
+
| Time to Interactive (TTI) | < 3.5s | Lighthouse CI |
|
|
1639
|
+
| Bundle Size (initial) | < 200KB gzipped | size-limit package |
|
|
1558
1640
|
|
|
1559
1641
|
---
|
|
1560
1642
|
|
|
@@ -1562,21 +1644,23 @@ Authorization code review checklist:
|
|
|
1562
1644
|
|
|
1563
1645
|
### 15.1 Minimum Required Docs
|
|
1564
1646
|
|
|
1565
|
-
| File
|
|
1566
|
-
|
|
1567
|
-
| README.md
|
|
1568
|
-
| CONTRIBUTING.md
|
|
1569
|
-
| .env.example
|
|
1570
|
-
| docs/architecture.md | High-level architecture decisions
|
|
1571
|
-
| docs/onboarding.md
|
|
1647
|
+
| File | Purpose | Required |
|
|
1648
|
+
| -------------------- | ----------------------------------- | -------------------- |
|
|
1649
|
+
| README.md | Project overview, setup, commands | Yes |
|
|
1650
|
+
| CONTRIBUTING.md | How to contribute, coding standards | Yes |
|
|
1651
|
+
| .env.example | All required environment variables | Yes |
|
|
1652
|
+
| docs/architecture.md | High-level architecture decisions | For complex projects |
|
|
1653
|
+
| docs/onboarding.md | Setup checklist for new developers | For monorepos |
|
|
1572
1654
|
|
|
1573
1655
|
### 15.2 README Template
|
|
1574
1656
|
|
|
1575
|
-
|
|
1657
|
+
````markdown
|
|
1576
1658
|
# Project Name
|
|
1659
|
+
|
|
1577
1660
|
[Brief description]
|
|
1578
1661
|
|
|
1579
1662
|
## Quick Start
|
|
1663
|
+
|
|
1580
1664
|
```bash
|
|
1581
1665
|
git clone <repo>
|
|
1582
1666
|
cd project
|
|
@@ -1584,8 +1668,10 @@ pnpm install
|
|
|
1584
1668
|
cp .env.example .env
|
|
1585
1669
|
pnpm dev
|
|
1586
1670
|
```
|
|
1671
|
+
````
|
|
1587
1672
|
|
|
1588
1673
|
## Available Scripts
|
|
1674
|
+
|
|
1589
1675
|
| Command | Description |
|
|
1590
1676
|
| pnpm dev | Start development server |
|
|
1591
1677
|
| pnpm build | Build for production |
|
|
@@ -1593,8 +1679,10 @@ pnpm dev
|
|
|
1593
1679
|
| pnpm lint | Lint code |
|
|
1594
1680
|
|
|
1595
1681
|
## Environment Variables
|
|
1682
|
+
|
|
1596
1683
|
[Table of required env vars]
|
|
1597
|
-
|
|
1684
|
+
|
|
1685
|
+
````
|
|
1598
1686
|
|
|
1599
1687
|
---
|
|
1600
1688
|
|
|
@@ -1611,7 +1699,7 @@ pnpm lint # Lint all files
|
|
|
1611
1699
|
pnpm format # Format with Prettier
|
|
1612
1700
|
pnpm check-types # TypeScript type checking
|
|
1613
1701
|
pnpm audit # Security audit
|
|
1614
|
-
|
|
1702
|
+
````
|
|
1615
1703
|
|
|
1616
1704
|
### 16.2 File Naming Reference
|
|
1617
1705
|
|
|
@@ -1627,19 +1715,19 @@ constants/auth.ts # Constants
|
|
|
1627
1715
|
|
|
1628
1716
|
### 16.3 Key Config Files
|
|
1629
1717
|
|
|
1630
|
-
| File
|
|
1631
|
-
|
|
1632
|
-
| tsconfig.json
|
|
1633
|
-
| vite.config.ts
|
|
1634
|
-
| eslint.config.js
|
|
1635
|
-
| .prettierrc
|
|
1636
|
-
| .husky/pre-commit
|
|
1637
|
-
| commitlint.config.js
|
|
1638
|
-
| .github/workflows/ci.yml | CI pipeline
|
|
1639
|
-
| .github/workflows/cd.yml | CD pipeline
|
|
1640
|
-
| Dockerfile
|
|
1641
|
-
| docker-compose.yml
|
|
1642
|
-
| .env.example
|
|
1718
|
+
| File | Purpose |
|
|
1719
|
+
| ------------------------ | ----------------------------- |
|
|
1720
|
+
| tsconfig.json | TypeScript configuration |
|
|
1721
|
+
| vite.config.ts | Vite build configuration |
|
|
1722
|
+
| eslint.config.js | ESLint configuration |
|
|
1723
|
+
| .prettierrc | Prettier formatting rules |
|
|
1724
|
+
| .husky/pre-commit | Pre-commit hook (lint-staged) |
|
|
1725
|
+
| commitlint.config.js | Conventional commit rules |
|
|
1726
|
+
| .github/workflows/ci.yml | CI pipeline |
|
|
1727
|
+
| .github/workflows/cd.yml | CD pipeline |
|
|
1728
|
+
| Dockerfile | Container build |
|
|
1729
|
+
| docker-compose.yml | Local development |
|
|
1730
|
+
| .env.example | Documented env variables |
|
|
1643
1731
|
|
|
1644
1732
|
### 16.4 Package.json Scripts Template
|
|
1645
1733
|
|
|
@@ -1805,8 +1893,7 @@ import type { EmployeeBio } from '@/types';
|
|
|
1805
1893
|
const ENDPOINT = '/employees';
|
|
1806
1894
|
|
|
1807
1895
|
export const employeeBioDetailService = {
|
|
1808
|
-
getBio: (employeeId: string) =>
|
|
1809
|
-
apiClient.get<EmployeeBio>(`${ENDPOINT}/${employeeId}/bio`),
|
|
1896
|
+
getBio: (employeeId: string) => apiClient.get<EmployeeBio>(`${ENDPOINT}/${employeeId}/bio`),
|
|
1810
1897
|
updateBio: (employeeId: string, data: Partial<EmployeeBio>) =>
|
|
1811
1898
|
apiClient.put<EmployeeBio>(`${ENDPOINT}/${employeeId}/bio`, data),
|
|
1812
1899
|
};
|
|
@@ -1852,8 +1939,8 @@ export function useUsers(page = 1) {
|
|
|
1852
1939
|
return useQuery({
|
|
1853
1940
|
queryKey: userKeys.list({ page }),
|
|
1854
1941
|
queryFn: () => usersService.getAll(page),
|
|
1855
|
-
staleTime: 5 * 60 * 1000,
|
|
1856
|
-
gcTime: 30 * 60 * 1000,
|
|
1942
|
+
staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
|
|
1943
|
+
gcTime: 30 * 60 * 1000, // Keep cache for 30 minutes
|
|
1857
1944
|
refetchOnWindowFocus: true,
|
|
1858
1945
|
retry: 3,
|
|
1859
1946
|
});
|
|
@@ -1871,15 +1958,16 @@ export function useCreateUser() {
|
|
|
1871
1958
|
|
|
1872
1959
|
### 17.6 Client-Side Caching Strategy
|
|
1873
1960
|
|
|
1874
|
-
| Cache Layer
|
|
1875
|
-
|
|
1876
|
-
| Server State
|
|
1877
|
-
| Persistent Storage | localStorage
|
|
1878
|
-
| Large Data
|
|
1879
|
-
| Route Prefetch
|
|
1880
|
-
| Service Worker
|
|
1961
|
+
| Cache Layer | Technology | Use Case | Persistence |
|
|
1962
|
+
| ------------------ | ---------------------- | -------------------------------------- | ----------------- |
|
|
1963
|
+
| Server State | TanStack React Query | API caching, dedup, background refetch | In-memory |
|
|
1964
|
+
| Persistent Storage | localStorage | User preferences, theme, auth tokens | Disk |
|
|
1965
|
+
| Large Data | IndexedDB via Dexie.js | Offline data, large datasets | Disk |
|
|
1966
|
+
| Route Prefetch | Router loaders | Anticipatory data loading | In-memory |
|
|
1967
|
+
| Service Worker | Workbox | Full offline support | Cache Storage API |
|
|
1881
1968
|
|
|
1882
1969
|
**Rules**:
|
|
1970
|
+
|
|
1883
1971
|
- Never cache sensitive data (tokens, PII) in localStorage without encryption
|
|
1884
1972
|
- Always set staleTime / gcTime for API responses
|
|
1885
1973
|
- Invalidate caches on mutations (create, update, delete)
|
|
@@ -1893,6 +1981,7 @@ export function useCreateUser() {
|
|
|
1893
1981
|
### 18.1 The Golden Rule
|
|
1894
1982
|
|
|
1895
1983
|
> **Hooks are for behavior, not data.** If data comes from an API, use React Query + your service layer. Reserve hooks for:
|
|
1984
|
+
>
|
|
1896
1985
|
> - Connecting UI to data (React Query hooks)
|
|
1897
1986
|
> - Encapsulating complex browser APIs (resize, scroll, geolocation)
|
|
1898
1987
|
> - Reusable form logic
|
|
@@ -1901,12 +1990,14 @@ export function useCreateUser() {
|
|
|
1901
1990
|
### 18.2 useEffect -- When and How
|
|
1902
1991
|
|
|
1903
1992
|
**DO use useEffect for**:
|
|
1993
|
+
|
|
1904
1994
|
- Synchronizing with external systems (WebSocket, browser APIs, analytics)
|
|
1905
1995
|
- Setting up subscriptions with proper cleanup
|
|
1906
1996
|
- Managing non-React state (syncing to localStorage)
|
|
1907
1997
|
- Imperative operations (focus management, scroll restoration)
|
|
1908
1998
|
|
|
1909
1999
|
**DO NOT use useEffect for**:
|
|
2000
|
+
|
|
1910
2001
|
- Fetching data on mount --> Use React Query
|
|
1911
2002
|
- Deriving state from props --> Use useMemo
|
|
1912
2003
|
- Responding to user actions --> Use event handlers directly
|
|
@@ -1921,20 +2012,28 @@ useEffect(() => {
|
|
|
1921
2012
|
}, [url]);
|
|
1922
2013
|
|
|
1923
2014
|
// BAD: Fetching data manually (use React Query instead)
|
|
1924
|
-
useEffect(() => {
|
|
2015
|
+
useEffect(() => {
|
|
2016
|
+
fetch('/api/users')
|
|
2017
|
+
.then((r) => r.json())
|
|
2018
|
+
.then(setUsers);
|
|
2019
|
+
}, []);
|
|
1925
2020
|
|
|
1926
2021
|
// BAD: Deriving state from props
|
|
1927
|
-
useEffect(() => {
|
|
2022
|
+
useEffect(() => {
|
|
2023
|
+
setFullName(`${firstName} ${lastName}`);
|
|
2024
|
+
}, [firstName, lastName]);
|
|
1928
2025
|
// FIX: const fullName = `${firstName} ${lastName}`;
|
|
1929
2026
|
```
|
|
1930
2027
|
|
|
1931
2028
|
### 18.3 useMemo -- When to Memoize
|
|
1932
2029
|
|
|
1933
2030
|
**DO use useMemo for**:
|
|
2031
|
+
|
|
1934
2032
|
- Expensive computations (filtering/sorting large lists, complex data transformation)
|
|
1935
2033
|
- Creating stable object references for child component props (prevents re-renders)
|
|
1936
2034
|
|
|
1937
2035
|
**DO NOT use useMemo for**:
|
|
2036
|
+
|
|
1938
2037
|
- Simple calculations (firstName + lastName)
|
|
1939
2038
|
- Values used once or in a single render
|
|
1940
2039
|
- Premature optimization -- measure first, then memoize
|
|
@@ -1942,9 +2041,7 @@ useEffect(() => { setFullName(`${firstName} ${lastName}`); }, [firstName, lastNa
|
|
|
1942
2041
|
```typescript
|
|
1943
2042
|
// GOOD: Expensive computation memoized
|
|
1944
2043
|
const filteredUsers = useMemo(() => {
|
|
1945
|
-
return users
|
|
1946
|
-
.filter((u) => u.role === selectedRole)
|
|
1947
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
2044
|
+
return users.filter((u) => u.role === selectedRole).sort((a, b) => a.name.localeCompare(b.name));
|
|
1948
2045
|
}, [users, selectedRole]);
|
|
1949
2046
|
|
|
1950
2047
|
// GOOD: Stable object reference for child
|
|
@@ -1958,11 +2055,13 @@ const greeting = useMemo(() => `Hello, ${name}!`, [name]);
|
|
|
1958
2055
|
### 18.4 useCallback -- When to Memoize Functions
|
|
1959
2056
|
|
|
1960
2057
|
**DO use useCallback for**:
|
|
2058
|
+
|
|
1961
2059
|
- Callback props passed to memoized children (React.memo)
|
|
1962
2060
|
- Functions in useEffect dependency arrays
|
|
1963
2061
|
- Functions passed to context providers
|
|
1964
2062
|
|
|
1965
2063
|
**DO NOT use useCallback for**:
|
|
2064
|
+
|
|
1966
2065
|
- Event handlers on native HTML elements (<button onClick>)
|
|
1967
2066
|
- Functions used in a single parent component with no memoized children
|
|
1968
2067
|
- Premature optimization
|
|
@@ -2028,6 +2127,7 @@ function Dashboard() {
|
|
|
2028
2127
|
```
|
|
2029
2128
|
|
|
2030
2129
|
**When to use**:
|
|
2130
|
+
|
|
2031
2131
|
- Search fields with large result sets
|
|
2032
2132
|
- Tab switching with heavy content
|
|
2033
2133
|
- Chart/table rendering blocking the UI thread
|
|
@@ -2068,18 +2168,18 @@ function SearchUsers() {
|
|
|
2068
2168
|
|
|
2069
2169
|
### 18.7 Performance Decision Guide
|
|
2070
2170
|
|
|
2071
|
-
| Situation
|
|
2072
|
-
|
|
2073
|
-
| Fetching API data
|
|
2074
|
-
| Mutating server data
|
|
2075
|
-
| Expensive calculation on props change | useMemo
|
|
2076
|
-
| Stable callback for memoized child
|
|
2077
|
-
| Debouncing user input
|
|
2078
|
-
| Non-urgent state update
|
|
2079
|
-
| Deferring slow component re-render
|
|
2080
|
-
| Subscribing to browser API
|
|
2081
|
-
| Form state
|
|
2082
|
-
| Global state
|
|
2171
|
+
| Situation | Solution | Why |
|
|
2172
|
+
| ------------------------------------- | ------------------------- | ------------------------------------------------------ |
|
|
2173
|
+
| Fetching API data | React Query (useQuery) | Built-in caching, dedup, retry, stale-while-revalidate |
|
|
2174
|
+
| Mutating server data | React Query (useMutation) | Optimistic updates, cache invalidation, rollback |
|
|
2175
|
+
| Expensive calculation on props change | useMemo | Only re-computes when dependencies change |
|
|
2176
|
+
| Stable callback for memoized child | useCallback | Prevents unnecessary child re-renders |
|
|
2177
|
+
| Debouncing user input | Custom useDebounce hook | Reduces API calls during rapid typing |
|
|
2178
|
+
| Non-urgent state update | useTransition | Keeps UI responsive during heavy renders |
|
|
2179
|
+
| Deferring slow component re-render | useDeferredValue | Shows stale content while new renders |
|
|
2180
|
+
| Subscribing to browser API | useEffect with cleanup | Proper lifecycle management |
|
|
2181
|
+
| Form state | React Hook Form | Built-in validation, minimal re-renders |
|
|
2182
|
+
| Global state | Zustand | No providers, selective subscriptions |
|
|
2083
2183
|
|
|
2084
2184
|
### 18.8 Common Anti-Patterns to Avoid
|
|
2085
2185
|
|
|
@@ -2143,6 +2243,7 @@ Is the UI freezing during state updates?
|
|
|
2143
2243
|
|
|
|
2144
2244
|
You probably don't need any hook. Write plain JS/TS.
|
|
2145
2245
|
```
|
|
2246
|
+
|
|
2146
2247
|
### 18.10 Form Validation & Submission Standards
|
|
2147
2248
|
|
|
2148
2249
|
Every form must use React Hook Form with Zod validation. The schema must be co-located with the form component.
|
|
@@ -2212,6 +2313,7 @@ export function EmployeeBioForm() {
|
|
|
2212
2313
|
```
|
|
2213
2314
|
|
|
2214
2315
|
**Rules**:
|
|
2316
|
+
|
|
2215
2317
|
- Zod schema co-located with form (not in a separate `types/` file)
|
|
2216
2318
|
- Server errors mapped to field-level via `setError` (not shown as generic toast)
|
|
2217
2319
|
- Unsaved changes warning via `beforeunload` when `isDirty`
|
|
@@ -2227,18 +2329,18 @@ export function EmployeeBioForm() {
|
|
|
2227
2329
|
|
|
2228
2330
|
### 19.1 Core Principle: Automation Over Manual Review
|
|
2229
2331
|
|
|
2230
|
-
No rule is valuable if it relies on humans remembering to check it. Every standard in this document must have automated enforcement.
|
|
2332
|
+
No rule is valuable if it relies on humans remembering to check it. Every standard in this document must have automated enforcement.
|
|
2231
2333
|
|
|
2232
2334
|
**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
2335
|
|
|
2234
2336
|
**Mandatory Enforcement Layers**:
|
|
2235
2337
|
|
|
2236
|
-
| Layer
|
|
2237
|
-
|
|
2238
|
-
| L1: Editor
|
|
2239
|
-
| L2: Pre-commit Hook
|
|
2240
|
-
| L3: CI Pipeline
|
|
2241
|
-
| L4: Code Review
|
|
2338
|
+
| Layer | Tool | When | Purpose |
|
|
2339
|
+
| ------------------- | ------------------- | ------------------ | ------------------------------------------------------- |
|
|
2340
|
+
| L1: Editor | ESLint + Prettier | Every save | Immediate feedback while coding |
|
|
2341
|
+
| L2: Pre-commit Hook | Husky + lint-staged | Every `git commit` | Block bad code before it enters history |
|
|
2342
|
+
| L3: CI Pipeline | GitHub Actions | Every PR / push | Enforce before merge, across all contributors |
|
|
2343
|
+
| L4: Code Review | Human review | Every PR | Architecture, logic, edge cases automation cannot catch |
|
|
2242
2344
|
|
|
2243
2345
|
### 19.2 Required Husky + lint-staged Setup
|
|
2244
2346
|
|
|
@@ -2302,10 +2404,7 @@ The `check-modularization` script (see Step 7) validates folder structure and ba
|
|
|
2302
2404
|
// package.json
|
|
2303
2405
|
{
|
|
2304
2406
|
"lint-staged": {
|
|
2305
|
-
"src/**/*.{ts,tsx}": [
|
|
2306
|
-
"eslint --fix --max-warnings 0",
|
|
2307
|
-
"prettier --write"
|
|
2308
|
-
],
|
|
2407
|
+
"src/**/*.{ts,tsx}": ["eslint --fix --max-warnings 0", "prettier --write"],
|
|
2309
2408
|
"src/**/*.{css,scss}": ["prettier --write"],
|
|
2310
2409
|
"*.{json,md,yaml,yml}": ["prettier --write"]
|
|
2311
2410
|
}
|
|
@@ -2378,12 +2477,12 @@ rules: {
|
|
|
2378
2477
|
|
|
2379
2478
|
**The `--no-verify` policy**:
|
|
2380
2479
|
|
|
2381
|
-
| Action
|
|
2382
|
-
|
|
2383
|
-
| `git commit --no-verify`
|
|
2384
|
-
| `git push --no-verify`
|
|
2385
|
-
| Tampering with `.husky/` files
|
|
2386
|
-
| Removing hooks from `package.json` | CI `pnpm install` re-installs husky; hooks re-created
|
|
2480
|
+
| Action | Consequence |
|
|
2481
|
+
| ---------------------------------- | ----------------------------------------------------------------------- |
|
|
2482
|
+
| `git commit --no-verify` | Commit lands locally, but CI catches all skipped checks on push/PR |
|
|
2483
|
+
| `git push --no-verify` | Push succeeds, but CI pre-push checks run anyway; PR blocked if failing |
|
|
2484
|
+
| Tampering with `.husky/` files | Caught in code review; `.husky/` is committed and reviewed |
|
|
2485
|
+
| Removing hooks from `package.json` | CI `pnpm install` re-installs husky; hooks re-created |
|
|
2387
2486
|
|
|
2388
2487
|
> **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
2488
|
|
|
@@ -2430,12 +2529,22 @@ function logWarning(msg) {
|
|
|
2430
2529
|
|
|
2431
2530
|
// Helper: check if a path is a directory
|
|
2432
2531
|
function isDirectory(path) {
|
|
2433
|
-
try {
|
|
2532
|
+
try {
|
|
2533
|
+
return statSync(path).isDirectory();
|
|
2534
|
+
} catch {
|
|
2535
|
+
return false;
|
|
2536
|
+
}
|
|
2434
2537
|
}
|
|
2435
2538
|
|
|
2436
2539
|
// Helper: check if a file is a TypeScript file
|
|
2437
2540
|
function isTsFile(filename) {
|
|
2438
|
-
return
|
|
2541
|
+
return (
|
|
2542
|
+
/\.(ts|tsx)$/.test(filename) &&
|
|
2543
|
+
!filename.endsWith('.test.ts') &&
|
|
2544
|
+
!filename.endsWith('.test.tsx') &&
|
|
2545
|
+
!filename.endsWith('.spec.ts') &&
|
|
2546
|
+
!filename.endsWith('.spec.tsx')
|
|
2547
|
+
);
|
|
2439
2548
|
}
|
|
2440
2549
|
|
|
2441
2550
|
console.log('\n🔍 Checking modularization compliance...\n');
|
|
@@ -2460,9 +2569,11 @@ for (const dirName of MODULARIZED_DIRS) {
|
|
|
2460
2569
|
} else {
|
|
2461
2570
|
// Verify barrel exports at least one member
|
|
2462
2571
|
const barrelContent = readdirSync(entryPath);
|
|
2463
|
-
const tsFiles = barrelContent.filter(f => isTsFile(f) && f !== BARREL_FILE);
|
|
2572
|
+
const tsFiles = barrelContent.filter((f) => isTsFile(f) && f !== BARREL_FILE);
|
|
2464
2573
|
if (tsFiles.length === 0) {
|
|
2465
|
-
logWarning(
|
|
2574
|
+
logWarning(
|
|
2575
|
+
`${dirName}/${entry}/${BARREL_FILE} exists but no implementation files found in this domain`
|
|
2576
|
+
);
|
|
2466
2577
|
}
|
|
2467
2578
|
}
|
|
2468
2579
|
|
|
@@ -2470,8 +2581,15 @@ for (const dirName of MODULARIZED_DIRS) {
|
|
|
2470
2581
|
const subEntries = readdirSync(entryPath);
|
|
2471
2582
|
for (const subEntry of subEntries) {
|
|
2472
2583
|
const subPath = join(entryPath, subEntry);
|
|
2473
|
-
if (
|
|
2474
|
-
|
|
2584
|
+
if (
|
|
2585
|
+
subEntry !== 'test' &&
|
|
2586
|
+
subEntry !== '__tests__' &&
|
|
2587
|
+
subEntry !== '__mocks__' &&
|
|
2588
|
+
isDirectory(subPath)
|
|
2589
|
+
) {
|
|
2590
|
+
logWarning(
|
|
2591
|
+
`${dirName}/${entry}/ should not contain nested subdirectory "${subEntry}/" -- keep domains flat`
|
|
2592
|
+
);
|
|
2475
2593
|
}
|
|
2476
2594
|
}
|
|
2477
2595
|
} else if (isTsFile(entry)) {
|
|
@@ -2479,8 +2597,8 @@ for (const dirName of MODULARIZED_DIRS) {
|
|
|
2479
2597
|
if (!allowedRoot.includes(entry)) {
|
|
2480
2598
|
logError(
|
|
2481
2599
|
`Flat file "${dirName}/${entry}" is not allowed at the root level. ` +
|
|
2482
|
-
|
|
2483
|
-
|
|
2600
|
+
`Move it to a domain subdirectory (e.g., ${dirName}/${entry.replace(/\.(ts|tsx)$/, '')}/${entry}). ` +
|
|
2601
|
+
`Allowed root files: ${allowedRoot.join(', ') || 'none'}`
|
|
2484
2602
|
);
|
|
2485
2603
|
}
|
|
2486
2604
|
}
|
|
@@ -2496,7 +2614,9 @@ try {
|
|
|
2496
2614
|
logError('ESLint found violations (may include deep imports bypassing barrel files)');
|
|
2497
2615
|
}
|
|
2498
2616
|
|
|
2499
|
-
console.log(
|
|
2617
|
+
console.log(
|
|
2618
|
+
`\n${errors === 0 ? '✅' : '❌'} Modularization check complete. Errors: ${errors}, Warnings: ${warnings}\n`
|
|
2619
|
+
);
|
|
2500
2620
|
|
|
2501
2621
|
if (errors > 0) {
|
|
2502
2622
|
console.error('Fix the errors above and try again.\n');
|
|
@@ -2509,6 +2629,7 @@ if (warnings > 0) {
|
|
|
2509
2629
|
```
|
|
2510
2630
|
|
|
2511
2631
|
**What this enforces at commit time**:
|
|
2632
|
+
|
|
2512
2633
|
- ESLint auto-fixes what it can, then fails hard on remaining errors (`--max-warnings 0`)
|
|
2513
2634
|
- Prettier formats all staged files
|
|
2514
2635
|
- Commitlint rejects non-conventional commit messages
|
|
@@ -2524,17 +2645,7 @@ export default {
|
|
|
2524
2645
|
rules: {
|
|
2525
2646
|
'type-enum': [
|
|
2526
2647
|
'error',
|
|
2527
|
-
[
|
|
2528
|
-
'feat',
|
|
2529
|
-
'fix',
|
|
2530
|
-
'security',
|
|
2531
|
-
'perf',
|
|
2532
|
-
'docs',
|
|
2533
|
-
'style',
|
|
2534
|
-
'refactor',
|
|
2535
|
-
'test',
|
|
2536
|
-
'chore',
|
|
2537
|
-
],
|
|
2648
|
+
['feat', 'fix', 'security', 'perf', 'docs', 'style', 'refactor', 'test', 'chore'],
|
|
2538
2649
|
],
|
|
2539
2650
|
'subject-max-length': [2, 'always', 100],
|
|
2540
2651
|
'body-max-line-length': [2, 'always', 100],
|
|
@@ -2791,11 +2902,11 @@ console.log(chalk.green('\nAll pre-commit checks passed!'));
|
|
|
2791
2902
|
|
|
2792
2903
|
### 19.9 What Each Layer Catches
|
|
2793
2904
|
|
|
2794
|
-
| Layer
|
|
2795
|
-
|
|
2796
|
-
| L1 Editor
|
|
2797
|
-
| L2 Pre-commit
|
|
2798
|
-
| L3 CI Pipeline | Everything automated can check
|
|
2905
|
+
| Layer | Catches | Cannot Catch |
|
|
2906
|
+
| -------------- | ---------------------------------- | --------------------------------- |
|
|
2907
|
+
| L1 Editor | Typos, bad names, bad formatting | Logic bugs, architecture flaws |
|
|
2908
|
+
| L2 Pre-commit | Same as L1 + blocks commit | Business logic errors |
|
|
2909
|
+
| L3 CI Pipeline | Everything automated can check | Business logic correctness |
|
|
2799
2910
|
| L4 Code Review | Architecture, security, edge cases | Naming that already passed ESLint |
|
|
2800
2911
|
|
|
2801
2912
|
### 19.10 Enforcement Checklist for New Projects
|
|
@@ -2826,6 +2937,7 @@ When setting up a new project, verify:
|
|
|
2826
2937
|
> **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
2938
|
|
|
2828
2939
|
---
|
|
2940
|
+
|
|
2829
2941
|
## Appendix C. Frontend Cost & Pricing Reference (June 2026)
|
|
2830
2942
|
|
|
2831
2943
|
> 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.
|
|
@@ -2844,39 +2956,39 @@ Use this table to compare partner-inclusive pricing vs. standard list pricing wh
|
|
|
2844
2956
|
|
|
2845
2957
|
### C.1 Estimated Annual Costs by Project Size
|
|
2846
2958
|
|
|
2847
|
-
| Project Size
|
|
2848
|
-
|
|
2849
|
-
| Small (MVP/closed beta)
|
|
2850
|
-
| Medium (production SaaS)
|
|
2851
|
-
| Large (enterprise/multi-tenant)
|
|
2852
|
-
| Enterprise (regulated/high-scale) | 6+
|
|
2959
|
+
| Project Size | Frontend Devs | Backend Devs | DevOps | QA | Tools & Licenses | Infrastructure | Total Est. Annual |
|
|
2960
|
+
| --------------------------------- | ------------- | ------------ | ------ | --- | ---------------- | -------------- | ----------------- |
|
|
2961
|
+
| Small (MVP/closed beta) | 1 | 1 | 0.25 | 0.5 | ~USD 2,000 | ~USD 1,500 | ~USD 160,000 |
|
|
2962
|
+
| Medium (production SaaS) | 2 | 2 | 0.5 | 1 | ~USD 5,000 | ~USD 3,500 | ~USD 350,000 |
|
|
2963
|
+
| Large (enterprise/multi-tenant) | 4 | 4 | 1 | 2 | ~USD 12,000 | ~USD 8,000 | ~USD 800,000 |
|
|
2964
|
+
| Enterprise (regulated/high-scale) | 6+ | 6+ | 2+ | 3+ | ~USD 25,000+ | ~USD 20,000+ | ~USD 1,600,000+ |
|
|
2853
2965
|
|
|
2854
2966
|
### C.2 Tooling & License Costs (Annual)
|
|
2855
2967
|
|
|
2856
|
-
| Tool / Service
|
|
2857
|
-
|
|
2858
|
-
| GitHub Enterprise / Teams
|
|
2859
|
-
| Sentry (Error Tracking)
|
|
2860
|
-
| Datadog / New Relic / Azure Monitor
|
|
2861
|
-
| SonarCloud / SonarQube
|
|
2862
|
-
| Snyk / Trivy / Dependabot
|
|
2863
|
-
| Vercel / Netlify / Azure Static Web Apps
|
|
2864
|
-
| Azure Container Registry (ACR) or AWS ECR | Container image registry
|
|
2865
|
-
| Auth0 / WorkOS / Clerk
|
|
2866
|
-
| Storybook
|
|
2968
|
+
| Tool / Service | Purpose | Typical Tier | Est. Cost | Notes |
|
|
2969
|
+
| ----------------------------------------- | ------------------------------------------------- | ----------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
2970
|
+
| 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). |
|
|
2971
|
+
| Sentry (Error Tracking) | Frontend & backend error monitoring | Team: ~USD 29/app/mo | USD 350+/app/yr | Volume-based; consider self-hosted at scale. |
|
|
2972
|
+
| 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. |
|
|
2973
|
+
| 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. |
|
|
2974
|
+
| 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. |
|
|
2975
|
+
| 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. |
|
|
2976
|
+
| 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. |
|
|
2977
|
+
| 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. |
|
|
2978
|
+
| 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
2979
|
|
|
2868
2980
|
### C.3 Infrastructure Costs (Monthly)
|
|
2869
2981
|
|
|
2870
|
-
| Environment
|
|
2871
|
-
|
|
2872
|
-
| Dev (1 replica, no HA)
|
|
2873
|
-
| Staging (2 replicas)
|
|
2874
|
-
| Production (min 3 replicas, autoscale)
|
|
2875
|
-
| Managed PostgreSQL (e.g., Azure Database, RDS, Cloud SQL) | 2 vCPU / 8 GB + storage
|
|
2876
|
-
| Managed Redis / Valkey
|
|
2877
|
-
| Object Storage (S3 / Blob)
|
|
2878
|
-
| SSL (managed by ingress or ACM)
|
|
2879
|
-
| DNS / domain
|
|
2982
|
+
| Environment | Typical Specs | Est. Monthly | Annual Est. |
|
|
2983
|
+
| --------------------------------------------------------- | -------------------------------------- | ----------------- | ---------------------- |
|
|
2984
|
+
| Dev (1 replica, no HA) | 1 vCPU / 2 GB RAM | USD 40 – USD 120 | USD 500 – USD 1,500 |
|
|
2985
|
+
| Staging (2 replicas) | 2 vCPU / 4 GB RAM each | USD 120 – USD 300 | USD 1,500 – USD 3,600 |
|
|
2986
|
+
| Production (min 3 replicas, autoscale) | 2 vCPU / 4 GB RAM each + load balancer | USD 300 – USD 900 | USD 3,600 – USD 10,800 |
|
|
2987
|
+
| Managed PostgreSQL (e.g., Azure Database, RDS, Cloud SQL) | 2 vCPU / 8 GB + storage | USD 150 – USD 500 | USD 1,800 – USD 6,000 |
|
|
2988
|
+
| Managed Redis / Valkey | 1 vCPU / 2 GB | USD 15 – USD 50 | USD 180 – USD 600 |
|
|
2989
|
+
| Object Storage (S3 / Blob) | 1 TB + egress | USD 20 – USD 80 | USD 240 – USD 960 |
|
|
2990
|
+
| SSL (managed by ingress or ACM) | -- | Often free | USD 0 |
|
|
2991
|
+
| DNS / domain | -- | USD 10 – USD 30 | USD 120 – USD 360 |
|
|
2880
2992
|
|
|
2881
2993
|
> These are mid-2026 market reference prices. Cloud providers frequently discount reserved/commitment terms.
|
|
2882
2994
|
|
|
@@ -2889,28 +3001,28 @@ Use this table to compare partner-inclusive pricing vs. standard list pricing wh
|
|
|
2889
3001
|
|
|
2890
3002
|
### C.5 Open-Source vs Commercial Alternatives (2026)
|
|
2891
3003
|
|
|
2892
|
-
| Need
|
|
2893
|
-
|
|
2894
|
-
| CI/CD
|
|
2895
|
-
| Error Tracking
|
|
2896
|
-
| Monitoring/APM
|
|
2897
|
-
| Static Analysis
|
|
2898
|
-
| Dependency Scanning
|
|
2899
|
-
| Auth (if not custom) | Auth0 / Clerk
|
|
2900
|
-
| Component Review
|
|
2901
|
-
| Frontend Hosting
|
|
2902
|
-
| Container Registry
|
|
3004
|
+
| Need | Commercial Option | OSS Alternative | Est. Savings | Trade-offs / Notes |
|
|
3005
|
+
| -------------------- | ------------------------ | --------------------------------------------------------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
3006
|
+
| 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. |
|
|
3007
|
+
| 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. |
|
|
3008
|
+
| 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. |
|
|
3009
|
+
| 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. |
|
|
3010
|
+
| 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. |
|
|
3011
|
+
| 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. |
|
|
3012
|
+
| 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. |
|
|
3013
|
+
| 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. |
|
|
3014
|
+
| 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
3015
|
|
|
2904
3016
|
### C.6 OSS Cost-Scoring Cheat Sheet
|
|
2905
3017
|
|
|
2906
|
-
| Solution
|
|
2907
|
-
|
|
2908
|
-
| Prometheus + Grafana | Medium
|
|
2909
|
-
| Sentry Self-Hosted
|
|
2910
|
-
| Keycloak
|
|
2911
|
-
| Woodpecker / Gitea
|
|
2912
|
-
| Jenkins
|
|
2913
|
-
| Harbor
|
|
3018
|
+
| Solution | Setup Effort | Maintenance | When It Wins | When to Buy Commercial |
|
|
3019
|
+
| -------------------- | ------------ | ----------- | -------------------------------------------- | ---------------------------------------------------------------------- |
|
|
3020
|
+
| Prometheus + Grafana | Medium | Medium | Metrics-heavy infra, in-house SRE skill | When you want hosted dashboards, alerting, and support SLAs |
|
|
3021
|
+
| Sentry Self-Hosted | Medium | Medium | Apps with many errors and sensitive logs | When uptime and incident response are more important than license cost |
|
|
3022
|
+
| Keycloak | High | High | Enterprise SSO, internal identity, B2B | When identity is not your core competency |
|
|
3023
|
+
| Woodpecker / Gitea | Medium | Medium | Heavy CI usage, private repo cost control | When you want zero per-minute billing and full data control |
|
|
3024
|
+
| Jenkins | High | High | Legacy enterprise estates, Windows workloads | When your team already knows Jenkins and plugin ecosystem |
|
|
3025
|
+
| Harbor | Medium | Medium | Air-gapped/on-prem registries, large images | When egress or compliance prevents cloud registries |
|
|
2914
3026
|
|
|
2915
3027
|
### C.7 Backend-Oriented Cost Notes (Frontend Impact)
|
|
2916
3028
|
|
|
@@ -2938,5 +3050,5 @@ Use this table to compare partner-inclusive pricing vs. standard list pricing wh
|
|
|
2938
3050
|
- Azure Hybrid Benefit: https://azure.microsoft.com/en-us/pricing/benefits/azure-hybrid-benefit/
|
|
2939
3051
|
- Visual Studio subscriptions: https://visualstudio.microsoft.com/subscriptions/
|
|
2940
3052
|
- FastTrack: https://azure.microsoft.com/en-us/programs/azure-fasttrack/
|
|
2941
|
-
|
|
3053
|
+
Actual entitlement depends on partner tier, agreement, and region; treat these as **items to explore** with your account team.
|
|
2942
3054
|
- 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.
|