create-ng-tailwind 1.0.0 → 2.0.1
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/CHANGELOG.md +209 -8
- package/CLAUDE.md +178 -0
- package/LICENSE +1 -1
- package/README.md +151 -246
- package/bin/create-ng-tailwind.js +5 -407
- package/lib/cli/index.js +222 -0
- package/lib/cli/interactive.js +26 -0
- package/lib/cli/validators.js +23 -0
- package/lib/config/defaults.js +7 -0
- package/lib/managers/ProjectManager.js +97 -0
- package/lib/managers/TailwindManager.js +100 -0
- package/lib/managers/TemplateManager.js +39 -0
- package/lib/templates/index.js +7 -0
- package/lib/templates/minimal/index.js +24 -0
- package/lib/templates/starter/advanced-features.js +528 -0
- package/lib/templates/starter/features.js +700 -0
- package/lib/templates/starter/index.js +4117 -0
- package/lib/templates/starter/ui-features.js +437 -0
- package/lib/utils/ai-config.js +606 -0
- package/lib/utils/constants.js +14 -0
- package/lib/utils/helpers.js +60 -0
- package/lib/utils/logger.js +109 -0
- package/package.json +6 -15
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
// ==================== HTTP INTERCEPTORS ====================
|
|
5
|
+
|
|
6
|
+
async function createAuthInterceptor(config) {
|
|
7
|
+
const authInterceptor = `import { HttpInterceptorFn } from '@angular/common/http';
|
|
8
|
+
import { inject } from '@angular/core';
|
|
9
|
+
import { AuthService } from '@core/services/auth.service';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Authentication Interceptor
|
|
13
|
+
* Adds JWT token to outgoing requests
|
|
14
|
+
*/
|
|
15
|
+
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|
16
|
+
const authService = inject(AuthService);
|
|
17
|
+
const token = authService.getToken();
|
|
18
|
+
|
|
19
|
+
// Skip adding token for login/register endpoints
|
|
20
|
+
const skipAuthUrls = ['/auth/login', '/auth/register', '/auth/refresh'];
|
|
21
|
+
const shouldSkip = skipAuthUrls.some(url => req.url.includes(url));
|
|
22
|
+
|
|
23
|
+
if (token && !shouldSkip) {
|
|
24
|
+
// Clone the request and add the authorization header
|
|
25
|
+
req = req.clone({
|
|
26
|
+
setHeaders: {
|
|
27
|
+
Authorization: \`Bearer \${token}\`
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return next(req);
|
|
33
|
+
};`;
|
|
34
|
+
|
|
35
|
+
await fs.writeFile(
|
|
36
|
+
path.join(config.fullPath, "src/app/core/interceptors/auth.interceptor.ts"),
|
|
37
|
+
authInterceptor,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function createErrorInterceptor(config) {
|
|
42
|
+
const errorInterceptor = `import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
|
|
43
|
+
import { inject } from '@angular/core';
|
|
44
|
+
import { Router } from '@angular/router';
|
|
45
|
+
import { catchError, throwError } from 'rxjs';
|
|
46
|
+
import { ToastService } from '@core/services/toast.service';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Error Handling Interceptor
|
|
50
|
+
* Handles HTTP errors globally and shows user-friendly messages
|
|
51
|
+
*/
|
|
52
|
+
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
|
|
53
|
+
const router = inject(Router);
|
|
54
|
+
const toastService = inject(ToastService);
|
|
55
|
+
|
|
56
|
+
return next(req).pipe(
|
|
57
|
+
catchError((error: HttpErrorResponse) => {
|
|
58
|
+
let errorMessage = 'An error occurred';
|
|
59
|
+
|
|
60
|
+
if (error.error instanceof ErrorEvent) {
|
|
61
|
+
// Client-side error
|
|
62
|
+
errorMessage = \`Client Error: \${error.error.message}\`;
|
|
63
|
+
} else {
|
|
64
|
+
// Server-side error
|
|
65
|
+
switch (error.status) {
|
|
66
|
+
case 0:
|
|
67
|
+
errorMessage = 'No internet connection';
|
|
68
|
+
break;
|
|
69
|
+
case 400:
|
|
70
|
+
errorMessage = error.error?.message || 'Bad request';
|
|
71
|
+
break;
|
|
72
|
+
case 401:
|
|
73
|
+
errorMessage = 'Unauthorized. Please login again.';
|
|
74
|
+
// Redirect to login
|
|
75
|
+
router.navigate(['/auth/login']);
|
|
76
|
+
break;
|
|
77
|
+
case 403:
|
|
78
|
+
errorMessage = 'Access forbidden';
|
|
79
|
+
break;
|
|
80
|
+
case 404:
|
|
81
|
+
errorMessage = 'Resource not found';
|
|
82
|
+
break;
|
|
83
|
+
case 500:
|
|
84
|
+
errorMessage = 'Internal server error';
|
|
85
|
+
break;
|
|
86
|
+
case 503:
|
|
87
|
+
errorMessage = 'Service unavailable';
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
errorMessage = error.error?.message || \`Error: \${error.status}\`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Show error toast
|
|
95
|
+
toastService.error(errorMessage);
|
|
96
|
+
|
|
97
|
+
console.error('HTTP Error:', error);
|
|
98
|
+
return throwError(() => new Error(errorMessage));
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
};`;
|
|
102
|
+
|
|
103
|
+
await fs.writeFile(
|
|
104
|
+
path.join(
|
|
105
|
+
config.fullPath,
|
|
106
|
+
"src/app/core/interceptors/error.interceptor.ts",
|
|
107
|
+
),
|
|
108
|
+
errorInterceptor,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function createLoadingInterceptor(config) {
|
|
113
|
+
const loadingInterceptor = `import { HttpInterceptorFn } from '@angular/common/http';
|
|
114
|
+
import { inject } from '@angular/core';
|
|
115
|
+
import { finalize } from 'rxjs';
|
|
116
|
+
import { LoadingService } from '@core/services/loading.service';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Loading Interceptor
|
|
120
|
+
* Shows/hides loading indicator for HTTP requests
|
|
121
|
+
*/
|
|
122
|
+
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
|
|
123
|
+
const loadingService = inject(LoadingService);
|
|
124
|
+
|
|
125
|
+
// Skip loading indicator for certain endpoints (optional)
|
|
126
|
+
const skipLoadingUrls = ['/api/polling', '/api/heartbeat'];
|
|
127
|
+
const shouldSkip = skipLoadingUrls.some(url => req.url.includes(url));
|
|
128
|
+
|
|
129
|
+
if (!shouldSkip) {
|
|
130
|
+
loadingService.show();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return next(req).pipe(
|
|
134
|
+
finalize(() => {
|
|
135
|
+
if (!shouldSkip) {
|
|
136
|
+
loadingService.hide();
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
};`;
|
|
141
|
+
|
|
142
|
+
await fs.writeFile(
|
|
143
|
+
path.join(
|
|
144
|
+
config.fullPath,
|
|
145
|
+
"src/app/core/interceptors/loading.interceptor.ts",
|
|
146
|
+
),
|
|
147
|
+
loadingInterceptor,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function createCachingInterceptor(config) {
|
|
152
|
+
const cachingInterceptor = `import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
|
|
153
|
+
import { inject } from '@angular/core';
|
|
154
|
+
import { of, tap } from 'rxjs';
|
|
155
|
+
import { CacheService } from '@core/services/cache.service';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Caching Interceptor
|
|
159
|
+
* Caches GET requests for better performance
|
|
160
|
+
*/
|
|
161
|
+
export const cachingInterceptor: HttpInterceptorFn = (req, next) => {
|
|
162
|
+
const cacheService = inject(CacheService);
|
|
163
|
+
|
|
164
|
+
// Only cache GET requests
|
|
165
|
+
if (req.method !== 'GET') {
|
|
166
|
+
return next(req);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check if request has cache bypass header
|
|
170
|
+
if (req.headers.has('X-Bypass-Cache')) {
|
|
171
|
+
return next(req);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check cache
|
|
175
|
+
const cachedResponse = cacheService.get(req.url);
|
|
176
|
+
if (cachedResponse) {
|
|
177
|
+
return of(cachedResponse);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// If not cached, make request and cache the response
|
|
181
|
+
return next(req).pipe(
|
|
182
|
+
tap(event => {
|
|
183
|
+
if (event instanceof HttpResponse) {
|
|
184
|
+
cacheService.set(req.url, event);
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
};`;
|
|
189
|
+
|
|
190
|
+
await fs.writeFile(
|
|
191
|
+
path.join(
|
|
192
|
+
config.fullPath,
|
|
193
|
+
"src/app/core/interceptors/caching.interceptor.ts",
|
|
194
|
+
),
|
|
195
|
+
cachingInterceptor,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ==================== TOAST SERVICE & COMPONENT ====================
|
|
200
|
+
|
|
201
|
+
async function createToastService(config) {
|
|
202
|
+
const toastService = `import { Injectable, signal } from '@angular/core';
|
|
203
|
+
|
|
204
|
+
export interface Toast {
|
|
205
|
+
id: string;
|
|
206
|
+
type: 'success' | 'error' | 'warning' | 'info';
|
|
207
|
+
message: string;
|
|
208
|
+
duration?: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@Injectable({
|
|
212
|
+
providedIn: 'root'
|
|
213
|
+
})
|
|
214
|
+
export class ToastService {
|
|
215
|
+
// Reactive signal for toast state
|
|
216
|
+
toasts = signal<Toast[]>([]);
|
|
217
|
+
|
|
218
|
+
private toastId = 0;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Show a success toast
|
|
222
|
+
*/
|
|
223
|
+
success(message: string, duration = 5000): void {
|
|
224
|
+
this.show('success', message, duration);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Show an error toast
|
|
229
|
+
*/
|
|
230
|
+
error(message: string, duration = 7000): void {
|
|
231
|
+
this.show('error', message, duration);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Show a warning toast
|
|
236
|
+
*/
|
|
237
|
+
warning(message: string, duration = 6000): void {
|
|
238
|
+
this.show('warning', message, duration);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Show an info toast
|
|
243
|
+
*/
|
|
244
|
+
info(message: string, duration = 5000): void {
|
|
245
|
+
this.show('info', message, duration);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Show a toast with custom type
|
|
250
|
+
*/
|
|
251
|
+
private show(type: Toast['type'], message: string, duration: number): void {
|
|
252
|
+
const id = \`toast-\${++this.toastId}\`;
|
|
253
|
+
const toast: Toast = { id, type, message, duration };
|
|
254
|
+
|
|
255
|
+
// Add toast to the list
|
|
256
|
+
this.toasts.update(toasts => [...toasts, toast]);
|
|
257
|
+
|
|
258
|
+
// Auto-remove after duration
|
|
259
|
+
if (duration > 0) {
|
|
260
|
+
setTimeout(() => this.remove(id), duration);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Remove a toast by ID
|
|
266
|
+
*/
|
|
267
|
+
remove(id: string): void {
|
|
268
|
+
this.toasts.update(toasts => toasts.filter(t => t.id !== id));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Clear all toasts
|
|
273
|
+
*/
|
|
274
|
+
clear(): void {
|
|
275
|
+
this.toasts.set([]);
|
|
276
|
+
}
|
|
277
|
+
}`;
|
|
278
|
+
|
|
279
|
+
await fs.writeFile(
|
|
280
|
+
path.join(config.fullPath, "src/app/core/services/toast.service.ts"),
|
|
281
|
+
toastService,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function createToastComponent(config) {
|
|
286
|
+
const toastComponent = `import { Component, inject } from '@angular/core';
|
|
287
|
+
import { CommonModule } from '@angular/common';
|
|
288
|
+
import { ToastService, Toast } from '@core/services/toast.service';
|
|
289
|
+
|
|
290
|
+
@Component({
|
|
291
|
+
selector: 'app-toast',
|
|
292
|
+
imports: [CommonModule],
|
|
293
|
+
template: \`
|
|
294
|
+
<!-- Toast Container -->
|
|
295
|
+
<div class="fixed top-4 right-4 z-50 space-y-2 max-w-sm w-full">
|
|
296
|
+
@for (toast of toastService.toasts(); track toast.id) {
|
|
297
|
+
<div
|
|
298
|
+
[class]="getToastClasses(toast)"
|
|
299
|
+
class="p-4 rounded-lg shadow-lg flex items-start space-x-3 animate-slide-in-right">
|
|
300
|
+
|
|
301
|
+
<!-- Icon -->
|
|
302
|
+
<div class="shrink-0">
|
|
303
|
+
@switch (toast.type) {
|
|
304
|
+
@case ('success') {
|
|
305
|
+
<svg class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
306
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
307
|
+
</svg>
|
|
308
|
+
}
|
|
309
|
+
@case ('error') {
|
|
310
|
+
<svg class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
311
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
312
|
+
</svg>
|
|
313
|
+
}
|
|
314
|
+
@case ('warning') {
|
|
315
|
+
<svg class="h-6 w-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
316
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
|
317
|
+
</svg>
|
|
318
|
+
}
|
|
319
|
+
@case ('info') {
|
|
320
|
+
<svg class="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
321
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
322
|
+
</svg>
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<!-- Message -->
|
|
328
|
+
<div class="flex-1 pt-0.5">
|
|
329
|
+
<p class="text-sm font-medium text-gray-900">{{ toast.message }}</p>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<!-- Close Button -->
|
|
333
|
+
<button
|
|
334
|
+
(click)="toastService.remove(toast.id)"
|
|
335
|
+
class="shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
|
336
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
337
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
338
|
+
</svg>
|
|
339
|
+
</button>
|
|
340
|
+
</div>
|
|
341
|
+
}
|
|
342
|
+
</div>
|
|
343
|
+
\`,
|
|
344
|
+
styles: [\`
|
|
345
|
+
@keyframes slide-in-right {
|
|
346
|
+
from {
|
|
347
|
+
opacity: 0;
|
|
348
|
+
transform: translateX(100%);
|
|
349
|
+
}
|
|
350
|
+
to {
|
|
351
|
+
opacity: 1;
|
|
352
|
+
transform: translateX(0);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.animate-slide-in-right {
|
|
357
|
+
animation: slide-in-right 0.3s ease-out;
|
|
358
|
+
}
|
|
359
|
+
\`]
|
|
360
|
+
})
|
|
361
|
+
export class ToastComponent {
|
|
362
|
+
toastService = inject(ToastService);
|
|
363
|
+
|
|
364
|
+
getToastClasses(toast: Toast): string {
|
|
365
|
+
const baseClasses = 'border-l-4';
|
|
366
|
+
const typeClasses = {
|
|
367
|
+
success: 'bg-green-50 border-green-500',
|
|
368
|
+
error: 'bg-red-50 border-red-500',
|
|
369
|
+
warning: 'bg-yellow-50 border-yellow-500',
|
|
370
|
+
info: 'bg-blue-50 border-blue-500'
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
return \`\${baseClasses} \${typeClasses[toast.type]}\`;
|
|
374
|
+
}
|
|
375
|
+
}`;
|
|
376
|
+
|
|
377
|
+
await fs.writeFile(
|
|
378
|
+
path.join(
|
|
379
|
+
config.fullPath,
|
|
380
|
+
"src/app/shared/components/toast/toast.component.ts",
|
|
381
|
+
),
|
|
382
|
+
toastComponent,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ==================== LOADING SERVICE ====================
|
|
387
|
+
|
|
388
|
+
async function createLoadingService(config) {
|
|
389
|
+
const loadingService = `import { Injectable, signal } from '@angular/core';
|
|
390
|
+
|
|
391
|
+
@Injectable({
|
|
392
|
+
providedIn: 'root'
|
|
393
|
+
})
|
|
394
|
+
export class LoadingService {
|
|
395
|
+
// Reactive signal for loading state
|
|
396
|
+
private loadingCount = 0;
|
|
397
|
+
isLoading = signal(false);
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Show loading indicator
|
|
401
|
+
*/
|
|
402
|
+
show(): void {
|
|
403
|
+
this.loadingCount++;
|
|
404
|
+
this.isLoading.set(true);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Hide loading indicator
|
|
409
|
+
*/
|
|
410
|
+
hide(): void {
|
|
411
|
+
this.loadingCount = Math.max(0, this.loadingCount - 1);
|
|
412
|
+
if (this.loadingCount === 0) {
|
|
413
|
+
this.isLoading.set(false);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Force hide loading (reset counter)
|
|
419
|
+
*/
|
|
420
|
+
forceHide(): void {
|
|
421
|
+
this.loadingCount = 0;
|
|
422
|
+
this.isLoading.set(false);
|
|
423
|
+
}
|
|
424
|
+
}`;
|
|
425
|
+
|
|
426
|
+
await fs.writeFile(
|
|
427
|
+
path.join(config.fullPath, "src/app/core/services/loading.service.ts"),
|
|
428
|
+
loadingService,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ==================== CACHE SERVICE ====================
|
|
433
|
+
|
|
434
|
+
async function createCacheService(config) {
|
|
435
|
+
const cacheService = `import { Injectable } from '@angular/core';
|
|
436
|
+
import { HttpResponse } from '@angular/common/http';
|
|
437
|
+
|
|
438
|
+
interface CacheEntry {
|
|
439
|
+
response: HttpResponse<unknown>;
|
|
440
|
+
timestamp: number;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
@Injectable({
|
|
444
|
+
providedIn: 'root'
|
|
445
|
+
})
|
|
446
|
+
export class CacheService {
|
|
447
|
+
private cache = new Map<string, CacheEntry>();
|
|
448
|
+
private readonly maxAge = 5 * 60 * 1000; // 5 minutes
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get cached response
|
|
452
|
+
*/
|
|
453
|
+
get(url: string): HttpResponse<unknown> | null {
|
|
454
|
+
const entry = this.cache.get(url);
|
|
455
|
+
|
|
456
|
+
if (!entry) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Check if cache is expired
|
|
461
|
+
const isExpired = Date.now() - entry.timestamp > this.maxAge;
|
|
462
|
+
if (isExpired) {
|
|
463
|
+
this.cache.delete(url);
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return entry.response;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Set cache entry
|
|
472
|
+
*/
|
|
473
|
+
set(url: string, response: HttpResponse<unknown>): void {
|
|
474
|
+
this.cache.set(url, {
|
|
475
|
+
response,
|
|
476
|
+
timestamp: Date.now()
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Clear specific cache entry
|
|
482
|
+
*/
|
|
483
|
+
delete(url: string): void {
|
|
484
|
+
this.cache.delete(url);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Clear all cache
|
|
489
|
+
*/
|
|
490
|
+
clear(): void {
|
|
491
|
+
this.cache.clear();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Clear expired cache entries
|
|
496
|
+
*/
|
|
497
|
+
clearExpired(): void {
|
|
498
|
+
const now = Date.now();
|
|
499
|
+
for (const [url, entry] of this.cache.entries()) {
|
|
500
|
+
if (now - entry.timestamp > this.maxAge) {
|
|
501
|
+
this.cache.delete(url);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}`;
|
|
506
|
+
|
|
507
|
+
await fs.writeFile(
|
|
508
|
+
path.join(config.fullPath, "src/app/core/services/cache.service.ts"),
|
|
509
|
+
cacheService,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Export all functions
|
|
514
|
+
module.exports = {
|
|
515
|
+
// Interceptors
|
|
516
|
+
createAuthInterceptor,
|
|
517
|
+
createErrorInterceptor,
|
|
518
|
+
createLoadingInterceptor,
|
|
519
|
+
createCachingInterceptor,
|
|
520
|
+
|
|
521
|
+
// Toast System
|
|
522
|
+
createToastService,
|
|
523
|
+
createToastComponent,
|
|
524
|
+
|
|
525
|
+
// Supporting Services
|
|
526
|
+
createLoadingService,
|
|
527
|
+
createCacheService,
|
|
528
|
+
};
|