initkit 1.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.
- package/LICENSE +21 -0
- package/README.md +844 -0
- package/bin/index.js +8 -0
- package/package.json +79 -0
- package/src/cli.js +264 -0
- package/src/commands/create.js +264 -0
- package/src/index.js +9 -0
- package/src/prompts/questions.js +358 -0
- package/src/templates/express.js +915 -0
- package/src/templates/fullstack.js +1236 -0
- package/src/templates/nextjs.js +620 -0
- package/src/templates/react.js +586 -0
- package/src/templates/vue.js +545 -0
- package/src/utils/errorHandler.js +275 -0
- package/src/utils/git.js +69 -0
- package/src/utils/packageManager.js +90 -0
- package/src/utils/templateGenerator.js +365 -0
- package/src/utils/validation.js +186 -0
- package/src/utils/versionFetcher.js +128 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { generateNextjsTemplate } from '../templates/nextjs.js';
|
|
4
|
+
import { generateReactTemplate } from '../templates/react.js';
|
|
5
|
+
import { generateVueTemplate } from '../templates/vue.js';
|
|
6
|
+
import { generateExpressTemplate } from '../templates/express.js';
|
|
7
|
+
import { generateFullStackTemplate } from '../templates/fullstack.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate project files from templates based on user configuration
|
|
11
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
12
|
+
* @param {Object} config - User's project configuration object
|
|
13
|
+
* @param {string} config.projectType - Type of project ('frontend'|'backend'|'fullstack'|'library')
|
|
14
|
+
* @param {string} [config.frontend] - Frontend framework choice
|
|
15
|
+
* @param {string} [config.backend] - Backend framework choice
|
|
16
|
+
* @param {Array<string>} [config.features] - Additional features to enable
|
|
17
|
+
* @returns {Promise<void>}
|
|
18
|
+
* @throws {Error} If template generation fails
|
|
19
|
+
*/
|
|
20
|
+
async function generateTemplate(projectPath, config) {
|
|
21
|
+
// Generate .gitignore
|
|
22
|
+
await generateGitignore(projectPath, config);
|
|
23
|
+
|
|
24
|
+
// Generate project-specific files based on type
|
|
25
|
+
switch (config.projectType) {
|
|
26
|
+
case 'frontend':
|
|
27
|
+
await generateFrontendFiles(projectPath, config);
|
|
28
|
+
break;
|
|
29
|
+
case 'backend':
|
|
30
|
+
await generateBackendFiles(projectPath, config);
|
|
31
|
+
break;
|
|
32
|
+
case 'fullstack':
|
|
33
|
+
await generateFullStackFiles(projectPath, config);
|
|
34
|
+
break;
|
|
35
|
+
case 'library':
|
|
36
|
+
await generateLibraryFiles(projectPath, config);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Add additional features (Docker, GitHub Actions, etc.)
|
|
41
|
+
if (config.features) {
|
|
42
|
+
await addFeatures(projectPath, config);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate frontend project files based on selected framework
|
|
48
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
49
|
+
* @param {Object} config - User's project configuration
|
|
50
|
+
* @param {string} config.frontend - Frontend framework ('react'|'vue'|'nextjs'|'angular'|'svelte'|'nuxt')
|
|
51
|
+
* @param {string} config.language - Language choice ('typescript'|'javascript')
|
|
52
|
+
* @param {string} config.folderStructure - Folder structure preference
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
* @throws {Error} If framework template is not found or generation fails
|
|
55
|
+
*/
|
|
56
|
+
async function generateFrontendFiles(projectPath, config) {
|
|
57
|
+
const framework = config.frontend;
|
|
58
|
+
|
|
59
|
+
switch (framework) {
|
|
60
|
+
case 'nextjs':
|
|
61
|
+
await generateNextjsTemplate(projectPath, config);
|
|
62
|
+
break;
|
|
63
|
+
case 'react':
|
|
64
|
+
await generateReactTemplate(projectPath, config);
|
|
65
|
+
break;
|
|
66
|
+
case 'vue':
|
|
67
|
+
await generateVueTemplate(projectPath, config);
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Frontend framework "${framework}" is not yet implemented. Available: react, nextjs, vue`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generate backend project files based on selected framework
|
|
78
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
79
|
+
* @param {Object} config - User's project configuration
|
|
80
|
+
* @param {string} config.backend - Backend framework ('express'|'fastify'|'nestjs'|'koa'|'hapi')
|
|
81
|
+
* @param {string} [config.database] - Database choice ('mongodb'|'postgresql'|'mysql'|'sqlite')
|
|
82
|
+
* @param {string} config.language - Language choice ('typescript'|'javascript')
|
|
83
|
+
* @param {string} config.projectName - Name of the project
|
|
84
|
+
* @param {string} config.packageManager - Package manager to use
|
|
85
|
+
* @returns {Promise<void>}
|
|
86
|
+
* @throws {Error} If backend template generation fails
|
|
87
|
+
*/
|
|
88
|
+
async function generateBackendFiles(projectPath, config) {
|
|
89
|
+
const backend = config.backend;
|
|
90
|
+
|
|
91
|
+
switch (backend) {
|
|
92
|
+
case 'express':
|
|
93
|
+
await generateExpressTemplate(projectPath, config);
|
|
94
|
+
break;
|
|
95
|
+
default:
|
|
96
|
+
throw new Error(`Backend framework "${backend}" is not yet implemented. Available: express`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Generate full-stack project with both frontend and backend
|
|
102
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
103
|
+
* @param {Object} config - User's project configuration
|
|
104
|
+
* @param {string} [config.fullstackType] - Project structure type ('monorepo'|'traditional')
|
|
105
|
+
* @param {string} [config.stack] - Technology stack ('MERN'|'PERN'|'T3'|etc)
|
|
106
|
+
* @param {string} [config.frontend] - Frontend framework
|
|
107
|
+
* @param {string} [config.backend] - Backend framework
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
* @throws {Error} If full-stack template generation fails
|
|
110
|
+
*/
|
|
111
|
+
async function generateFullStackFiles(projectPath, config) {
|
|
112
|
+
// Use the dedicated full-stack template generator
|
|
113
|
+
await generateFullStackTemplate(projectPath, config);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate Node.js library/package structure
|
|
118
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
119
|
+
* @param {Object} config - User's project configuration
|
|
120
|
+
* @param {string} config.projectName - Name of the library
|
|
121
|
+
* @param {string} config.language - Language choice ('typescript'|'javascript')
|
|
122
|
+
* @param {string} config.packageManager - Package manager to use
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
* @throws {Error} If library template generation fails
|
|
125
|
+
*/
|
|
126
|
+
async function generateLibraryFiles(projectPath, config) {
|
|
127
|
+
const srcPath = path.join(projectPath, 'src');
|
|
128
|
+
await fs.ensureDir(srcPath);
|
|
129
|
+
|
|
130
|
+
const ext = config.language === 'typescript' ? 'ts' : 'js';
|
|
131
|
+
|
|
132
|
+
const indexContent = `/**
|
|
133
|
+
* Main entry point for ${config.projectName}
|
|
134
|
+
*/
|
|
135
|
+
|
|
136
|
+
export function hello() {
|
|
137
|
+
return 'Hello from ${config.projectName}!';
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
await fs.writeFile(path.join(srcPath, `index.${ext}`), indexContent);
|
|
142
|
+
|
|
143
|
+
// Package.json for library
|
|
144
|
+
const packageJson = {
|
|
145
|
+
name: config.projectName,
|
|
146
|
+
version: '1.0.0',
|
|
147
|
+
description: 'A reusable library',
|
|
148
|
+
main: `dist/index.js`,
|
|
149
|
+
types: config.language === 'typescript' ? 'dist/index.d.ts' : undefined,
|
|
150
|
+
files: ['dist'],
|
|
151
|
+
scripts: {
|
|
152
|
+
build: 'echo "Configure build script"',
|
|
153
|
+
test: 'echo "Configure test script"',
|
|
154
|
+
},
|
|
155
|
+
keywords: [],
|
|
156
|
+
author: '',
|
|
157
|
+
license: 'MIT',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
await fs.writeJSON(path.join(projectPath, 'package.json'), packageJson, { spaces: 2 });
|
|
161
|
+
|
|
162
|
+
const readme = `# ${config.projectName}
|
|
163
|
+
|
|
164
|
+
Library created with InitKit CLI
|
|
165
|
+
|
|
166
|
+
## Installation
|
|
167
|
+
|
|
168
|
+
\`\`\`bash
|
|
169
|
+
npm install ${config.projectName}
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
## Usage
|
|
173
|
+
|
|
174
|
+
\`\`\`javascript
|
|
175
|
+
import { hello } from '${config.projectName}';
|
|
176
|
+
|
|
177
|
+
console.log(hello());
|
|
178
|
+
\`\`\`
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
Built with InitKit
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
await fs.writeFile(path.join(projectPath, 'README.md'), readme);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate .gitignore file with framework-specific patterns
|
|
190
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
191
|
+
* @param {Object} config - User's project configuration
|
|
192
|
+
* @param {string} config.projectType - Type of project ('frontend'|'backend'|'fullstack'|'library')
|
|
193
|
+
* @param {string} [config.frontend] - Frontend framework name
|
|
194
|
+
* @param {string} [config.backend] - Backend framework name
|
|
195
|
+
* @param {boolean} [config.monorepo] - Whether project uses monorepo structure
|
|
196
|
+
* @returns {Promise<void>}
|
|
197
|
+
* @throws {Error} If file creation fails
|
|
198
|
+
*/
|
|
199
|
+
async function generateGitignore(projectPath, _config) {
|
|
200
|
+
const gitignore = `# Dependencies
|
|
201
|
+
node_modules/
|
|
202
|
+
.pnp
|
|
203
|
+
.pnp.js
|
|
204
|
+
|
|
205
|
+
# Testing
|
|
206
|
+
coverage/
|
|
207
|
+
|
|
208
|
+
# Production
|
|
209
|
+
build/
|
|
210
|
+
dist/
|
|
211
|
+
.next/
|
|
212
|
+
out/
|
|
213
|
+
|
|
214
|
+
# Misc
|
|
215
|
+
.DS_Store
|
|
216
|
+
*.pem
|
|
217
|
+
|
|
218
|
+
# Debug
|
|
219
|
+
npm-debug.log*
|
|
220
|
+
yarn-debug.log*
|
|
221
|
+
yarn-error.log*
|
|
222
|
+
.pnpm-debug.log*
|
|
223
|
+
|
|
224
|
+
# Local env files
|
|
225
|
+
.env
|
|
226
|
+
.env*.local
|
|
227
|
+
|
|
228
|
+
# Vercel
|
|
229
|
+
.vercel
|
|
230
|
+
|
|
231
|
+
# TypeScript
|
|
232
|
+
*.tsbuildinfo
|
|
233
|
+
next-env.d.ts
|
|
234
|
+
|
|
235
|
+
# IDE
|
|
236
|
+
.vscode/
|
|
237
|
+
.idea/
|
|
238
|
+
*.swp
|
|
239
|
+
*.swo
|
|
240
|
+
*~
|
|
241
|
+
`;
|
|
242
|
+
|
|
243
|
+
await fs.writeFile(path.join(projectPath, '.gitignore'), gitignore);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Add additional features to the project (Docker, CI/CD, Git hooks)
|
|
248
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
249
|
+
* @param {Object} config - User's project configuration
|
|
250
|
+
* @param {Array<string>} config.features - List of features to add (e.g., ['docker', 'github-actions', 'husky'])
|
|
251
|
+
* @returns {Promise<void>}
|
|
252
|
+
*/
|
|
253
|
+
async function addFeatures(projectPath, config) {
|
|
254
|
+
const features = config.features || [];
|
|
255
|
+
|
|
256
|
+
// Docker configuration
|
|
257
|
+
if (features.includes('docker')) {
|
|
258
|
+
await generateDockerFiles(projectPath, config);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// GitHub Actions
|
|
262
|
+
if (features.includes('github-actions')) {
|
|
263
|
+
await generateGitHubActions(projectPath, config);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Husky
|
|
267
|
+
if (features.includes('husky')) {
|
|
268
|
+
await generateHuskyFiles(projectPath, config);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Generate Docker configuration files (Dockerfile and .dockerignore)
|
|
274
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
275
|
+
* @param {Object} config - User's project configuration
|
|
276
|
+
* @returns {Promise<void>}
|
|
277
|
+
*/
|
|
278
|
+
async function generateDockerFiles(projectPath, _config) {
|
|
279
|
+
const dockerfile = `FROM node:18-alpine
|
|
280
|
+
|
|
281
|
+
WORKDIR /app
|
|
282
|
+
|
|
283
|
+
COPY package*.json ./
|
|
284
|
+
|
|
285
|
+
RUN npm ci --only=production
|
|
286
|
+
|
|
287
|
+
COPY . .
|
|
288
|
+
|
|
289
|
+
EXPOSE 3000
|
|
290
|
+
|
|
291
|
+
CMD ["npm", "start"]
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
await fs.writeFile(path.join(projectPath, 'Dockerfile'), dockerfile);
|
|
295
|
+
|
|
296
|
+
const dockerignore = `node_modules
|
|
297
|
+
.git
|
|
298
|
+
.env
|
|
299
|
+
npm-debug.log
|
|
300
|
+
`;
|
|
301
|
+
|
|
302
|
+
await fs.writeFile(path.join(projectPath, '.dockerignore'), dockerignore);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Generate GitHub Actions CI/CD workflow
|
|
307
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
308
|
+
* @param {Object} config - User's project configuration
|
|
309
|
+
* @returns {Promise<void>}
|
|
310
|
+
*/
|
|
311
|
+
async function generateGitHubActions(projectPath, _config) {
|
|
312
|
+
await fs.ensureDir(path.join(projectPath, '.github', 'workflows'));
|
|
313
|
+
|
|
314
|
+
const workflow = `name: CI
|
|
315
|
+
|
|
316
|
+
on:
|
|
317
|
+
push:
|
|
318
|
+
branches: [ main, develop ]
|
|
319
|
+
pull_request:
|
|
320
|
+
branches: [ main ]
|
|
321
|
+
|
|
322
|
+
jobs:
|
|
323
|
+
build:
|
|
324
|
+
runs-on: ubuntu-latest
|
|
325
|
+
|
|
326
|
+
steps:
|
|
327
|
+
- uses: actions/checkout@v3
|
|
328
|
+
|
|
329
|
+
- name: Use Node.js
|
|
330
|
+
uses: actions/setup-node@v3
|
|
331
|
+
with:
|
|
332
|
+
node-version: '18'
|
|
333
|
+
|
|
334
|
+
- name: Install dependencies
|
|
335
|
+
run: npm ci
|
|
336
|
+
|
|
337
|
+
- name: Run lint
|
|
338
|
+
run: npm run lint
|
|
339
|
+
|
|
340
|
+
- name: Run tests
|
|
341
|
+
run: npm test
|
|
342
|
+
`;
|
|
343
|
+
|
|
344
|
+
await fs.writeFile(path.join(projectPath, '.github', 'workflows', 'ci.yml'), workflow);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Generate Husky Git hooks for pre-commit linting
|
|
349
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
350
|
+
* @param {Object} config - User's project configuration
|
|
351
|
+
* @returns {Promise<void>}
|
|
352
|
+
*/
|
|
353
|
+
async function generateHuskyFiles(projectPath, _config) {
|
|
354
|
+
await fs.ensureDir(path.join(projectPath, '.husky'));
|
|
355
|
+
|
|
356
|
+
const preCommit = `#!/usr/bin/env sh
|
|
357
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
358
|
+
|
|
359
|
+
npm run lint
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
await fs.writeFile(path.join(projectPath, '.husky', 'pre-commit'), preCommit);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export { generateTemplate };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import validateNpmName from 'validate-npm-package-name';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate project name against npm package naming rules
|
|
8
|
+
* @param {string} name - Project name to validate
|
|
9
|
+
* @returns {Object} - { valid: boolean, errors: string[] }
|
|
10
|
+
*/
|
|
11
|
+
export function validateProjectName(name) {
|
|
12
|
+
const errors = [];
|
|
13
|
+
|
|
14
|
+
// Check if name is provided
|
|
15
|
+
if (!name || name.trim() === '') {
|
|
16
|
+
errors.push('Project name is required');
|
|
17
|
+
return { valid: false, errors };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Trim and convert to lowercase for validation
|
|
21
|
+
const trimmedName = name.trim();
|
|
22
|
+
|
|
23
|
+
// Check for spaces
|
|
24
|
+
if (trimmedName.includes(' ')) {
|
|
25
|
+
errors.push('Project name cannot contain spaces. Use hyphens instead.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check length
|
|
29
|
+
if (trimmedName.length > 214) {
|
|
30
|
+
errors.push('Project name must be 214 characters or less');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (trimmedName.length === 0) {
|
|
34
|
+
errors.push('Project name cannot be empty');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check if starts with . or _
|
|
38
|
+
if (trimmedName.startsWith('.') || trimmedName.startsWith('_')) {
|
|
39
|
+
errors.push('Project name cannot start with a dot or underscore');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check for uppercase letters
|
|
43
|
+
if (trimmedName !== trimmedName.toLowerCase()) {
|
|
44
|
+
errors.push('Project name must be lowercase');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check for invalid characters
|
|
48
|
+
const invalidChars = /[~'!()*]/;
|
|
49
|
+
if (invalidChars.test(trimmedName)) {
|
|
50
|
+
errors.push('Project name contains invalid characters: ~\'!()*');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Use npm's official validator
|
|
54
|
+
const npmValidation = validateNpmName(trimmedName);
|
|
55
|
+
|
|
56
|
+
if (!npmValidation.validForNewPackages) {
|
|
57
|
+
if (npmValidation.errors) {
|
|
58
|
+
errors.push(...npmValidation.errors);
|
|
59
|
+
}
|
|
60
|
+
if (npmValidation.warnings) {
|
|
61
|
+
errors.push(...npmValidation.warnings);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
valid: errors.length === 0,
|
|
67
|
+
errors: [...new Set(errors)], // Remove duplicates
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if directory already exists
|
|
73
|
+
* @param {string} projectName - Name of the project
|
|
74
|
+
* @param {string} basePath - Base path where project will be created (default: cwd)
|
|
75
|
+
* @returns {Object} - { exists: boolean, path: string }
|
|
76
|
+
*/
|
|
77
|
+
export function checkDirectoryExists(projectName, basePath = process.cwd()) {
|
|
78
|
+
const projectPath = path.join(basePath, projectName);
|
|
79
|
+
const exists = fs.existsSync(projectPath);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
exists,
|
|
83
|
+
path: projectPath,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate that the target directory is writable
|
|
89
|
+
* @param {string} dirPath - Directory path to check
|
|
90
|
+
* @returns {Object} - { writable: boolean, error: string }
|
|
91
|
+
*/
|
|
92
|
+
export async function checkDirectoryWritable(dirPath) {
|
|
93
|
+
try {
|
|
94
|
+
await fs.access(dirPath, fs.constants.W_OK);
|
|
95
|
+
return { writable: true, error: null };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
writable: false,
|
|
99
|
+
error: `Directory ${dirPath} is not writable`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate folder structure name
|
|
106
|
+
* @param {string} structure - Folder structure type
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
export function validateFolderStructure(structure) {
|
|
110
|
+
const validStructures = ['feature-based', 'type-based', 'domain-driven', 'flat'];
|
|
111
|
+
return validStructures.includes(structure);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format validation errors for display
|
|
116
|
+
* @param {string[]} errors - Array of error messages
|
|
117
|
+
* @returns {string} - Formatted error message
|
|
118
|
+
*/
|
|
119
|
+
export function formatValidationErrors(errors) {
|
|
120
|
+
if (errors.length === 0) return '';
|
|
121
|
+
|
|
122
|
+
if (errors.length === 1) {
|
|
123
|
+
return chalk.red(`Error: ${errors[0]}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const errorList = errors.map((err, idx) => ` ${idx + 1}. ${err}`).join('\n');
|
|
127
|
+
return chalk.red(`Invalid project name:\n${errorList}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Sanitize project name by fixing common issues
|
|
132
|
+
* @param {string} name - Project name to sanitize
|
|
133
|
+
* @returns {string} - Sanitized name
|
|
134
|
+
*/
|
|
135
|
+
export function sanitizeProjectName(name) {
|
|
136
|
+
return name
|
|
137
|
+
.trim()
|
|
138
|
+
.toLowerCase()
|
|
139
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
140
|
+
.replace(/[^a-z0-9-_]/g, '') // Remove invalid characters
|
|
141
|
+
.replace(/^[._]/, '') // Remove leading dots/underscores
|
|
142
|
+
.substring(0, 214); // Limit length
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Suggest an alternative project name if the current one is invalid
|
|
147
|
+
* @param {string} name - Original project name
|
|
148
|
+
* @returns {string} - Suggested name
|
|
149
|
+
*/
|
|
150
|
+
export function suggestProjectName(name) {
|
|
151
|
+
const sanitized = sanitizeProjectName(name);
|
|
152
|
+
if (sanitized.length === 0) {
|
|
153
|
+
return 'my-awesome-project';
|
|
154
|
+
}
|
|
155
|
+
return sanitized;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Validate multiple inputs at once
|
|
160
|
+
* @param {Object} inputs - Object with input values to validate
|
|
161
|
+
* @returns {Object} - { valid: boolean, errors: Object }
|
|
162
|
+
*/
|
|
163
|
+
export function validateAllInputs(inputs) {
|
|
164
|
+
const errors = {};
|
|
165
|
+
let valid = true;
|
|
166
|
+
|
|
167
|
+
// Validate project name
|
|
168
|
+
if (inputs.projectName) {
|
|
169
|
+
const nameValidation = validateProjectName(inputs.projectName);
|
|
170
|
+
if (!nameValidation.valid) {
|
|
171
|
+
errors.projectName = nameValidation.errors;
|
|
172
|
+
valid = false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check directory existence
|
|
177
|
+
if (inputs.projectName && inputs.checkDirectory !== false) {
|
|
178
|
+
const dirCheck = checkDirectoryExists(inputs.projectName);
|
|
179
|
+
if (dirCheck.exists) {
|
|
180
|
+
errors.directory = [`Directory "${inputs.projectName}" already exists`];
|
|
181
|
+
valid = false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { valid, errors };
|
|
186
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch the latest version of a package from npm registry
|
|
5
|
+
* @param {string} packageName - Name of the npm package
|
|
6
|
+
* @returns {Promise<string>} Latest version number
|
|
7
|
+
*/
|
|
8
|
+
export async function getLatestVersion(packageName) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const url = `https://registry.npmjs.org/${packageName}/latest`;
|
|
11
|
+
|
|
12
|
+
https
|
|
13
|
+
.get(url, (res) => {
|
|
14
|
+
let data = '';
|
|
15
|
+
|
|
16
|
+
res.on('data', (chunk) => {
|
|
17
|
+
data += chunk;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
res.on('end', () => {
|
|
21
|
+
try {
|
|
22
|
+
const json = JSON.parse(data);
|
|
23
|
+
resolve(json.version || 'latest');
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// If parsing fails, return 'latest' as fallback
|
|
26
|
+
resolve('latest');
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
})
|
|
30
|
+
.on('error', (error) => {
|
|
31
|
+
// On network error, return 'latest' as fallback
|
|
32
|
+
console.warn(`Warning: Could not fetch version for ${packageName}, using 'latest'`);
|
|
33
|
+
resolve('latest');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fetch latest versions for multiple packages
|
|
40
|
+
* @param {Object} packages - Object with package names as keys
|
|
41
|
+
* @returns {Promise<Object>} Object with package names as keys and latest versions as values
|
|
42
|
+
*/
|
|
43
|
+
export async function getLatestVersions(packages) {
|
|
44
|
+
const packageNames = Object.keys(packages);
|
|
45
|
+
const versions = {};
|
|
46
|
+
|
|
47
|
+
await Promise.all(
|
|
48
|
+
packageNames.map(async (name) => {
|
|
49
|
+
try {
|
|
50
|
+
const version = await getLatestVersion(name);
|
|
51
|
+
versions[name] = `^${version}`;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
versions[name] = 'latest';
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return versions;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get latest versions for common dependencies
|
|
63
|
+
* @returns {Promise<Object>} Object with latest versions
|
|
64
|
+
*/
|
|
65
|
+
export async function getLatestDependencies() {
|
|
66
|
+
const commonPackages = {
|
|
67
|
+
// React ecosystem
|
|
68
|
+
react: null,
|
|
69
|
+
'react-dom': null,
|
|
70
|
+
'@types/react': null,
|
|
71
|
+
'@types/react-dom': null,
|
|
72
|
+
'@vitejs/plugin-react': null,
|
|
73
|
+
vite: null,
|
|
74
|
+
typescript: null,
|
|
75
|
+
|
|
76
|
+
// Next.js
|
|
77
|
+
next: null,
|
|
78
|
+
'@types/node': null,
|
|
79
|
+
|
|
80
|
+
// Vue
|
|
81
|
+
vue: null,
|
|
82
|
+
'@vitejs/plugin-vue': null,
|
|
83
|
+
'vue-tsc': null,
|
|
84
|
+
'vue-router': null,
|
|
85
|
+
pinia: null,
|
|
86
|
+
|
|
87
|
+
// State Management
|
|
88
|
+
'@reduxjs/toolkit': null,
|
|
89
|
+
'react-redux': null,
|
|
90
|
+
zustand: null,
|
|
91
|
+
jotai: null,
|
|
92
|
+
|
|
93
|
+
// Routing
|
|
94
|
+
'react-router-dom': null,
|
|
95
|
+
|
|
96
|
+
// Data Fetching
|
|
97
|
+
'@tanstack/react-query': null,
|
|
98
|
+
'@tanstack/vue-query': null,
|
|
99
|
+
|
|
100
|
+
// Forms
|
|
101
|
+
'react-hook-form': null,
|
|
102
|
+
'@hookform/resolvers': null,
|
|
103
|
+
|
|
104
|
+
// Validation
|
|
105
|
+
zod: null,
|
|
106
|
+
|
|
107
|
+
// HTTP
|
|
108
|
+
axios: null,
|
|
109
|
+
|
|
110
|
+
// UI
|
|
111
|
+
'framer-motion': null,
|
|
112
|
+
'react-icons': null,
|
|
113
|
+
'@radix-ui/react-dialog': null,
|
|
114
|
+
'@radix-ui/react-dropdown-menu': null,
|
|
115
|
+
'@radix-ui/react-select': null,
|
|
116
|
+
'@radix-ui/react-slot': null,
|
|
117
|
+
|
|
118
|
+
// Styling
|
|
119
|
+
tailwindcss: null,
|
|
120
|
+
|
|
121
|
+
// Utils
|
|
122
|
+
'date-fns': null,
|
|
123
|
+
lodash: null,
|
|
124
|
+
'@vueuse/core': null,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return await getLatestVersions(commonPackages);
|
|
128
|
+
}
|