create-ng-tailwind 1.0.0 → 2.0.2

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.
@@ -0,0 +1,606 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * AI Configuration Generator
6
+ *
7
+ * Strategy:
8
+ * Creates AI tool-specific configuration files only when explicitly selected
9
+ */
10
+
11
+ // ==================== CLAUDE CONFIG ====================
12
+
13
+ async function createClaudeConfig(projectPath, projectName) {
14
+ const claudeMd = `# ${projectName} - AI Assistant Guidelines
15
+
16
+ This is an Angular project scaffolded with **create-ng-tailwind**, using **Angular 20+ and Tailwind CSS v4**.
17
+
18
+ ## šŸ“ Project Structure
19
+
20
+ \`\`\`
21
+ src/app/
22
+ ā”œā”€ā”€ core/ # Core application logic
23
+ │ ā”œā”€ā”€ services/ # Auth, API, Toast, Modal, Loading, Cache, Storage, i18n
24
+ │ ā”œā”€ā”€ guards/ # Route guards (auth, role-based)
25
+ │ ā”œā”€ā”€ interceptors/ # HTTP interceptors (auth, error, loading, caching)
26
+ │ └── i18n/ # Translation services
27
+ ā”œā”€ā”€ shared/ # Reusable shared code
28
+ │ ā”œā”€ā”€ components/ # Button, Card, Spinner, Toast, Modal
29
+ │ ā”œā”€ā”€ pipes/ # TimeAgo, Truncate, FileSize, SafeHtml
30
+ │ ā”œā”€ā”€ directives/ # ClickOutside, Tooltip
31
+ │ └── models/ # TypeScript interfaces and types
32
+ ā”œā”€ā”€ features/ # Feature modules
33
+ │ ā”œā”€ā”€ home/
34
+ │ ā”œā”€ā”€ about/
35
+ │ ā”œā”€ā”€ contact/
36
+ │ └── auth/ # Login, Register, Forgot Password
37
+ └── layout/ # Layout components
38
+ ā”œā”€ā”€ header/ # Header with language switcher
39
+ ā”œā”€ā”€ footer/
40
+ └── auth/ # Auth layout
41
+ \`\`\`
42
+
43
+ ## šŸ›  Technology Stack
44
+
45
+ - **Framework:** Angular 20+ (Standalone Components)
46
+ - **Styling:** Tailwind CSS v4 (PostCSS)
47
+ - **State:** Angular Signals (reactive primitives)
48
+ - **HTTP:** HttpClient with 4 Interceptors
49
+ - **i18n:** @ngx-translate/core (English & Arabic with RTL)
50
+ - **Forms:** Reactive Forms with validation
51
+ - **Routing:** Angular Router with lazy loading
52
+ - **Icons:** @ng-icons/heroicons
53
+ - **Linting:** ESLint + Prettier + Husky
54
+
55
+ ## šŸŽÆ Path Aliases (Use These!)
56
+
57
+ - \`@core/*\` → \`src/app/core/*\`
58
+ - \`@shared/*\` → \`src/app/shared/*\`
59
+ - \`@features/*\` → \`src/app/features/*\`
60
+ - \`@environments/*\` → \`src/environments/*\`
61
+
62
+ ## ⚔ HTTP Interceptors (Pre-configured)
63
+
64
+ Interceptors run in this order:
65
+ 1. **authInterceptor** → Adds JWT Bearer token to requests
66
+ 2. **cachingInterceptor** → Caches GET requests (5min TTL)
67
+ 3. **loadingInterceptor** → Shows/hides loading indicator
68
+ 4. **errorInterceptor** → Handles errors globally + shows toast
69
+
70
+ ## 🧩 Available Services (Use These!)
71
+
72
+ ### ToastService
73
+ \`\`\`typescript
74
+ import { inject } from '@angular/core';
75
+ import { ToastService } from '@core/services/toast.service';
76
+
77
+ const toast = inject(ToastService);
78
+ toast.success('Profile updated!');
79
+ toast.error('Failed to save changes');
80
+ toast.warning('Session expiring soon');
81
+ toast.info('New message received');
82
+ \`\`\`
83
+
84
+ ### ModalService
85
+ \`\`\`typescript
86
+ import { inject } from '@angular/core';
87
+ import { ModalService } from '@core/services/modal.service';
88
+
89
+ const modal = inject(ModalService);
90
+
91
+ // Confirmation dialog
92
+ const confirmed = await modal.confirm(
93
+ 'Are you sure you want to delete this item?',
94
+ 'Confirm Delete'
95
+ );
96
+
97
+ if (confirmed) {
98
+ // Proceed with deletion
99
+ }
100
+
101
+ // Alert dialog
102
+ await modal.alert('Your session has expired', 'Session Timeout');
103
+ \`\`\`
104
+
105
+ ### ApiService
106
+ \`\`\`typescript
107
+ import { inject } from '@angular/core';
108
+ import { ApiService } from '@core/services/api.service';
109
+
110
+ const api = inject(ApiService);
111
+
112
+ // GET request
113
+ api.get<User[]>('users').subscribe(users => {
114
+ console.log(users);
115
+ });
116
+
117
+ // POST request
118
+ api.post<User>('users', userData).subscribe(newUser => {
119
+ console.log('Created:', newUser);
120
+ });
121
+
122
+ // PUT/DELETE also available
123
+ \`\`\`
124
+
125
+ ### AuthService
126
+ \`\`\`typescript
127
+ import { inject } from '@angular/core';
128
+ import { AuthService } from '@core/services/auth.service';
129
+
130
+ const auth = inject(AuthService);
131
+
132
+ // Login
133
+ auth.login({ email, password }).subscribe({
134
+ next: () => router.navigate(['/dashboard']),
135
+ error: (err) => console.error(err)
136
+ });
137
+
138
+ // Check auth state (reactive)
139
+ auth.isAuthenticated$.subscribe(isAuth => {
140
+ console.log('Authenticated:', isAuth);
141
+ });
142
+
143
+ // Logout
144
+ auth.logout();
145
+ \`\`\`
146
+
147
+ ### LoadingService
148
+ \`\`\`typescript
149
+ import { inject } from '@angular/core';
150
+ import { LoadingService } from '@core/services/loading.service';
151
+
152
+ const loading = inject(LoadingService);
153
+
154
+ // In template (automatic with HTTP interceptor)
155
+ @if (loading.isLoading()) {
156
+ <app-loading-spinner />
157
+ }
158
+
159
+ // Manual control (rarely needed)
160
+ loading.show();
161
+ // ... do work
162
+ loading.hide();
163
+ \`\`\`
164
+
165
+ ## šŸ“ Coding Patterns
166
+
167
+ ### Component Structure (Standalone)
168
+ \`\`\`typescript
169
+ import { Component, inject, signal } from '@angular/core';
170
+ import { CommonModule } from '@angular/common';
171
+ import { ToastService } from '@core/services/toast.service';
172
+
173
+ @Component({
174
+ selector: 'app-example',
175
+ standalone: true,
176
+ imports: [CommonModule],
177
+ template: \`
178
+ <div class="p-6 bg-white rounded-lg shadow">
179
+ <h2 class="text-xl font-bold mb-4">{{ title() }}</h2>
180
+ <button
181
+ (click)="handleClick()"
182
+ class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
183
+ Click Me ({{ count() }})
184
+ </button>
185
+ </div>
186
+ \`
187
+ })
188
+ export class ExampleComponent {
189
+ private toast = inject(ToastService);
190
+
191
+ // Use signals for reactive state
192
+ title = signal('Example Component');
193
+ count = signal(0);
194
+
195
+ handleClick(): void {
196
+ this.count.update(n => n + 1);
197
+ this.toast.success(\`Clicked \${this.count()} times\`);
198
+ }
199
+ }
200
+ \`\`\`
201
+
202
+ ### Service Creation
203
+ \`\`\`typescript
204
+ import { Injectable, inject, signal } from '@angular/core';
205
+ import { ApiService } from '@core/services/api.service';
206
+
207
+ interface Item {
208
+ id: string;
209
+ name: string;
210
+ }
211
+
212
+ @Injectable({ providedIn: 'root' })
213
+ export class ItemService {
214
+ private api = inject(ApiService);
215
+
216
+ // Use signals for reactive state
217
+ items = signal<Item[]>([]);
218
+ loading = signal(false);
219
+
220
+ loadItems(): void {
221
+ this.loading.set(true);
222
+ this.api.get<Item[]>('items').subscribe({
223
+ next: (data) => {
224
+ this.items.set(data);
225
+ this.loading.set(false);
226
+ },
227
+ error: () => this.loading.set(false)
228
+ });
229
+ }
230
+ }
231
+ \`\`\`
232
+
233
+ ### Reactive Forms
234
+ \`\`\`typescript
235
+ import { Component, inject } from '@angular/core';
236
+ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
237
+
238
+ @Component({
239
+ selector: 'app-contact-form',
240
+ standalone: true,
241
+ imports: [ReactiveFormsModule],
242
+ template: \`
243
+ <form [formGroup]="form" (ngSubmit)="onSubmit()">
244
+ <input formControlName="email" class="border p-2 rounded" />
245
+ @if (form.get('email')?.invalid && form.get('email')?.touched) {
246
+ <span class="text-red-500 text-sm">Email is required</span>
247
+ }
248
+ <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded">
249
+ Submit
250
+ </button>
251
+ </form>
252
+ \`
253
+ })
254
+ export class ContactFormComponent {
255
+ private fb = inject(FormBuilder);
256
+
257
+ form = this.fb.group({
258
+ email: ['', [Validators.required, Validators.email]],
259
+ message: ['', [Validators.required, Validators.minLength(10)]]
260
+ });
261
+
262
+ onSubmit(): void {
263
+ if (this.form.valid) {
264
+ console.log(this.form.value);
265
+ } else {
266
+ Object.keys(this.form.controls).forEach(key => {
267
+ this.form.get(key)?.markAsTouched();
268
+ });
269
+ }
270
+ }
271
+ }
272
+ \`\`\`
273
+
274
+ ### Routing with Lazy Loading
275
+ \`\`\`typescript
276
+ import { Routes } from '@angular/router';
277
+ import { authGuard } from '@core/guards/auth.guard';
278
+
279
+ export const routes: Routes = [
280
+ {
281
+ path: '',
282
+ loadComponent: () => import('./features/home/home.component')
283
+ .then(c => c.HomeComponent)
284
+ },
285
+ {
286
+ path: 'dashboard',
287
+ loadComponent: () => import('./features/dashboard/dashboard.component')
288
+ .then(c => c.DashboardComponent),
289
+ canActivate: [authGuard]
290
+ }
291
+ ];
292
+ \`\`\`
293
+
294
+ ## šŸŽØ Styling Guidelines
295
+
296
+ **Always use Tailwind CSS classes. Do NOT write custom CSS.**
297
+
298
+ Common patterns:
299
+ \`\`\`html
300
+ <!-- Layout -->
301
+ <div class="container mx-auto px-4">
302
+ <div class="flex items-center justify-between">
303
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
304
+
305
+ <!-- Spacing -->
306
+ <div class="p-4 mt-6 space-y-4">
307
+
308
+ <!-- Colors -->
309
+ <div class="bg-blue-500 text-white">
310
+ <button class="bg-gray-100 hover:bg-gray-200 text-gray-900">
311
+
312
+ <!-- Typography -->
313
+ <h1 class="text-3xl font-bold">
314
+ <p class="text-sm text-gray-600">
315
+
316
+ <!-- Responsive -->
317
+ <div class="hidden md:block">
318
+ <div class="w-full md:w-1/2 lg:w-1/3">
319
+ \`\`\`
320
+
321
+ ## šŸŒ Internationalization (i18n)
322
+
323
+ Support English and Arabic with RTL:
324
+
325
+ **Add translations to both files:**
326
+ - \`public/assets/i18n/en.json\`
327
+ - \`public/assets/i18n/ar.json\`
328
+
329
+ **In templates:**
330
+ \`\`\`html
331
+ <h1>{{ 'welcome.title' | translate }}</h1>
332
+ <button>{{ 'common.save' | translate }}</button>
333
+ \`\`\`
334
+
335
+ **In components:**
336
+ \`\`\`typescript
337
+ import { inject } from '@angular/core';
338
+ import { TranslateService } from '@ngx-translate/core';
339
+
340
+ const translate = inject(TranslateService);
341
+ const message = translate.instant('error.notFound');
342
+ \`\`\`
343
+
344
+ ## āœ… Best Practices
345
+
346
+ 1. **Always use standalone components** (no NgModule)
347
+ 2. **Use inject() function** (not constructor injection)
348
+ 3. **Use signals for reactive state** (not BehaviorSubject unless needed)
349
+ 4. **Use existing services** before creating new ones
350
+ 5. **Use Tailwind CSS classes** (no custom CSS)
351
+ 6. **Use path aliases** (@core/, @shared/, @features/)
352
+ 7. **Use lazy loading** for all routes
353
+ 8. **Use Reactive Forms** (not template-driven)
354
+ 9. **Write unit tests** for services and components
355
+ 10. **Support RTL** for Arabic language
356
+
357
+ ## āŒ Don't Do This
358
+
359
+ - āŒ Don't use NgModule (use standalone)
360
+ - āŒ Don't use constructor injection (use inject())
361
+ - āŒ Don't write custom CSS (use Tailwind)
362
+ - āŒ Don't use \`any\` type (use proper TypeScript types)
363
+ - āŒ Don't use \`src/assets/\` (use \`public/assets/\`)
364
+ - āŒ Don't use HttpClient directly (use ApiService)
365
+ - āŒ Don't create services that already exist
366
+ - āŒ Don't use template-driven forms
367
+ - āŒ Don't forget to add translations for new text
368
+
369
+ ## šŸ“¦ Available Pipes
370
+
371
+ - \`truncate:50:true\` - Truncate text to 50 chars with word boundaries
372
+ - \`translate\` - i18n translations (from @ngx-translate)
373
+
374
+ ## šŸ“¦ Available Directives
375
+
376
+ - \`appClickOutside\` - Detect clicks outside element
377
+ - \`appTooltip="text"\` - Show tooltip on hover
378
+
379
+ ## 🧪 Testing
380
+
381
+ Write unit tests using Jasmine/Karma:
382
+ \`\`\`typescript
383
+ describe('ExampleComponent', () => {
384
+ let component: ExampleComponent;
385
+ let fixture: ComponentFixture<ExampleComponent>;
386
+
387
+ beforeEach(() => {
388
+ TestBed.configureTestingModule({
389
+ imports: [ExampleComponent]
390
+ });
391
+ fixture = TestBed.createComponent(ExampleComponent);
392
+ component = fixture.componentInstance;
393
+ });
394
+
395
+ it('should create', () => {
396
+ expect(component).toBeTruthy();
397
+ });
398
+ });
399
+ \`\`\`
400
+
401
+ ## šŸ“š Important Notes
402
+
403
+ - Assets go in \`public/assets/\` (not \`src/assets/\`)
404
+ - Tailwind v4 uses CSS imports: \`@import "tailwindcss";\`
405
+ - PostCSS config is in \`.postcssrc.json\`
406
+ - HTTP interceptors run automatically (no manual setup needed)
407
+ - Loading state is automatic for HTTP requests
408
+ - Error handling is automatic (shows toast notifications)
409
+
410
+ ---
411
+
412
+ **Remember:** This is a production-ready starter. Use the existing infrastructure before creating new utilities!
413
+ `;
414
+
415
+ // Create .claude directory if it doesn't exist
416
+ const claudeDir = path.join(projectPath, ".claude");
417
+ await fs.ensureDir(claudeDir);
418
+
419
+ // Write claude.md inside .claude folder
420
+ await fs.writeFile(path.join(claudeDir, "claude.md"), claudeMd);
421
+ }
422
+
423
+ // ==================== CURSOR-SPECIFIC CONFIG ====================
424
+
425
+ async function createCursorConfig(projectPath, projectName) {
426
+ const cursorRules = `# ${projectName} - Cursor AI Rules
427
+
428
+ ## Quick Reference
429
+ - Angular 20+ standalone components
430
+ - Tailwind CSS v4 (use utility classes only)
431
+ - inject() for DI (not constructors)
432
+ - Signals for state (not BehaviorSubject)
433
+ - Path aliases: @core/, @shared/, @features/, @environments/
434
+
435
+ ## Component Pattern
436
+ \`\`\`typescript
437
+ import { Component, inject, signal } from '@angular/core';
438
+ import { CommonModule } from '@angular/common';
439
+
440
+ @Component({
441
+ selector: 'app-name',
442
+ standalone: true,
443
+ imports: [CommonModule],
444
+ template: \`<div class="p-4">{{ text() }}</div>\`
445
+ })
446
+ export class NameComponent {
447
+ private service = inject(ServiceName);
448
+ text = signal('Hello');
449
+ }
450
+ \`\`\`
451
+
452
+ ## Available Services (Use These!)
453
+ - **ToastService** - \`toast.success('Message')\`
454
+ - **ModalService** - \`await modal.confirm('Message')\`
455
+ - **ApiService** - \`api.get<Type>('endpoint')\`
456
+ - **AuthService** - \`auth.login(credentials)\`
457
+ - **LoadingService** - \`loading.isLoading()\`
458
+
459
+ ## HTTP Interceptors (Automatic)
460
+ Auth → Cache → Loading → Error
461
+
462
+ ## Forms: Use Reactive Forms
463
+ \`\`\`typescript
464
+ form = inject(FormBuilder).group({
465
+ email: ['', [Validators.required, Validators.email]]
466
+ });
467
+ \`\`\`
468
+
469
+ ## Routing: Use Lazy Loading
470
+ \`\`\`typescript
471
+ {
472
+ path: 'feature',
473
+ loadComponent: () => import('./feature/feature.component')
474
+ .then(c => c.FeatureComponent)
475
+ }
476
+ \`\`\`
477
+
478
+ ## Styling: Tailwind Only
479
+ Use: \`bg-blue-500 text-white p-4 rounded hover:bg-blue-600\`
480
+
481
+ ## i18n: Support English + Arabic
482
+ Add keys to \`public/assets/i18n/en.json\` and \`ar.json\`
483
+
484
+ ## Don't
485
+ - No NgModule
486
+ - No constructor injection
487
+ - No custom CSS
488
+ - No \`any\` type
489
+ - No \`src/assets/\` (use \`public/assets/\`)
490
+ - No HttpClient directly (use ApiService)
491
+ `;
492
+
493
+ await fs.writeFile(path.join(projectPath, ".cursorrules"), cursorRules);
494
+ }
495
+
496
+ // ==================== WINDSURF-SPECIFIC CONFIG ====================
497
+
498
+ async function createWindsurfConfig(projectPath, projectName) {
499
+ const windsurfRules = `# ${projectName} - Windsurf AI Rules
500
+
501
+ Angular 20+ | Tailwind CSS v4 | Standalone Components | Signals
502
+
503
+ ## Quick Patterns
504
+
505
+ Component:
506
+ \`\`\`typescript
507
+ @Component({
508
+ selector: 'app-name',
509
+ standalone: true,
510
+ imports: [CommonModule],
511
+ template: \`<div class="p-4">{{ state() }}</div>\`
512
+ })
513
+ export class NameComponent {
514
+ private svc = inject(Service);
515
+ state = signal(value);
516
+ }
517
+ \`\`\`
518
+
519
+ Show Toast:
520
+ \`\`\`typescript
521
+ inject(ToastService).success('Message');
522
+ \`\`\`
523
+
524
+ Open Modal:
525
+ \`\`\`typescript
526
+ await inject(ModalService).confirm('Message');
527
+ \`\`\`
528
+
529
+ API Call:
530
+ \`\`\`typescript
531
+ inject(ApiService).get<Type>('endpoint').subscribe(...);
532
+ \`\`\`
533
+
534
+ Form:
535
+ \`\`\`typescript
536
+ form = inject(FormBuilder).group({
537
+ field: ['', Validators.required]
538
+ });
539
+ \`\`\`
540
+
541
+ ## Path Aliases
542
+ @core/ @shared/ @features/ @environments/
543
+
544
+ ## Services Available
545
+ ToastService, ModalService, ApiService, AuthService, LoadingService, CacheService
546
+
547
+ ## HTTP Interceptors (Auto)
548
+ Auth → Cache → Loading → Error
549
+
550
+ ## Styling
551
+ Tailwind only: bg-blue-500 text-white p-4 rounded
552
+
553
+ ## i18n
554
+ public/assets/i18n/en.json + ar.json
555
+
556
+ ## Don't
557
+ No NgModule, No constructors, No custom CSS, No 'any', No src/assets
558
+ `;
559
+
560
+ await fs.writeFile(path.join(projectPath, ".windsurfrules"), windsurfRules);
561
+ }
562
+
563
+ // ==================== MAIN EXPORT ====================
564
+
565
+ /**
566
+ * Create AI configuration files
567
+ * Creates tool-specific configs only when explicitly selected
568
+ *
569
+ * @param {string} projectPath - Full path to the project
570
+ * @param {string} projectName - Name of the project
571
+ * @param {string[]} aiTools - Array of selected AI tools
572
+ */
573
+ async function createAIConfigs(projectPath, projectName, aiTools) {
574
+ // Skip if user selected "none"
575
+ if (
576
+ !aiTools ||
577
+ aiTools.length === 0 ||
578
+ (aiTools.length === 1 && aiTools[0] === "none")
579
+ ) {
580
+ return;
581
+ }
582
+
583
+ console.log("\nšŸ¤– Configuring AI tools...");
584
+
585
+ // Create tool-specific configs only if selected
586
+ if (aiTools.includes("claude")) {
587
+ await createClaudeConfig(projectPath, projectName);
588
+ console.log(" āœ“ .claude/claude.md created");
589
+ }
590
+
591
+ if (aiTools.includes("cursor")) {
592
+ await createCursorConfig(projectPath, projectName);
593
+ console.log(" āœ“ .cursorrules created");
594
+ }
595
+
596
+ if (aiTools.includes("windsurf")) {
597
+ await createWindsurfConfig(projectPath, projectName);
598
+ console.log(" āœ“ .windsurfrules created");
599
+ }
600
+
601
+ console.log("✨ AI configuration complete!\n");
602
+ }
603
+
604
+ module.exports = {
605
+ createAIConfigs,
606
+ };
@@ -0,0 +1,14 @@
1
+ const TEMPLATES = {
2
+ MINIMAL: "minimal",
3
+ STARTER: "starter",
4
+ };
5
+
6
+ const ANGULAR_VERSION = "latest";
7
+
8
+ const TAILWIND_VERSION = "^4.0.0";
9
+
10
+ module.exports = {
11
+ TEMPLATES,
12
+ ANGULAR_VERSION,
13
+ TAILWIND_VERSION,
14
+ };
@@ -0,0 +1,60 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Validate project name
6
+ */
7
+ function validateProjectName(name) {
8
+ if (!name || !name.trim()) {
9
+ return { valid: false, error: "Project name is required" };
10
+ }
11
+
12
+ if (!/^[a-z0-9-]+$/.test(name)) {
13
+ return {
14
+ valid: false,
15
+ error:
16
+ "Project name can only contain lowercase letters, numbers, and hyphens",
17
+ };
18
+ }
19
+
20
+ return { valid: true };
21
+ }
22
+
23
+ /**
24
+ * Check if directory exists
25
+ */
26
+ async function directoryExists(dirPath) {
27
+ try {
28
+ const stats = await fs.stat(dirPath);
29
+ return stats.isDirectory();
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Ensure directory exists
37
+ */
38
+ async function ensureDirectory(dirPath) {
39
+ await fs.ensureDir(dirPath);
40
+ }
41
+
42
+ /**
43
+ * Copy file with error handling
44
+ */
45
+ async function safeCopy(src, dest) {
46
+ try {
47
+ await fs.copy(src, dest);
48
+ return true;
49
+ } catch (error) {
50
+ console.error(`Failed to copy ${src} to ${dest}:`, error.message);
51
+ return false;
52
+ }
53
+ }
54
+
55
+ module.exports = {
56
+ validateProjectName,
57
+ directoryExists,
58
+ ensureDirectory,
59
+ safeCopy,
60
+ };