@malamute/ai-rules 1.0.0 → 1.2.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/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.routes.ts"
|
|
4
|
+
- "**/app.routes.ts"
|
|
5
|
+
- "**/app.config.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Angular Routing (Angular 21)
|
|
9
|
+
|
|
10
|
+
## Route Configuration
|
|
11
|
+
|
|
12
|
+
### Basic Routes
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// app.routes.ts
|
|
16
|
+
import { Routes } from '@angular/router';
|
|
17
|
+
|
|
18
|
+
export const routes: Routes = [
|
|
19
|
+
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
|
|
20
|
+
{ path: 'dashboard', loadComponent: () => import('./dashboard/dashboard.component') },
|
|
21
|
+
{ path: 'users', loadChildren: () => import('./users/users.routes') },
|
|
22
|
+
{ path: '**', loadComponent: () => import('./not-found/not-found.component') },
|
|
23
|
+
];
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Feature Routes with Lazy Loading
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// users/users.routes.ts
|
|
30
|
+
import { Routes } from '@angular/router';
|
|
31
|
+
import { authGuard } from '@app/core/guards/auth.guard';
|
|
32
|
+
import { userResolver } from './resolvers/user.resolver';
|
|
33
|
+
|
|
34
|
+
export default [
|
|
35
|
+
{
|
|
36
|
+
path: '',
|
|
37
|
+
loadComponent: () => import('./user-list/user-list.component'),
|
|
38
|
+
canActivate: [authGuard],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: ':id',
|
|
42
|
+
loadComponent: () => import('./user-detail/user-detail.component'),
|
|
43
|
+
resolve: { user: userResolver },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: ':id/edit',
|
|
47
|
+
loadComponent: () => import('./user-edit/user-edit.component'),
|
|
48
|
+
canActivate: [authGuard],
|
|
49
|
+
canDeactivate: [unsavedChangesGuard],
|
|
50
|
+
},
|
|
51
|
+
] satisfies Routes;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Guards (Functional)
|
|
55
|
+
|
|
56
|
+
### Auth Guard
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// guards/auth.guard.ts
|
|
60
|
+
import { inject } from '@angular/core';
|
|
61
|
+
import { CanActivateFn, Router } from '@angular/router';
|
|
62
|
+
import { AuthService } from '@app/core/services/auth.service';
|
|
63
|
+
|
|
64
|
+
export const authGuard: CanActivateFn = () => {
|
|
65
|
+
const authService = inject(AuthService);
|
|
66
|
+
const router = inject(Router);
|
|
67
|
+
|
|
68
|
+
if (authService.isAuthenticated()) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return router.createUrlTree(['/login'], {
|
|
73
|
+
queryParams: { returnUrl: router.url },
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Role Guard
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// guards/role.guard.ts
|
|
82
|
+
import { inject } from '@angular/core';
|
|
83
|
+
import { CanActivateFn, Router } from '@angular/router';
|
|
84
|
+
import { AuthService } from '@app/core/services/auth.service';
|
|
85
|
+
|
|
86
|
+
export const roleGuard = (allowedRoles: string[]): CanActivateFn => {
|
|
87
|
+
return () => {
|
|
88
|
+
const authService = inject(AuthService);
|
|
89
|
+
const router = inject(Router);
|
|
90
|
+
const userRole = authService.currentUser()?.role;
|
|
91
|
+
|
|
92
|
+
if (userRole && allowedRoles.includes(userRole)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return router.createUrlTree(['/forbidden']);
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Usage in routes
|
|
101
|
+
{
|
|
102
|
+
path: 'admin',
|
|
103
|
+
loadComponent: () => import('./admin/admin.component'),
|
|
104
|
+
canActivate: [authGuard, roleGuard(['admin', 'superadmin'])],
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Unsaved Changes Guard
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// guards/unsaved-changes.guard.ts
|
|
112
|
+
import { CanDeactivateFn } from '@angular/router';
|
|
113
|
+
|
|
114
|
+
export interface HasUnsavedChanges {
|
|
115
|
+
hasUnsavedChanges(): boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
|
|
119
|
+
if (component.hasUnsavedChanges()) {
|
|
120
|
+
return confirm('You have unsaved changes. Do you really want to leave?');
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Resolvers
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// resolvers/user.resolver.ts
|
|
130
|
+
import { inject } from '@angular/core';
|
|
131
|
+
import { ResolveFn, Router } from '@angular/router';
|
|
132
|
+
import { catchError, EMPTY } from 'rxjs';
|
|
133
|
+
import { UserService } from '../services/user.service';
|
|
134
|
+
import { User } from '../models/user.model';
|
|
135
|
+
|
|
136
|
+
export const userResolver: ResolveFn<User> = (route) => {
|
|
137
|
+
const userService = inject(UserService);
|
|
138
|
+
const router = inject(Router);
|
|
139
|
+
const userId = route.paramMap.get('id')!;
|
|
140
|
+
|
|
141
|
+
return userService.getById(userId).pipe(
|
|
142
|
+
catchError(() => {
|
|
143
|
+
router.navigate(['/users']);
|
|
144
|
+
return EMPTY;
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Usage in component
|
|
150
|
+
@Component({ ... })
|
|
151
|
+
export class UserDetailComponent {
|
|
152
|
+
private readonly route = inject(ActivatedRoute);
|
|
153
|
+
protected readonly user = toSignal(
|
|
154
|
+
this.route.data.pipe(map(data => data['user'] as User))
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Route Parameters
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
@Component({ ... })
|
|
163
|
+
export class UserDetailComponent {
|
|
164
|
+
private readonly route = inject(ActivatedRoute);
|
|
165
|
+
|
|
166
|
+
// Signal-based params
|
|
167
|
+
protected readonly userId = toSignal(
|
|
168
|
+
this.route.paramMap.pipe(map(params => params.get('id')))
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Query params
|
|
172
|
+
protected readonly tab = toSignal(
|
|
173
|
+
this.route.queryParamMap.pipe(map(params => params.get('tab') ?? 'profile'))
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Programmatic Navigation
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
@Component({ ... })
|
|
182
|
+
export class UserListComponent {
|
|
183
|
+
private readonly router = inject(Router);
|
|
184
|
+
|
|
185
|
+
public navigateToUser(userId: string): void {
|
|
186
|
+
this.router.navigate(['/users', userId]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public navigateWithQuery(): void {
|
|
190
|
+
this.router.navigate(['/users'], {
|
|
191
|
+
queryParams: { status: 'active', page: 1 },
|
|
192
|
+
queryParamsHandling: 'merge', // or 'preserve'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public navigateRelative(): void {
|
|
197
|
+
// From /users to /users/123
|
|
198
|
+
this.router.navigate(['123'], { relativeTo: this.route });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Route Layout with Named Outlets
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// routes
|
|
207
|
+
{
|
|
208
|
+
path: 'dashboard',
|
|
209
|
+
component: DashboardLayoutComponent,
|
|
210
|
+
children: [
|
|
211
|
+
{ path: '', loadComponent: () => import('./main/main.component') },
|
|
212
|
+
{ path: '', outlet: 'sidebar', loadComponent: () => import('./sidebar/sidebar.component') },
|
|
213
|
+
],
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// template
|
|
217
|
+
<router-outlet />
|
|
218
|
+
<router-outlet name="sidebar" />
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Title & Meta
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// routes
|
|
225
|
+
{
|
|
226
|
+
path: 'users',
|
|
227
|
+
loadComponent: () => import('./users/users.component'),
|
|
228
|
+
title: 'User Management',
|
|
229
|
+
data: {
|
|
230
|
+
meta: {
|
|
231
|
+
description: 'Manage user accounts',
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Title strategy (app.config.ts)
|
|
237
|
+
export const appConfig: ApplicationConfig = {
|
|
238
|
+
providers: [
|
|
239
|
+
provideRouter(routes, withComponentInputBinding()),
|
|
240
|
+
{
|
|
241
|
+
provide: TitleStrategy,
|
|
242
|
+
useClass: CustomTitleStrategy,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Preloading Strategies
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// app.config.ts
|
|
252
|
+
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
|
|
253
|
+
|
|
254
|
+
export const appConfig: ApplicationConfig = {
|
|
255
|
+
providers: [
|
|
256
|
+
provideRouter(
|
|
257
|
+
routes,
|
|
258
|
+
withPreloading(PreloadAllModules), // or custom strategy
|
|
259
|
+
withComponentInputBinding(),
|
|
260
|
+
withRouterConfig({ onSameUrlNavigation: 'reload' }),
|
|
261
|
+
),
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Anti-patterns
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// BAD: Class-based guards (deprecated)
|
|
270
|
+
@Injectable()
|
|
271
|
+
export class AuthGuard implements CanActivate { ... }
|
|
272
|
+
|
|
273
|
+
// GOOD: Functional guards
|
|
274
|
+
export const authGuard: CanActivateFn = () => { ... };
|
|
275
|
+
|
|
276
|
+
// BAD: Subscribing in component for route params
|
|
277
|
+
ngOnInit() {
|
|
278
|
+
this.route.params.subscribe(params => this.id = params['id']);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// GOOD: Signal-based
|
|
282
|
+
protected readonly id = toSignal(
|
|
283
|
+
this.route.paramMap.pipe(map(p => p.get('id')))
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// BAD: Hardcoded paths
|
|
287
|
+
this.router.navigateByUrl('/users/123/edit');
|
|
288
|
+
|
|
289
|
+
// GOOD: Relative navigation or route constants
|
|
290
|
+
this.router.navigate(['edit'], { relativeTo: this.route });
|
|
291
|
+
```
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/app.config.ts"
|
|
4
|
+
- "**/app.config.server.ts"
|
|
5
|
+
- "**/app.routes.server.ts"
|
|
6
|
+
- "**/server.ts"
|
|
7
|
+
- "**/main.server.ts"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Angular SSR & Hydration
|
|
11
|
+
|
|
12
|
+
## Server Configuration
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// app.config.ts
|
|
16
|
+
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
|
17
|
+
import { provideRouter } from '@angular/router';
|
|
18
|
+
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
|
19
|
+
|
|
20
|
+
import { routes } from './app.routes';
|
|
21
|
+
|
|
22
|
+
export const appConfig: ApplicationConfig = {
|
|
23
|
+
providers: [
|
|
24
|
+
provideZoneChangeDetection({ eventCoalescing: true }),
|
|
25
|
+
provideRouter(routes),
|
|
26
|
+
provideClientHydration(withEventReplay()),
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// app.config.server.ts
|
|
33
|
+
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
|
|
34
|
+
import { provideServerRendering } from '@angular/platform-server';
|
|
35
|
+
import { provideServerRoutesConfig } from '@angular/ssr';
|
|
36
|
+
|
|
37
|
+
import { appConfig } from './app.config';
|
|
38
|
+
import { serverRoutes } from './app.routes.server';
|
|
39
|
+
|
|
40
|
+
const serverConfig: ApplicationConfig = {
|
|
41
|
+
providers: [
|
|
42
|
+
provideServerRendering(),
|
|
43
|
+
provideServerRoutesConfig(serverRoutes),
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Server Routes Configuration
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// app.routes.server.ts
|
|
54
|
+
import { RenderMode, ServerRoute } from '@angular/ssr';
|
|
55
|
+
|
|
56
|
+
export const serverRoutes: ServerRoute[] = [
|
|
57
|
+
{
|
|
58
|
+
path: '',
|
|
59
|
+
renderMode: RenderMode.Prerender, // Static at build time
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
path: 'products',
|
|
63
|
+
renderMode: RenderMode.Prerender,
|
|
64
|
+
async getPrerenderParams() {
|
|
65
|
+
// Return params for prerendering
|
|
66
|
+
return [{ id: '1' }, { id: '2' }, { id: '3' }];
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
path: 'dashboard/**',
|
|
71
|
+
renderMode: RenderMode.Client, // Client-only, no SSR
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
path: '**',
|
|
75
|
+
renderMode: RenderMode.Server, // SSR at request time
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Render Modes
|
|
81
|
+
|
|
82
|
+
| Mode | When to Use | SEO | Performance |
|
|
83
|
+
|------|-------------|-----|-------------|
|
|
84
|
+
| `Prerender` | Static content, marketing pages | Best | Best (cached) |
|
|
85
|
+
| `Server` | Dynamic content, user-specific | Good | Good |
|
|
86
|
+
| `Client` | Dashboards, authenticated areas | None | Fastest initial |
|
|
87
|
+
|
|
88
|
+
## Platform Detection
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
|
|
92
|
+
import { PLATFORM_ID, inject } from '@angular/core';
|
|
93
|
+
|
|
94
|
+
@Component({ ... })
|
|
95
|
+
export class MyComponent {
|
|
96
|
+
private readonly platformId = inject(PLATFORM_ID);
|
|
97
|
+
|
|
98
|
+
protected readonly isBrowser = isPlatformBrowser(this.platformId);
|
|
99
|
+
protected readonly isServer = isPlatformServer(this.platformId);
|
|
100
|
+
|
|
101
|
+
constructor() {
|
|
102
|
+
if (this.isBrowser) {
|
|
103
|
+
// Browser-only code (localStorage, window, etc.)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## afterNextRender / afterRender
|
|
110
|
+
|
|
111
|
+
Use these for DOM manipulation that should only happen in browser:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { afterNextRender, afterRender, Component, ElementRef, viewChild } from '@angular/core';
|
|
115
|
+
|
|
116
|
+
@Component({ ... })
|
|
117
|
+
export class ChartComponent {
|
|
118
|
+
private readonly canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('chart');
|
|
119
|
+
|
|
120
|
+
constructor() {
|
|
121
|
+
// Runs once after first render (browser only)
|
|
122
|
+
afterNextRender(() => {
|
|
123
|
+
this.initChart(this.canvas().nativeElement);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Runs after every render (browser only)
|
|
127
|
+
afterRender(() => {
|
|
128
|
+
this.updateChart();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private initChart(canvas: HTMLCanvasElement): void {
|
|
133
|
+
// Safe to use browser APIs here
|
|
134
|
+
const ctx = canvas.getContext('2d');
|
|
135
|
+
// Initialize chart library...
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Transfer State
|
|
141
|
+
|
|
142
|
+
Share data between server and client to avoid duplicate requests:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { makeStateKey, TransferState } from '@angular/core';
|
|
146
|
+
|
|
147
|
+
const USERS_KEY = makeStateKey<User[]>('users');
|
|
148
|
+
|
|
149
|
+
@Component({ ... })
|
|
150
|
+
export class UserListComponent {
|
|
151
|
+
private readonly transferState = inject(TransferState);
|
|
152
|
+
private readonly userService = inject(UserService);
|
|
153
|
+
private readonly platformId = inject(PLATFORM_ID);
|
|
154
|
+
|
|
155
|
+
protected readonly users = signal<User[]>([]);
|
|
156
|
+
|
|
157
|
+
constructor() {
|
|
158
|
+
afterNextRender(() => {
|
|
159
|
+
this.loadUsers();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// On server, load and store
|
|
163
|
+
if (isPlatformServer(this.platformId)) {
|
|
164
|
+
this.loadUsersServer();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// On client, retrieve from transfer state first
|
|
168
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
169
|
+
const cached = this.transferState.get(USERS_KEY, null);
|
|
170
|
+
if (cached) {
|
|
171
|
+
this.users.set(cached);
|
|
172
|
+
this.transferState.remove(USERS_KEY);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async loadUsersServer(): Promise<void> {
|
|
178
|
+
const users = await firstValueFrom(this.userService.getAll());
|
|
179
|
+
this.users.set(users);
|
|
180
|
+
this.transferState.set(USERS_KEY, users);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Hydration Best Practices
|
|
186
|
+
|
|
187
|
+
### Avoid Hydration Mismatch
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// BAD: Different content on server vs client
|
|
191
|
+
@Component({
|
|
192
|
+
template: `<p>Current time: {{ now }}</p>`,
|
|
193
|
+
})
|
|
194
|
+
export class TimeComponent {
|
|
195
|
+
now = new Date().toISOString(); // Different on server vs client!
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// GOOD: Use consistent initial state
|
|
199
|
+
@Component({
|
|
200
|
+
template: `<p>Current time: {{ now() }}</p>`,
|
|
201
|
+
})
|
|
202
|
+
export class TimeComponent {
|
|
203
|
+
protected readonly now = signal<string>('');
|
|
204
|
+
|
|
205
|
+
constructor() {
|
|
206
|
+
afterNextRender(() => {
|
|
207
|
+
this.now.set(new Date().toISOString());
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Skip Hydration for Dynamic Content
|
|
214
|
+
|
|
215
|
+
```html
|
|
216
|
+
<!-- Skip hydration for parts that will differ -->
|
|
217
|
+
<div ngSkipHydration>
|
|
218
|
+
<app-live-clock />
|
|
219
|
+
<app-user-presence />
|
|
220
|
+
</div>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// Or in component
|
|
225
|
+
@Component({
|
|
226
|
+
host: { ngSkipHydration: 'true' },
|
|
227
|
+
})
|
|
228
|
+
export class LiveClockComponent { }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## HTTP Caching with Transfer State
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// interceptors/transfer-state.interceptor.ts
|
|
235
|
+
import { HttpInterceptorFn } from '@angular/common/http';
|
|
236
|
+
import { inject } from '@angular/core';
|
|
237
|
+
import { makeStateKey, TransferState } from '@angular/core';
|
|
238
|
+
import { isPlatformServer } from '@angular/common';
|
|
239
|
+
import { PLATFORM_ID } from '@angular/core';
|
|
240
|
+
import { of, tap } from 'rxjs';
|
|
241
|
+
|
|
242
|
+
export const transferStateInterceptor: HttpInterceptorFn = (req, next) => {
|
|
243
|
+
if (req.method !== 'GET') {
|
|
244
|
+
return next(req);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const transferState = inject(TransferState);
|
|
248
|
+
const platformId = inject(PLATFORM_ID);
|
|
249
|
+
const key = makeStateKey<unknown>(req.urlWithParams);
|
|
250
|
+
|
|
251
|
+
if (isPlatformServer(platformId)) {
|
|
252
|
+
return next(req).pipe(
|
|
253
|
+
tap((response) => {
|
|
254
|
+
transferState.set(key, response);
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const cached = transferState.get(key, null);
|
|
260
|
+
if (cached) {
|
|
261
|
+
transferState.remove(key);
|
|
262
|
+
return of(cached);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return next(req);
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Anti-patterns
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// BAD: Direct DOM access without platform check
|
|
273
|
+
@Component({ ... })
|
|
274
|
+
export class MyComponent {
|
|
275
|
+
constructor() {
|
|
276
|
+
window.addEventListener('scroll', this.onScroll); // Crashes on server!
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// GOOD: Use afterNextRender
|
|
281
|
+
@Component({ ... })
|
|
282
|
+
export class MyComponent {
|
|
283
|
+
constructor() {
|
|
284
|
+
afterNextRender(() => {
|
|
285
|
+
window.addEventListener('scroll', this.onScroll);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
// BAD: localStorage without platform check
|
|
292
|
+
const token = localStorage.getItem('token'); // Crashes on server!
|
|
293
|
+
|
|
294
|
+
// GOOD: Check platform or use afterNextRender
|
|
295
|
+
constructor() {
|
|
296
|
+
afterNextRender(() => {
|
|
297
|
+
const token = localStorage.getItem('token');
|
|
298
|
+
this.token.set(token);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
// BAD: Using setTimeout for "after render"
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
this.initChart();
|
|
306
|
+
}, 0);
|
|
307
|
+
|
|
308
|
+
// GOOD: Use afterNextRender
|
|
309
|
+
afterNextRender(() => {
|
|
310
|
+
this.initChart();
|
|
311
|
+
});
|
|
312
|
+
```
|