create-ng-tailwind 3.1.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of create-ng-tailwind might be problematic. Click here for more details.
- package/CHANGELOG.md +96 -341
- package/README.md +111 -157
- package/lib/cli/index.js +74 -3
- package/lib/cli/interactive.js +26 -1
- package/lib/managers/ProjectManager.js +2 -5
- package/lib/templates/base/components.js +243 -0
- package/lib/templates/base/index.js +207 -0
- package/lib/templates/base/infrastructure.js +314 -0
- package/lib/templates/base/linting.js +359 -0
- package/lib/templates/base/pwa.js +103 -0
- package/lib/templates/base/services.js +362 -0
- package/lib/templates/blog/app.js +250 -0
- package/lib/templates/blog/components.js +360 -0
- package/lib/templates/blog/i18n.js +77 -0
- package/lib/templates/blog/index.js +126 -0
- package/lib/templates/blog/pages.js +554 -0
- package/lib/templates/blog/services.js +390 -0
- package/lib/templates/dashboard/app.js +320 -0
- package/lib/templates/dashboard/charts.js +305 -0
- package/lib/templates/dashboard/components.js +410 -0
- package/lib/templates/dashboard/i18n.js +340 -0
- package/lib/templates/dashboard/index.js +141 -0
- package/lib/templates/dashboard/layout.js +310 -0
- package/lib/templates/dashboard/pages.js +681 -0
- package/lib/templates/ecommerce/app.js +315 -0
- package/lib/templates/ecommerce/components.js +496 -0
- package/lib/templates/ecommerce/i18n.js +389 -0
- package/lib/templates/ecommerce/index.js +152 -0
- package/lib/templates/ecommerce/layout.js +270 -0
- package/lib/templates/ecommerce/pages.js +969 -0
- package/lib/templates/ecommerce/services.js +300 -0
- package/lib/templates/index.js +12 -0
- package/lib/templates/landing/index.js +1117 -0
- package/lib/templates/portfolio/index.js +1160 -0
- package/lib/templates/saas/index.js +1371 -0
- package/lib/templates/starter/app.js +364 -0
- package/lib/templates/starter/i18n.js +856 -0
- package/lib/templates/starter/index.js +52 -4060
- package/lib/templates/starter/layout.js +852 -0
- package/lib/templates/starter/pages.js +1241 -0
- package/lib/utils/nodeCompat.js +85 -0
- package/package.json +1 -1
- package/lib/templates/starter/features.js +0 -867
- package/lib/utils/ai-config.js +0 -641
- /package/lib/templates/{starter → base}/advanced-features.js +0 -0
- /package/lib/templates/{starter → base}/seo-assets.js +0 -0
- /package/lib/templates/{starter → base}/seo-features.js +0 -0
- /package/lib/templates/{starter → base}/ui-features.js +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create directory structure for base template
|
|
6
|
+
*/
|
|
7
|
+
async function createDirectoryStructure(config) {
|
|
8
|
+
const directories = [
|
|
9
|
+
'src/environments',
|
|
10
|
+
'src/app/core/services',
|
|
11
|
+
'src/app/core/guards',
|
|
12
|
+
'src/app/core/interceptors',
|
|
13
|
+
'src/app/core/utils',
|
|
14
|
+
'src/app/shared/components/button',
|
|
15
|
+
'src/app/shared/components/card',
|
|
16
|
+
'src/app/shared/components/loading-spinner',
|
|
17
|
+
'src/app/shared/components/toast',
|
|
18
|
+
'src/app/shared/components/modal',
|
|
19
|
+
'src/app/shared/pipes',
|
|
20
|
+
'src/app/shared/directives',
|
|
21
|
+
'src/app/shared/models',
|
|
22
|
+
'public/assets/images',
|
|
23
|
+
'public/assets/icons',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
for (const dir of directories) {
|
|
27
|
+
await fs.ensureDir(path.join(config.fullPath, dir));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create environment files
|
|
33
|
+
*/
|
|
34
|
+
async function createEnvironments(config) {
|
|
35
|
+
// Development environment
|
|
36
|
+
const devEnvironment = `export const environment = {
|
|
37
|
+
production: false,
|
|
38
|
+
apiUrl: 'http://localhost:3000/api',
|
|
39
|
+
appName: '${config.projectName}',
|
|
40
|
+
version: '1.0.0',
|
|
41
|
+
enableDevTools: true,
|
|
42
|
+
logLevel: 'debug' as 'debug' | 'info' | 'warn' | 'error',
|
|
43
|
+
features: {
|
|
44
|
+
enableAnalytics: false,
|
|
45
|
+
enableLogging: true,
|
|
46
|
+
enableMocking: true
|
|
47
|
+
},
|
|
48
|
+
auth: {
|
|
49
|
+
tokenKey: 'auth_token',
|
|
50
|
+
refreshTokenKey: 'refresh_token',
|
|
51
|
+
tokenExpirationBuffer: 300000 // 5 minutes
|
|
52
|
+
},
|
|
53
|
+
api: {
|
|
54
|
+
timeout: 30000,
|
|
55
|
+
retryAttempts: 3
|
|
56
|
+
}
|
|
57
|
+
};`;
|
|
58
|
+
|
|
59
|
+
await fs.writeFile(
|
|
60
|
+
path.join(config.fullPath, 'src/environments/environment.ts'),
|
|
61
|
+
devEnvironment
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Production environment
|
|
65
|
+
const prodEnvironment = `export const environment = {
|
|
66
|
+
production: true,
|
|
67
|
+
apiUrl: 'https://api.${config.projectName}.com/api',
|
|
68
|
+
appName: '${config.projectName}',
|
|
69
|
+
version: '1.0.0',
|
|
70
|
+
enableDevTools: false,
|
|
71
|
+
logLevel: 'error' as 'debug' | 'info' | 'warn' | 'error',
|
|
72
|
+
features: {
|
|
73
|
+
enableAnalytics: true,
|
|
74
|
+
enableLogging: false,
|
|
75
|
+
enableMocking: false
|
|
76
|
+
},
|
|
77
|
+
auth: {
|
|
78
|
+
tokenKey: 'auth_token',
|
|
79
|
+
refreshTokenKey: 'refresh_token',
|
|
80
|
+
tokenExpirationBuffer: 300000 // 5 minutes
|
|
81
|
+
},
|
|
82
|
+
api: {
|
|
83
|
+
timeout: 30000,
|
|
84
|
+
retryAttempts: 1
|
|
85
|
+
}
|
|
86
|
+
};`;
|
|
87
|
+
|
|
88
|
+
await fs.writeFile(
|
|
89
|
+
path.join(config.fullPath, 'src/environments/environment.prod.ts'),
|
|
90
|
+
prodEnvironment
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create auth guard
|
|
96
|
+
*/
|
|
97
|
+
async function createGuards(config) {
|
|
98
|
+
const authGuard = `import { CanActivateFn, Router } from '@angular/router';
|
|
99
|
+
import { inject } from '@angular/core';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Simple authentication guard
|
|
103
|
+
* Replace with your actual authentication logic
|
|
104
|
+
*/
|
|
105
|
+
export const authGuard: CanActivateFn = (_route, _state) => {
|
|
106
|
+
const router = inject(Router);
|
|
107
|
+
|
|
108
|
+
// Replace with your actual authentication check
|
|
109
|
+
const isAuthenticated = localStorage.getItem('auth-token') !== null;
|
|
110
|
+
|
|
111
|
+
if (!isAuthenticated) {
|
|
112
|
+
// Redirect to login page
|
|
113
|
+
router.navigate(['/auth/login']);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return true;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Example: Check if user has specific role
|
|
122
|
+
*/
|
|
123
|
+
export const roleGuard = (requiredRole: string): CanActivateFn => {
|
|
124
|
+
return (_route, _state) => {
|
|
125
|
+
const router = inject(Router);
|
|
126
|
+
|
|
127
|
+
// Replace with your actual role checking logic
|
|
128
|
+
const userRole = localStorage.getItem('user-role');
|
|
129
|
+
|
|
130
|
+
if (userRole !== requiredRole) {
|
|
131
|
+
router.navigate(['/unauthorized']);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
};
|
|
137
|
+
};`;
|
|
138
|
+
|
|
139
|
+
await fs.writeFile(path.join(config.fullPath, 'src/app/core/guards/auth.guard.ts'), authGuard);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create model interfaces
|
|
144
|
+
*/
|
|
145
|
+
async function createModels(config) {
|
|
146
|
+
// User Interface
|
|
147
|
+
const userInterface = `export interface User {
|
|
148
|
+
id: string;
|
|
149
|
+
email: string;
|
|
150
|
+
firstName: string;
|
|
151
|
+
lastName: string;
|
|
152
|
+
avatar?: string;
|
|
153
|
+
role: 'user' | 'admin' | 'moderator';
|
|
154
|
+
createdAt: Date;
|
|
155
|
+
updatedAt: Date;
|
|
156
|
+
isActive: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface UserProfile {
|
|
160
|
+
user: User;
|
|
161
|
+
preferences: {
|
|
162
|
+
notifications: boolean;
|
|
163
|
+
language: string;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface CreateUserRequest {
|
|
168
|
+
email: string;
|
|
169
|
+
password: string;
|
|
170
|
+
firstName: string;
|
|
171
|
+
lastName: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface UpdateUserRequest {
|
|
175
|
+
firstName?: string;
|
|
176
|
+
lastName?: string;
|
|
177
|
+
avatar?: string;
|
|
178
|
+
}`;
|
|
179
|
+
|
|
180
|
+
await fs.writeFile(
|
|
181
|
+
path.join(config.fullPath, 'src/app/shared/models/user.interface.ts'),
|
|
182
|
+
userInterface
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// API Response Interface
|
|
186
|
+
const apiResponseInterface = `export interface ApiResponse<T = unknown> {
|
|
187
|
+
data: T;
|
|
188
|
+
message: string;
|
|
189
|
+
success: boolean;
|
|
190
|
+
errors?: string[];
|
|
191
|
+
meta?: {
|
|
192
|
+
total: number;
|
|
193
|
+
page: number;
|
|
194
|
+
perPage: number;
|
|
195
|
+
totalPages: number;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface ApiError {
|
|
200
|
+
message: string;
|
|
201
|
+
code: string;
|
|
202
|
+
status: number;
|
|
203
|
+
timestamp: string;
|
|
204
|
+
details?: Record<string, string[]>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface PaginatedResponse<T> {
|
|
208
|
+
data: T[];
|
|
209
|
+
meta: {
|
|
210
|
+
total: number;
|
|
211
|
+
page: number;
|
|
212
|
+
perPage: number;
|
|
213
|
+
totalPages: number;
|
|
214
|
+
hasNextPage: boolean;
|
|
215
|
+
hasPrevPage: boolean;
|
|
216
|
+
};
|
|
217
|
+
}`;
|
|
218
|
+
|
|
219
|
+
await fs.writeFile(
|
|
220
|
+
path.join(config.fullPath, 'src/app/shared/models/api-response.interface.ts'),
|
|
221
|
+
apiResponseInterface
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create pipes (Truncate, TimeAgo)
|
|
227
|
+
*/
|
|
228
|
+
async function createPipes(config) {
|
|
229
|
+
// Truncate Pipe
|
|
230
|
+
const truncatePipe = `import { Pipe, PipeTransform } from '@angular/core';
|
|
231
|
+
|
|
232
|
+
@Pipe({
|
|
233
|
+
name: 'truncate',
|
|
234
|
+
standalone: true
|
|
235
|
+
})
|
|
236
|
+
export class TruncatePipe implements PipeTransform {
|
|
237
|
+
transform(value: string | null | undefined, limit = 50, suffix = '...'): string {
|
|
238
|
+
if (!value) return '';
|
|
239
|
+
|
|
240
|
+
if (value.length <= limit) {
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return value.substring(0, limit).trim() + suffix;
|
|
245
|
+
}
|
|
246
|
+
}`;
|
|
247
|
+
|
|
248
|
+
await fs.writeFile(
|
|
249
|
+
path.join(config.fullPath, 'src/app/shared/pipes/truncate.pipe.ts'),
|
|
250
|
+
truncatePipe
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Time Ago Pipe
|
|
254
|
+
const timeAgoPipe = `import { Pipe, PipeTransform } from '@angular/core';
|
|
255
|
+
|
|
256
|
+
@Pipe({
|
|
257
|
+
name: 'timeAgo',
|
|
258
|
+
standalone: true
|
|
259
|
+
})
|
|
260
|
+
export class TimeAgoPipe implements PipeTransform {
|
|
261
|
+
transform(value: string | number | Date | null | undefined): string {
|
|
262
|
+
if (!value) return '';
|
|
263
|
+
|
|
264
|
+
const date = new Date(value);
|
|
265
|
+
const now = new Date();
|
|
266
|
+
const secondsAgo = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
267
|
+
|
|
268
|
+
if (secondsAgo < 60) {
|
|
269
|
+
return 'just now';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const minutesAgo = Math.floor(secondsAgo / 60);
|
|
273
|
+
if (minutesAgo < 60) {
|
|
274
|
+
return \`\${minutesAgo} minute\${minutesAgo === 1 ? '' : 's'} ago\`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const hoursAgo = Math.floor(minutesAgo / 60);
|
|
278
|
+
if (hoursAgo < 24) {
|
|
279
|
+
return \`\${hoursAgo} hour\${hoursAgo === 1 ? '' : 's'} ago\`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const daysAgo = Math.floor(hoursAgo / 24);
|
|
283
|
+
if (daysAgo < 7) {
|
|
284
|
+
return \`\${daysAgo} day\${daysAgo === 1 ? '' : 's'} ago\`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const weeksAgo = Math.floor(daysAgo / 7);
|
|
288
|
+
if (weeksAgo < 4) {
|
|
289
|
+
return \`\${weeksAgo} week\${weeksAgo === 1 ? '' : 's'} ago\`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const monthsAgo = Math.floor(daysAgo / 30);
|
|
293
|
+
if (monthsAgo < 12) {
|
|
294
|
+
return \`\${monthsAgo} month\${monthsAgo === 1 ? '' : 's'} ago\`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const yearsAgo = Math.floor(daysAgo / 365);
|
|
298
|
+
return \`\${yearsAgo} year\${yearsAgo === 1 ? '' : 's'} ago\`;
|
|
299
|
+
}
|
|
300
|
+
}`;
|
|
301
|
+
|
|
302
|
+
await fs.writeFile(
|
|
303
|
+
path.join(config.fullPath, 'src/app/shared/pipes/time-ago.pipe.ts'),
|
|
304
|
+
timeAgoPipe
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
createDirectoryStructure,
|
|
310
|
+
createEnvironments,
|
|
311
|
+
createGuards,
|
|
312
|
+
createModels,
|
|
313
|
+
createPipes,
|
|
314
|
+
};
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create VS Code settings and extensions
|
|
6
|
+
*/
|
|
7
|
+
async function createVSCodeSettings(config) {
|
|
8
|
+
// Create .vscode directory
|
|
9
|
+
await fs.ensureDir(path.join(config.fullPath, '.vscode'));
|
|
10
|
+
|
|
11
|
+
// VSCode settings for format on save
|
|
12
|
+
const vscodeSettings = {
|
|
13
|
+
'editor.formatOnSave': true,
|
|
14
|
+
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
15
|
+
'editor.codeActionsOnSave': {
|
|
16
|
+
'source.fixAll.eslint': 'explicit',
|
|
17
|
+
},
|
|
18
|
+
'[typescript]': {
|
|
19
|
+
'editor.formatOnSave': true,
|
|
20
|
+
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
21
|
+
},
|
|
22
|
+
'[html]': {
|
|
23
|
+
'editor.formatOnSave': true,
|
|
24
|
+
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
25
|
+
},
|
|
26
|
+
'[css]': {
|
|
27
|
+
'editor.formatOnSave': true,
|
|
28
|
+
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
29
|
+
},
|
|
30
|
+
'[json]': {
|
|
31
|
+
'editor.formatOnSave': true,
|
|
32
|
+
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
33
|
+
},
|
|
34
|
+
'[jsonc]': {
|
|
35
|
+
'editor.formatOnSave': true,
|
|
36
|
+
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
37
|
+
},
|
|
38
|
+
'files.eol': '\n',
|
|
39
|
+
'files.trimTrailingWhitespace': true,
|
|
40
|
+
'files.insertFinalNewline': true,
|
|
41
|
+
'editor.tabSize': 2,
|
|
42
|
+
'prettier.requireConfig': false,
|
|
43
|
+
'eslint.validate': ['javascript', 'typescript', 'html'],
|
|
44
|
+
// Suppress CSS linting warnings for Tailwind CSS directives
|
|
45
|
+
'css.lint.unknownAtRules': 'ignore',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// VSCode extensions recommendations
|
|
49
|
+
const vscodeExtensions = {
|
|
50
|
+
recommendations: [
|
|
51
|
+
'esbenp.prettier-vscode',
|
|
52
|
+
'dbaeumer.vscode-eslint',
|
|
53
|
+
'angular.ng-template',
|
|
54
|
+
'bradlc.vscode-tailwindcss',
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Write settings.json
|
|
59
|
+
await fs.writeFile(
|
|
60
|
+
path.join(config.fullPath, '.vscode/settings.json'),
|
|
61
|
+
JSON.stringify(vscodeSettings, null, 2)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Write extensions.json
|
|
65
|
+
await fs.writeFile(
|
|
66
|
+
path.join(config.fullPath, '.vscode/extensions.json'),
|
|
67
|
+
JSON.stringify(vscodeExtensions, null, 2)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Update TypeScript config with path mappings
|
|
73
|
+
*/
|
|
74
|
+
async function updateTsConfig(config) {
|
|
75
|
+
// Define path mapping configuration
|
|
76
|
+
const pathMappingConfig = ` "baseUrl": "./",
|
|
77
|
+
"paths": {
|
|
78
|
+
"@/*": ["src/*"],
|
|
79
|
+
"@app/*": ["src/app/*"],
|
|
80
|
+
"@core/*": ["src/app/core/*"],
|
|
81
|
+
"@shared/*": ["src/app/shared/*"],
|
|
82
|
+
"@features/*": ["src/app/features/*"],
|
|
83
|
+
"@layout/*": ["src/app/layout/*"],
|
|
84
|
+
"@environments/*": ["src/environments/*"]
|
|
85
|
+
},`;
|
|
86
|
+
|
|
87
|
+
// Helper function to add path mappings to a tsconfig file
|
|
88
|
+
const addPathMappings = (content) => {
|
|
89
|
+
if (content.includes('"compilerOptions": {')) {
|
|
90
|
+
const compilerOptionsStart =
|
|
91
|
+
content.indexOf('"compilerOptions": {') + '"compilerOptions": {'.length;
|
|
92
|
+
const beforeOptions = content.substring(0, compilerOptionsStart);
|
|
93
|
+
const afterOptions = content.substring(compilerOptionsStart);
|
|
94
|
+
return beforeOptions + '\n' + pathMappingConfig + afterOptions;
|
|
95
|
+
}
|
|
96
|
+
return content;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Update tsconfig.json (base config) - CRITICAL for IDE support
|
|
100
|
+
const tsConfigPath = path.join(config.fullPath, 'tsconfig.json');
|
|
101
|
+
let tsConfigContent = await fs.readFile(tsConfigPath, 'utf8');
|
|
102
|
+
tsConfigContent = addPathMappings(tsConfigContent);
|
|
103
|
+
await fs.writeFile(tsConfigPath, tsConfigContent);
|
|
104
|
+
|
|
105
|
+
// Update tsconfig.app.json
|
|
106
|
+
const tsConfigAppPath = path.join(config.fullPath, 'tsconfig.app.json');
|
|
107
|
+
let tsConfigAppContent = await fs.readFile(tsConfigAppPath, 'utf8');
|
|
108
|
+
tsConfigAppContent = addPathMappings(tsConfigAppContent);
|
|
109
|
+
await fs.writeFile(tsConfigAppPath, tsConfigAppContent);
|
|
110
|
+
|
|
111
|
+
// Update tsconfig.spec.json
|
|
112
|
+
const tsConfigSpecPath = path.join(config.fullPath, 'tsconfig.spec.json');
|
|
113
|
+
let tsConfigSpecContent = await fs.readFile(tsConfigSpecPath, 'utf8');
|
|
114
|
+
tsConfigSpecContent = addPathMappings(tsConfigSpecContent);
|
|
115
|
+
await fs.writeFile(tsConfigSpecPath, tsConfigSpecContent);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Configure angular.json for ESLint
|
|
120
|
+
*/
|
|
121
|
+
async function configureAngularJson(config) {
|
|
122
|
+
const angularJsonPath = path.join(config.fullPath, 'angular.json');
|
|
123
|
+
const angularJson = JSON.parse(await fs.readFile(angularJsonPath, 'utf8'));
|
|
124
|
+
|
|
125
|
+
// Add lint architect target
|
|
126
|
+
if (angularJson.projects && angularJson.projects[config.projectName]) {
|
|
127
|
+
angularJson.projects[config.projectName].architect.lint = {
|
|
128
|
+
builder: '@angular-eslint/builder:lint',
|
|
129
|
+
options: {
|
|
130
|
+
lintFilePatterns: ['src/**/*.ts', 'src/**/*.html'],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await fs.writeFile(angularJsonPath, JSON.stringify(angularJson, null, 2));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Install linting packages
|
|
140
|
+
*/
|
|
141
|
+
async function installLintingPackages(config) {
|
|
142
|
+
const execa = require('execa');
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const packages = [
|
|
146
|
+
'@eslint/js@^9.23.0',
|
|
147
|
+
'eslint@^9.23.0',
|
|
148
|
+
'angular-eslint@19.3.0',
|
|
149
|
+
'typescript-eslint@^8.20.0',
|
|
150
|
+
'prettier@^3.5.3',
|
|
151
|
+
'eslint-config-prettier@^10.1.2',
|
|
152
|
+
'prettier-plugin-tailwindcss@^0.6.11',
|
|
153
|
+
'simple-git-hooks@^2.11.1',
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
await execa.command(`npm install ${packages.join(' ')} --save-dev`, {
|
|
157
|
+
cwd: config.fullPath,
|
|
158
|
+
stdio: 'pipe',
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// Silently fail - packages will be installed when user runs npm install
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Setup linting (ESLint, Prettier, simple-git-hooks)
|
|
167
|
+
*/
|
|
168
|
+
async function setupLinting(config) {
|
|
169
|
+
// Read current package.json
|
|
170
|
+
const packageJsonPath = path.join(config.fullPath, 'package.json');
|
|
171
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
172
|
+
|
|
173
|
+
// Add icons and PWA to regular dependencies
|
|
174
|
+
if (!packageJson.dependencies) {
|
|
175
|
+
packageJson.dependencies = {};
|
|
176
|
+
}
|
|
177
|
+
packageJson.dependencies['@ng-icons/core'] = '^29.7.0';
|
|
178
|
+
packageJson.dependencies['@ng-icons/heroicons'] = '^29.7.0';
|
|
179
|
+
packageJson.dependencies['@angular/service-worker'] = packageJson.dependencies['@angular/core'];
|
|
180
|
+
|
|
181
|
+
// Add linting dev dependencies
|
|
182
|
+
const lintingDependencies = {
|
|
183
|
+
'@eslint/js': '^9.23.0',
|
|
184
|
+
eslint: '^9.23.0',
|
|
185
|
+
'angular-eslint': '19.3.0',
|
|
186
|
+
'typescript-eslint': '^8.20.0',
|
|
187
|
+
prettier: '^3.5.3',
|
|
188
|
+
'eslint-config-prettier': '^10.1.2',
|
|
189
|
+
'prettier-plugin-tailwindcss': '^0.6.11',
|
|
190
|
+
'simple-git-hooks': '^2.11.1',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Add enhanced scripts
|
|
194
|
+
const enhancedScripts = {
|
|
195
|
+
lint: 'ng lint',
|
|
196
|
+
'lint:fix': 'ng lint --fix',
|
|
197
|
+
format: 'prettier --write "**/*.{ts,html,css,json,md}"',
|
|
198
|
+
'format:check': 'prettier --check "**/*.{ts,html,css,json,md}"',
|
|
199
|
+
'format:ts': 'prettier --write "**/*.ts"',
|
|
200
|
+
'code:check': 'npm run format:check && npm run lint',
|
|
201
|
+
'code:fix': 'npm run format && npm run lint:fix',
|
|
202
|
+
prepare: 'simple-git-hooks',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Add prettier configuration
|
|
206
|
+
const prettierConfig = {
|
|
207
|
+
printWidth: 100,
|
|
208
|
+
singleQuote: true,
|
|
209
|
+
trailingComma: 'es5',
|
|
210
|
+
tabWidth: 2,
|
|
211
|
+
semi: true,
|
|
212
|
+
arrowParens: 'always',
|
|
213
|
+
endOfLine: 'lf',
|
|
214
|
+
plugins: ['prettier-plugin-tailwindcss'],
|
|
215
|
+
overrides: [
|
|
216
|
+
{
|
|
217
|
+
files: '*.html',
|
|
218
|
+
options: {
|
|
219
|
+
parser: 'angular',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// simple-git-hooks configuration (lightweight pre-commit hooks)
|
|
226
|
+
const simpleGitHooksConfig = {
|
|
227
|
+
'pre-commit': 'npm run lint',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Update package.json
|
|
231
|
+
packageJson.devDependencies = {
|
|
232
|
+
...packageJson.devDependencies,
|
|
233
|
+
...lintingDependencies,
|
|
234
|
+
};
|
|
235
|
+
packageJson.scripts = { ...packageJson.scripts, ...enhancedScripts };
|
|
236
|
+
packageJson.prettier = prettierConfig;
|
|
237
|
+
packageJson['simple-git-hooks'] = simpleGitHooksConfig;
|
|
238
|
+
|
|
239
|
+
// Write updated package.json
|
|
240
|
+
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
241
|
+
|
|
242
|
+
// Configure angular.json for ESLint
|
|
243
|
+
await configureAngularJson(config);
|
|
244
|
+
|
|
245
|
+
// Create ESLint configuration (ESLint 9+ flat config)
|
|
246
|
+
const eslintConfig = `// @ts-check
|
|
247
|
+
const eslint = require("@eslint/js");
|
|
248
|
+
const tseslint = require("typescript-eslint");
|
|
249
|
+
const angular = require("angular-eslint");
|
|
250
|
+
const eslintConfigPrettier = require("eslint-config-prettier");
|
|
251
|
+
|
|
252
|
+
module.exports = tseslint.config(
|
|
253
|
+
{
|
|
254
|
+
files: ["**/*.ts"],
|
|
255
|
+
extends: [
|
|
256
|
+
eslint.configs.recommended,
|
|
257
|
+
...tseslint.configs.recommended,
|
|
258
|
+
...tseslint.configs.stylistic,
|
|
259
|
+
...angular.configs.tsRecommended,
|
|
260
|
+
eslintConfigPrettier,
|
|
261
|
+
],
|
|
262
|
+
processor: angular.processInlineTemplates,
|
|
263
|
+
rules: {
|
|
264
|
+
"@angular-eslint/directive-selector": [
|
|
265
|
+
"error",
|
|
266
|
+
{
|
|
267
|
+
type: "attribute",
|
|
268
|
+
prefix: "app",
|
|
269
|
+
style: "camelCase",
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
"@angular-eslint/component-selector": [
|
|
273
|
+
"error",
|
|
274
|
+
{
|
|
275
|
+
type: "element",
|
|
276
|
+
prefix: "app",
|
|
277
|
+
style: "kebab-case",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
"@angular-eslint/component-class-suffix": [
|
|
281
|
+
"error",
|
|
282
|
+
{
|
|
283
|
+
"suffixes": ["Component", "App"]
|
|
284
|
+
}
|
|
285
|
+
],
|
|
286
|
+
"@typescript-eslint/no-unused-vars": [
|
|
287
|
+
"error",
|
|
288
|
+
{
|
|
289
|
+
"argsIgnorePattern": "^_",
|
|
290
|
+
"varsIgnorePattern": "^_"
|
|
291
|
+
}
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
files: ["**/*.html"],
|
|
297
|
+
extends: [
|
|
298
|
+
...angular.configs.templateRecommended,
|
|
299
|
+
...angular.configs.templateAccessibility,
|
|
300
|
+
],
|
|
301
|
+
rules: {},
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
`;
|
|
305
|
+
|
|
306
|
+
await fs.writeFile(path.join(config.fullPath, 'eslint.config.js'), eslintConfig);
|
|
307
|
+
|
|
308
|
+
// Create .prettierignore file
|
|
309
|
+
const prettierIgnore = `# Build outputs
|
|
310
|
+
dist/
|
|
311
|
+
coverage/
|
|
312
|
+
node_modules/
|
|
313
|
+
|
|
314
|
+
# Generated files
|
|
315
|
+
*.d.ts
|
|
316
|
+
`;
|
|
317
|
+
|
|
318
|
+
await fs.writeFile(path.join(config.fullPath, '.prettierignore'), prettierIgnore);
|
|
319
|
+
|
|
320
|
+
// Create .prettierrc.json file for better IDE support
|
|
321
|
+
await fs.writeFile(
|
|
322
|
+
path.join(config.fullPath, '.prettierrc.json'),
|
|
323
|
+
JSON.stringify(prettierConfig, null, 2)
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Install linting packages if not skipping install
|
|
327
|
+
if (!config.skipInstall) {
|
|
328
|
+
await installLintingPackages(config);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Format code using Prettier
|
|
334
|
+
*/
|
|
335
|
+
async function formatCode(config) {
|
|
336
|
+
const execa = require('execa');
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
// Run prettier to format all generated files (including config files)
|
|
340
|
+
await execa.command(
|
|
341
|
+
'npx prettier --write "**/*.{ts,html,css,json}" --ignore-path .gitignore --log-level silent',
|
|
342
|
+
{
|
|
343
|
+
cwd: config.fullPath,
|
|
344
|
+
stdio: 'pipe',
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
// If prettier fails, it's not critical - user can run it manually
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
createVSCodeSettings,
|
|
354
|
+
updateTsConfig,
|
|
355
|
+
configureAngularJson,
|
|
356
|
+
installLintingPackages,
|
|
357
|
+
setupLinting,
|
|
358
|
+
formatCode,
|
|
359
|
+
};
|