frontend-hamroun 1.2.28 → 1.2.29
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/bin/banner.js +0 -0
- package/bin/cli-utils.js +0 -0
- package/bin/cli.js +536 -598
- package/package.json +1 -1
package/bin/banner.js
ADDED
File without changes
|
package/bin/cli-utils.js
ADDED
File without changes
|
package/bin/cli.js
CHANGED
@@ -2,668 +2,606 @@
|
|
2
2
|
|
3
3
|
import { Command } from 'commander';
|
4
4
|
import inquirer from 'inquirer';
|
5
|
-
import fs from 'fs-extra';
|
6
|
-
import path from 'path';
|
7
|
-
import { fileURLToPath } from 'url';
|
8
5
|
import chalk from 'chalk';
|
9
6
|
import { createSpinner } from 'nanospinner';
|
7
|
+
import path from 'path';
|
8
|
+
import fs from 'fs-extra';
|
9
|
+
import { fileURLToPath } from 'url';
|
10
|
+
import { exec } from 'child_process';
|
11
|
+
import { promisify } from 'util';
|
10
12
|
|
13
|
+
// Convert to ESM-friendly __dirname equivalent
|
11
14
|
const __filename = fileURLToPath(import.meta.url);
|
12
15
|
const __dirname = path.dirname(__filename);
|
16
|
+
const execAsync = promisify(exec);
|
13
17
|
|
14
|
-
//
|
15
|
-
const
|
16
|
-
|
17
|
-
export function ${name}(props) {
|
18
|
-
// State hooks
|
19
|
-
const [state, setState] = useState(null);
|
20
|
-
|
21
|
-
// Effect hooks
|
22
|
-
useEffect(() => {
|
23
|
-
// Component mounted
|
24
|
-
return () => {
|
25
|
-
// Component will unmount
|
26
|
-
};
|
27
|
-
}, []);
|
28
|
-
|
29
|
-
return (
|
30
|
-
<div className="${name.toLowerCase()}">
|
31
|
-
<h2>${name} Component</h2>
|
32
|
-
{/* Your JSX here */}
|
33
|
-
</div>
|
34
|
-
);
|
35
|
-
}
|
36
|
-
`;
|
37
|
-
|
38
|
-
const CSS_TEMPLATE = (name) => `.${name.toLowerCase()} {
|
39
|
-
display: flex;
|
40
|
-
flex-direction: column;
|
41
|
-
padding: 1rem;
|
42
|
-
margin: 0.5rem;
|
43
|
-
border-radius: 4px;
|
44
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
45
|
-
}
|
46
|
-
`;
|
47
|
-
|
48
|
-
const TEST_TEMPLATE = (name) => `import { render, screen } from '@testing-library/frontend-hamroun';
|
49
|
-
import { ${name} } from './${name}';
|
50
|
-
|
51
|
-
describe('${name} Component', () => {
|
52
|
-
test('renders correctly', () => {
|
53
|
-
render(<${name} />);
|
54
|
-
const element = screen.getByText('${name} Component');
|
55
|
-
expect(element).toBeInTheDocument();
|
56
|
-
});
|
57
|
-
});
|
58
|
-
`;
|
59
|
-
|
60
|
-
// Dockerfile templates
|
61
|
-
const DOCKERFILE_TEMPLATE = `# Stage 1: Build the application
|
62
|
-
FROM node:18-alpine as build
|
63
|
-
|
64
|
-
# Set working directory
|
65
|
-
WORKDIR /app
|
66
|
-
|
67
|
-
# Copy package files
|
68
|
-
COPY package.json package-lock.json ./
|
69
|
-
|
70
|
-
# Install dependencies
|
71
|
-
RUN npm ci
|
72
|
-
|
73
|
-
# Copy source files
|
74
|
-
COPY . .
|
75
|
-
|
76
|
-
# Build the application
|
77
|
-
RUN npm run build
|
78
|
-
|
79
|
-
# Stage 2: Serve the application
|
80
|
-
FROM nginx:alpine
|
18
|
+
// CLI instance
|
19
|
+
const program = new Command();
|
81
20
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
# Start nginx
|
89
|
-
CMD ["nginx", "-g", "daemon off;"]
|
21
|
+
// Create ASCII art banner
|
22
|
+
const banner = `
|
23
|
+
${chalk.blue('╔══════════════════════════════════════════════╗')}
|
24
|
+
${chalk.blue('║')} ${chalk.bold.cyan('Frontend Hamroun')} ${chalk.blue('║')}
|
25
|
+
${chalk.blue('║')} ${chalk.yellow('A lightweight full-stack JavaScript framework')} ${chalk.blue('║')}
|
26
|
+
${chalk.blue('╚══════════════════════════════════════════════╝')}
|
90
27
|
`;
|
91
28
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
# Copy package files
|
99
|
-
COPY package.json package-lock.json ./
|
100
|
-
|
101
|
-
# Install dependencies
|
102
|
-
RUN npm ci
|
103
|
-
|
104
|
-
# Copy source files
|
105
|
-
COPY . .
|
106
|
-
|
107
|
-
# Build the application
|
108
|
-
RUN npm run build
|
109
|
-
|
110
|
-
# Stage 2: Run the server
|
111
|
-
FROM node:18-alpine
|
29
|
+
// Version and description
|
30
|
+
program
|
31
|
+
.name('frontend-hamroun')
|
32
|
+
.description('CLI for Frontend Hamroun - A lightweight full-stack JavaScript framework')
|
33
|
+
.version(JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version);
|
112
34
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
# Copy build artifacts
|
120
|
-
COPY --from=build /app/dist ./dist
|
121
|
-
COPY --from=build /app/server ./server
|
122
|
-
|
123
|
-
# Expose port 3000
|
124
|
-
EXPOSE 3000
|
125
|
-
|
126
|
-
# Start the server
|
127
|
-
CMD ["node", "server/index.js"]
|
128
|
-
`;
|
35
|
+
// Helper for creating visually consistent sections
|
36
|
+
const createSection = (title) => {
|
37
|
+
console.log('\n' + chalk.bold.cyan(`◆ ${title}`));
|
38
|
+
console.log(chalk.cyan('─'.repeat(50)) + '\n');
|
39
|
+
};
|
129
40
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
.
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
.
|
141
|
-
|
142
|
-
.argument('[name]', 'Project name')
|
143
|
-
.action(async (name) => {
|
144
|
-
const projectName = name || await askProjectName();
|
145
|
-
await createProject(projectName);
|
146
|
-
});
|
147
|
-
|
148
|
-
// Generate component
|
149
|
-
program
|
150
|
-
.command('generate')
|
151
|
-
.alias('g')
|
152
|
-
.description('Generate a new component')
|
153
|
-
.argument('<name>', 'Component name')
|
154
|
-
.option('-d, --directory <directory>', 'Target directory', './src/components')
|
155
|
-
.action(async (name, options) => {
|
156
|
-
await generateComponent(name, options.directory);
|
157
|
-
});
|
158
|
-
|
159
|
-
// Add Dockerfile
|
160
|
-
program
|
161
|
-
.command('docker')
|
162
|
-
.description('Add Dockerfile to project')
|
163
|
-
.option('-s, --ssr', 'Use SSR-compatible Dockerfile')
|
164
|
-
.action(async (options) => {
|
165
|
-
await addDockerfile(options.ssr);
|
166
|
-
});
|
167
|
-
|
168
|
-
// Add generate API route command
|
169
|
-
program
|
170
|
-
.command('api')
|
171
|
-
.description('Generate a new API route')
|
172
|
-
.argument('<name>', 'API route name (e.g., users or users/[id])')
|
173
|
-
.option('-d, --directory <directory>', 'Target directory', './api')
|
174
|
-
.action(async (name, options) => {
|
175
|
-
await generateApiRoute(name, options.directory);
|
176
|
-
});
|
177
|
-
|
178
|
-
// Interactive mode if no command provided
|
179
|
-
if (process.argv.length <= 2) {
|
180
|
-
await interactiveMode();
|
181
|
-
} else {
|
182
|
-
program.parse();
|
41
|
+
// Helper for checking dependencies
|
42
|
+
async function checkDependencies() {
|
43
|
+
const spinner = createSpinner('Checking dependencies...').start();
|
44
|
+
|
45
|
+
try {
|
46
|
+
await execAsync('npm --version');
|
47
|
+
spinner.success({ text: 'Dependencies verified' });
|
48
|
+
return true;
|
49
|
+
} catch (error) {
|
50
|
+
spinner.error({ text: 'Missing required dependencies' });
|
51
|
+
console.log(chalk.red('Error: Node.js and npm are required to use this tool.'));
|
52
|
+
return false;
|
183
53
|
}
|
184
54
|
}
|
185
55
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
]
|
197
|
-
}]);
|
198
|
-
|
199
|
-
if (action === 'create') {
|
200
|
-
const projectName = await askProjectName();
|
201
|
-
await createProject(projectName);
|
202
|
-
} else if (action === 'generate') {
|
203
|
-
const { name } = await inquirer.prompt([{
|
204
|
-
type: 'input',
|
205
|
-
name: 'name',
|
206
|
-
message: 'Component name:',
|
207
|
-
validate: (input) => input ? true : 'Component name is required'
|
208
|
-
}]);
|
209
|
-
|
210
|
-
const { directory } = await inquirer.prompt([{
|
211
|
-
type: 'input',
|
212
|
-
name: 'directory',
|
213
|
-
message: 'Target directory:',
|
214
|
-
default: './src/components'
|
215
|
-
}]);
|
216
|
-
|
217
|
-
await generateComponent(name, directory);
|
218
|
-
} else if (action === 'api') {
|
219
|
-
const { name } = await inquirer.prompt([{
|
220
|
-
type: 'input',
|
221
|
-
name: 'name',
|
222
|
-
message: 'API route name:',
|
223
|
-
validate: (input) => input ? true : 'API route name is required'
|
224
|
-
}]);
|
225
|
-
|
226
|
-
const { directory } = await inquirer.prompt([{
|
227
|
-
type: 'input',
|
228
|
-
name: 'directory',
|
229
|
-
message: 'Target directory:',
|
230
|
-
default: './api'
|
231
|
-
}]);
|
56
|
+
// Choose template interactively
|
57
|
+
async function chooseTemplate() {
|
58
|
+
const templatesPath = path.join(__dirname, '..', 'templates');
|
59
|
+
const templates = fs.readdirSync(templatesPath).filter(file =>
|
60
|
+
fs.statSync(path.join(templatesPath, file)).isDirectory()
|
61
|
+
);
|
62
|
+
|
63
|
+
// Create template descriptions
|
64
|
+
const templateOptions = templates.map(template => {
|
65
|
+
let description = '';
|
232
66
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
67
|
+
switch (template) {
|
68
|
+
case 'basic-app':
|
69
|
+
description = 'Simple client-side application with minimal setup';
|
70
|
+
break;
|
71
|
+
case 'ssr-template':
|
72
|
+
description = 'Server-side rendering with hydration support';
|
73
|
+
break;
|
74
|
+
case 'fullstack-app':
|
75
|
+
description = 'Complete fullstack application with API routes';
|
76
|
+
break;
|
77
|
+
default:
|
78
|
+
description = 'Application template';
|
79
|
+
}
|
241
80
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
name: 'template',
|
260
|
-
message: 'Select project type:',
|
261
|
-
choices: [
|
262
|
-
{ name: 'Client Side App', value: 'basic-app' },
|
263
|
-
{ name: 'Server Side Rendered App', value: 'ssr-template' },
|
264
|
-
{ name: 'Full Stack App', value: 'fullstack-app' }
|
265
|
-
]
|
266
|
-
}]);
|
267
|
-
return template;
|
81
|
+
return {
|
82
|
+
name: `${template} - ${chalk.dim(description)}`,
|
83
|
+
value: template
|
84
|
+
};
|
85
|
+
});
|
86
|
+
|
87
|
+
const answers = await inquirer.prompt([
|
88
|
+
{
|
89
|
+
type: 'list',
|
90
|
+
name: 'template',
|
91
|
+
message: 'Select a project template:',
|
92
|
+
choices: templateOptions,
|
93
|
+
loop: false
|
94
|
+
}
|
95
|
+
]);
|
96
|
+
|
97
|
+
return answers.template;
|
268
98
|
}
|
269
99
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
pkg.name = projectName;
|
288
|
-
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
289
|
-
|
290
|
-
// Automatically add Dockerfile
|
291
|
-
const isSSR = template === 'ssr-template';
|
292
|
-
const dockerContent = isSSR ? SSR_DOCKERFILE_TEMPLATE : DOCKERFILE_TEMPLATE;
|
293
|
-
await fs.writeFile(path.join(targetDir, 'Dockerfile'), dockerContent);
|
294
|
-
|
295
|
-
spinner.success({ text: `Project ${chalk.green(projectName)} created successfully with Dockerfile!` });
|
296
|
-
|
297
|
-
// Show next steps
|
298
|
-
console.log('\nNext steps:');
|
299
|
-
console.log(chalk.cyan(` cd ${projectName}`));
|
300
|
-
console.log(chalk.cyan(' npm install'));
|
301
|
-
console.log(chalk.cyan(' npm run dev'));
|
302
|
-
console.log(chalk.yellow('\nTo build Docker image:'));
|
303
|
-
console.log(chalk.cyan(' docker build -t my-app .'));
|
304
|
-
console.log(chalk.cyan(' docker run -p 3000:' + (isSSR ? '3000' : '80') + ' my-app'));
|
305
|
-
|
306
|
-
} catch (error) {
|
307
|
-
spinner.error({ text: 'Failed to create project' });
|
308
|
-
console.error(chalk.red(error));
|
309
|
-
process.exit(1);
|
100
|
+
// Create a new project
|
101
|
+
async function createProject(projectName, options) {
|
102
|
+
console.log(banner);
|
103
|
+
createSection('Project Setup');
|
104
|
+
|
105
|
+
// Validate project name
|
106
|
+
if (!projectName) {
|
107
|
+
const answers = await inquirer.prompt([
|
108
|
+
{
|
109
|
+
type: 'input',
|
110
|
+
name: 'projectName',
|
111
|
+
message: 'What is your project name?',
|
112
|
+
default: 'my-frontend-app',
|
113
|
+
validate: input => /^[a-z0-9-_]+$/.test(input) ? true : 'Project name can only contain lowercase letters, numbers, hyphens and underscores'
|
114
|
+
}
|
115
|
+
]);
|
116
|
+
projectName = answers.projectName;
|
310
117
|
}
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
118
|
+
|
119
|
+
// Check if dependencies are installed
|
120
|
+
if (!await checkDependencies()) {
|
121
|
+
return;
|
122
|
+
}
|
123
|
+
|
124
|
+
// Choose template if not specified
|
125
|
+
let template = options.template;
|
126
|
+
if (!template) {
|
127
|
+
template = await chooseTemplate();
|
128
|
+
}
|
129
|
+
|
130
|
+
// Create project directory
|
131
|
+
const targetDir = path.resolve(projectName);
|
132
|
+
const templateDir = path.join(__dirname, '..', 'templates', template);
|
133
|
+
|
134
|
+
if (fs.existsSync(targetDir)) {
|
135
|
+
const answers = await inquirer.prompt([
|
136
|
+
{
|
137
|
+
type: 'confirm',
|
138
|
+
name: 'overwrite',
|
139
|
+
message: `Directory ${projectName} already exists. Continue and overwrite existing files?`,
|
140
|
+
default: false
|
141
|
+
}
|
142
|
+
]);
|
318
143
|
|
319
|
-
|
144
|
+
if (!answers.overwrite) {
|
145
|
+
console.log(chalk.yellow('✖ Operation cancelled'));
|
146
|
+
return;
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
// Copy template
|
151
|
+
const spinner = createSpinner('Creating project...').start();
|
152
|
+
try {
|
320
153
|
await fs.ensureDir(targetDir);
|
154
|
+
await fs.copy(templateDir, targetDir, { overwrite: true });
|
321
155
|
|
322
|
-
// Create
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
path.join(targetDir, `${name}.css`),
|
330
|
-
CSS_TEMPLATE(name)
|
331
|
-
);
|
332
|
-
|
333
|
-
await fs.writeFile(
|
334
|
-
path.join(targetDir, `${name}.test.jsx`),
|
335
|
-
TEST_TEMPLATE(name)
|
336
|
-
);
|
337
|
-
|
338
|
-
await fs.writeFile(
|
339
|
-
path.join(targetDir, 'index.js'),
|
340
|
-
`export { ${name} } from './${name}';\n`
|
341
|
-
);
|
342
|
-
|
343
|
-
spinner.success({ text: `Component ${chalk.green(name)} generated successfully!` });
|
156
|
+
// Create package.json with project name
|
157
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
158
|
+
if (fs.existsSync(pkgJsonPath)) {
|
159
|
+
const pkgJson = await fs.readJson(pkgJsonPath);
|
160
|
+
pkgJson.name = projectName;
|
161
|
+
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
162
|
+
}
|
344
163
|
|
345
|
-
|
346
|
-
console.log(chalk.cyan(` ${path.join(directory, name, `${name}.jsx`)}`));
|
347
|
-
console.log(chalk.cyan(` ${path.join(directory, name, `${name}.css`)}`));
|
348
|
-
console.log(chalk.cyan(` ${path.join(directory, name, `${name}.test.jsx`)}`));
|
349
|
-
console.log(chalk.cyan(` ${path.join(directory, name, 'index.js')}`));
|
164
|
+
spinner.success({ text: `Project created successfully at ${chalk.green(targetDir)}` });
|
350
165
|
|
166
|
+
// Show next steps
|
167
|
+
createSection('Next Steps');
|
168
|
+
console.log(`${chalk.bold.green('✓')} Run the following commands to get started:\n`);
|
169
|
+
console.log(` ${chalk.cyan('cd')} ${projectName}`);
|
170
|
+
console.log(` ${chalk.cyan('npm install')}`);
|
171
|
+
console.log(` ${chalk.cyan('npm run dev')}\n`);
|
351
172
|
} catch (error) {
|
352
|
-
spinner.error({ text: 'Failed to
|
353
|
-
console.error(chalk.red(error)
|
354
|
-
process.exit(1);
|
173
|
+
spinner.error({ text: 'Failed to create project' });
|
174
|
+
console.error(chalk.red('Error: ') + error.message);
|
355
175
|
}
|
356
176
|
}
|
357
177
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
const
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
await fs.writeFile(targetPath, template);
|
375
|
-
|
376
|
-
// Check if server.ts file exists, if not create it
|
377
|
-
const serverFilePath = path.join(process.cwd(), 'server.ts');
|
378
|
-
if (!await fs.pathExists(serverFilePath)) {
|
379
|
-
await fs.writeFile(serverFilePath, SERVER_TEMPLATE);
|
380
|
-
console.log(chalk.green('\nCreated server.ts file with Express setup'));
|
381
|
-
|
382
|
-
// Create tsconfig.server.json if it doesn't exist
|
383
|
-
const tsconfigPath = path.join(process.cwd(), 'tsconfig.server.json');
|
384
|
-
if (!await fs.pathExists(tsconfigPath)) {
|
385
|
-
await fs.writeFile(tsconfigPath, TSCONFIG_SERVER_TEMPLATE);
|
386
|
-
console.log(chalk.green('Created tsconfig.server.json for server-side TypeScript'));
|
178
|
+
// Add component to existing project
|
179
|
+
async function addComponent(componentName, options) {
|
180
|
+
console.log(banner);
|
181
|
+
createSection('Create Component');
|
182
|
+
|
183
|
+
// Validate component name
|
184
|
+
if (!componentName) {
|
185
|
+
const answers = await inquirer.prompt([
|
186
|
+
{
|
187
|
+
type: 'input',
|
188
|
+
name: 'componentName',
|
189
|
+
message: 'What is your component name?',
|
190
|
+
validate: input => /^[A-Z][A-Za-z0-9]*$/.test(input)
|
191
|
+
? true
|
192
|
+
: 'Component name must start with uppercase letter and only contain alphanumeric characters'
|
387
193
|
}
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
}
|
404
|
-
|
405
|
-
const devDeps = {
|
406
|
-
"@types/express": "^4.17.17",
|
407
|
-
"@types/cors": "^2.8.13",
|
408
|
-
"@types/mongodb": "^4.0.7",
|
409
|
-
"@types/jsonwebtoken": "^9.0.3",
|
410
|
-
"@types/bcryptjs": "^2.4.4"
|
411
|
-
};
|
412
|
-
|
413
|
-
// Add dependencies if needed
|
414
|
-
pkg.dependencies = pkg.dependencies || {};
|
415
|
-
for (const [dep, version] of Object.entries(serverDeps)) {
|
416
|
-
if (!pkg.dependencies[dep]) {
|
417
|
-
pkg.dependencies[dep] = version;
|
418
|
-
needsUpdate = true;
|
419
|
-
}
|
420
|
-
}
|
421
|
-
|
422
|
-
// Add dev dependencies if needed
|
423
|
-
pkg.devDependencies = pkg.devDependencies || {};
|
424
|
-
for (const [dep, version] of Object.entries(devDeps)) {
|
425
|
-
if (!pkg.devDependencies[dep]) {
|
426
|
-
pkg.devDependencies[dep] = version;
|
427
|
-
needsUpdate = true;
|
428
|
-
}
|
429
|
-
}
|
430
|
-
|
431
|
-
// Add start script if it doesn't exist
|
432
|
-
pkg.scripts = pkg.scripts || {};
|
433
|
-
if (!pkg.scripts.start) {
|
434
|
-
pkg.scripts.start = "node server.js";
|
435
|
-
needsUpdate = true;
|
436
|
-
}
|
437
|
-
|
438
|
-
// Save changes if needed
|
439
|
-
if (needsUpdate) {
|
440
|
-
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
441
|
-
console.log(chalk.green('Updated package.json with server dependencies'));
|
442
|
-
}
|
443
|
-
}
|
444
|
-
} catch (error) {
|
445
|
-
console.warn(chalk.yellow('Could not update package.json:', error.message));
|
194
|
+
]);
|
195
|
+
componentName = answers.componentName;
|
196
|
+
}
|
197
|
+
|
198
|
+
// Determine file extension preference
|
199
|
+
let extension = options.typescript ? '.tsx' : options.jsx ? '.jsx' : null;
|
200
|
+
|
201
|
+
if (!extension) {
|
202
|
+
const answers = await inquirer.prompt([
|
203
|
+
{
|
204
|
+
type: 'list',
|
205
|
+
name: 'extension',
|
206
|
+
message: 'Select file type:',
|
207
|
+
choices: [
|
208
|
+
{ name: 'TypeScript (.tsx)', value: '.tsx' },
|
209
|
+
{ name: 'JavaScript (.jsx)', value: '.jsx' }
|
210
|
+
]
|
446
211
|
}
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
212
|
+
]);
|
213
|
+
extension = answers.extension;
|
214
|
+
}
|
215
|
+
|
216
|
+
// Determine component directory
|
217
|
+
let componentPath = options.path || 'src/components';
|
218
|
+
if (!options.path) {
|
219
|
+
const answers = await inquirer.prompt([
|
220
|
+
{
|
221
|
+
type: 'input',
|
222
|
+
name: 'path',
|
223
|
+
message: 'Where do you want to create this component?',
|
224
|
+
default: 'src/components',
|
225
|
+
validate: input => /^[a-zA-Z0-9-_/\\]+$/.test(input) ? true : 'Path can only contain letters, numbers, slashes, hyphens and underscores'
|
226
|
+
}
|
227
|
+
]);
|
228
|
+
componentPath = answers.path;
|
458
229
|
}
|
230
|
+
|
231
|
+
// Create component content based on type
|
232
|
+
const fullPath = path.join(process.cwd(), componentPath, `${componentName}${extension}`);
|
233
|
+
const dirPath = path.dirname(fullPath);
|
234
|
+
|
235
|
+
// Sample template
|
236
|
+
const componentTemplate = extension === '.tsx'
|
237
|
+
? `import { jsx } from 'frontend-hamroun';
|
238
|
+
|
239
|
+
interface ${componentName}Props {
|
240
|
+
title?: string;
|
241
|
+
children?: any;
|
459
242
|
}
|
460
243
|
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
244
|
+
export default function ${componentName}({ title, children }: ${componentName}Props) {
|
245
|
+
return (
|
246
|
+
<div className="component">
|
247
|
+
{title && <h2>{title}</h2>}
|
248
|
+
<div className="content">
|
249
|
+
{children}
|
250
|
+
</div>
|
251
|
+
</div>
|
252
|
+
);
|
253
|
+
}
|
254
|
+
`
|
255
|
+
: `import { jsx } from 'frontend-hamroun';
|
465
256
|
|
466
|
-
|
257
|
+
export default function ${componentName}({ title, children }) {
|
258
|
+
return (
|
259
|
+
<div className="component">
|
260
|
+
{title && <h2>{title}</h2>}
|
261
|
+
<div className="content">
|
262
|
+
{children}
|
263
|
+
</div>
|
264
|
+
</div>
|
265
|
+
);
|
266
|
+
}
|
267
|
+
`;
|
268
|
+
|
269
|
+
// Create the file
|
270
|
+
const spinner = createSpinner(`Creating ${componentName} component...`).start();
|
467
271
|
try {
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
// Create and configure the server
|
472
|
-
const app = new Server({
|
473
|
-
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
|
474
|
-
apiDir: './api',
|
475
|
-
pagesDir: './src/pages', // For SSR pages
|
476
|
-
staticDir: './public',
|
477
|
-
|
478
|
-
// Enable CORS
|
479
|
-
enableCors: true,
|
480
|
-
corsOptions: {
|
481
|
-
origin: process.env.CORS_ORIGIN || '*',
|
482
|
-
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
483
|
-
allowedHeaders: ['Content-Type', 'Authorization']
|
484
|
-
}
|
485
|
-
});
|
486
|
-
|
487
|
-
// Connect to database if configured
|
488
|
-
if (app.getDatabase()) {
|
489
|
-
await app.getDatabase().connect();
|
490
|
-
console.log('Connected to database');
|
491
|
-
}
|
492
|
-
|
493
|
-
// Start the server
|
494
|
-
await app.start();
|
495
|
-
|
496
|
-
console.log('Server running at http://localhost:' +
|
497
|
-
(process.env.PORT || 3000));
|
498
|
-
|
499
|
-
// Handle graceful shutdown
|
500
|
-
process.on('SIGINT', async () => {
|
501
|
-
console.log('Shutting down server...');
|
502
|
-
if (app.getDatabase()) {
|
503
|
-
await app.getDatabase().disconnect();
|
504
|
-
console.log('Database connection closed');
|
505
|
-
}
|
506
|
-
await app.stop();
|
507
|
-
console.log('Server stopped');
|
508
|
-
process.exit(0);
|
509
|
-
});
|
272
|
+
await fs.ensureDir(dirPath);
|
273
|
+
await fs.writeFile(fullPath, componentTemplate);
|
274
|
+
spinner.success({ text: `Component created at ${chalk.green(fullPath)}` });
|
510
275
|
} catch (error) {
|
511
|
-
|
512
|
-
|
276
|
+
spinner.error({ text: 'Failed to create component' });
|
277
|
+
console.error(chalk.red('Error: ') + error.message);
|
513
278
|
}
|
514
279
|
}
|
515
280
|
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
const spinner = createSpinner('Adding Dockerfile...').start();
|
281
|
+
// Create a page component
|
282
|
+
async function addPage(pageName, options) {
|
283
|
+
console.log(banner);
|
284
|
+
createSection('Create Page');
|
521
285
|
|
522
|
-
|
523
|
-
|
524
|
-
const
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
message: 'Dockerfile already exists. Overwrite?',
|
533
|
-
default: false
|
534
|
-
}]);
|
535
|
-
|
536
|
-
if (!overwrite) {
|
537
|
-
console.log(chalk.yellow('Operation cancelled.'));
|
538
|
-
return;
|
286
|
+
// Validate page name
|
287
|
+
if (!pageName) {
|
288
|
+
const answers = await inquirer.prompt([
|
289
|
+
{
|
290
|
+
type: 'input',
|
291
|
+
name: 'pageName',
|
292
|
+
message: 'What is your page name?',
|
293
|
+
validate: input => /^[a-zA-Z0-9-_]+$/.test(input)
|
294
|
+
? true
|
295
|
+
: 'Page name can only contain alphanumeric characters, hyphens and underscores'
|
539
296
|
}
|
540
|
-
|
541
|
-
|
542
|
-
}
|
543
|
-
|
544
|
-
// Write Dockerfile
|
545
|
-
await fs.writeFile(targetPath, dockerContent);
|
546
|
-
|
547
|
-
spinner.success({ text: 'Dockerfile added successfully!' });
|
548
|
-
|
549
|
-
console.log('\nTo build and run Docker image:');
|
550
|
-
console.log(chalk.cyan(' docker build -t my-app .'));
|
551
|
-
console.log(chalk.cyan(' docker run -p 3000:' + (isSSR ? '3000' : '80') + ' my-app'));
|
552
|
-
|
553
|
-
} catch (error) {
|
554
|
-
spinner.error({ text: 'Failed to add Dockerfile' });
|
555
|
-
console.error(chalk.red(error));
|
556
|
-
process.exit(1);
|
297
|
+
]);
|
298
|
+
pageName = answers.pageName;
|
557
299
|
}
|
558
|
-
|
300
|
+
|
301
|
+
// Format page name to be PascalCase for the component name
|
302
|
+
const pageComponentName = pageName
|
303
|
+
.split('-')
|
304
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
305
|
+
.join('') + 'Page';
|
306
|
+
|
307
|
+
// Determine file extension preference
|
308
|
+
let extension = options.typescript ? '.tsx' : options.jsx ? '.jsx' : null;
|
309
|
+
|
310
|
+
if (!extension) {
|
311
|
+
const answers = await inquirer.prompt([
|
312
|
+
{
|
313
|
+
type: 'list',
|
314
|
+
name: 'extension',
|
315
|
+
message: 'Select file type:',
|
316
|
+
choices: [
|
317
|
+
{ name: 'TypeScript (.tsx)', value: '.tsx' },
|
318
|
+
{ name: 'JavaScript (.jsx)', value: '.jsx' }
|
319
|
+
]
|
320
|
+
}
|
321
|
+
]);
|
322
|
+
extension = answers.extension;
|
323
|
+
}
|
324
|
+
|
325
|
+
// Create page file path
|
326
|
+
const pagePath = options.path || 'src/pages';
|
327
|
+
|
328
|
+
// For index page, use the directory itself
|
329
|
+
const fileName = pageName === 'index' ? 'index' : pageName;
|
330
|
+
const fullPath = path.join(process.cwd(), pagePath, `${fileName}${extension}`);
|
331
|
+
const dirPath = path.dirname(fullPath);
|
332
|
+
|
333
|
+
// Page template
|
334
|
+
const pageTemplate = extension === '.tsx'
|
335
|
+
? `import { jsx } from 'frontend-hamroun';
|
336
|
+
import Layout from '../components/Layout';
|
559
337
|
|
560
|
-
|
561
|
-
|
338
|
+
interface ${pageComponentName}Props {
|
339
|
+
initialState?: any;
|
340
|
+
}
|
562
341
|
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
342
|
+
const ${pageComponentName}: React.FC<${pageComponentName}Props> = ({ initialState }) => {
|
343
|
+
return (
|
344
|
+
<Layout title="${pageName.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}">
|
345
|
+
<div className="page">
|
346
|
+
<h1>${pageName.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}</h1>
|
347
|
+
<p>Welcome to this page!</p>
|
348
|
+
</div>
|
349
|
+
</Layout>
|
350
|
+
);
|
569
351
|
};
|
570
352
|
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
}
|
353
|
+
// Optional: Add data fetching for SSR support
|
354
|
+
${pageComponentName}.getInitialData = async () => {
|
355
|
+
// You can fetch data for server-side rendering here
|
356
|
+
return {
|
357
|
+
// Your data here
|
358
|
+
};
|
577
359
|
};
|
578
360
|
|
579
|
-
export
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
361
|
+
export default ${pageComponentName};
|
362
|
+
`
|
363
|
+
: `import { jsx } from 'frontend-hamroun';
|
364
|
+
import Layout from '../components/Layout';
|
365
|
+
|
366
|
+
const ${pageComponentName} = ({ initialState }) => {
|
367
|
+
return (
|
368
|
+
<Layout title="${pageName.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}">
|
369
|
+
<div className="page">
|
370
|
+
<h1>${pageName.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}</h1>
|
371
|
+
<p>Welcome to this page!</p>
|
372
|
+
</div>
|
373
|
+
</Layout>
|
374
|
+
);
|
585
375
|
};
|
586
376
|
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
377
|
+
// Optional: Add data fetching for SSR support
|
378
|
+
${pageComponentName}.getInitialData = async () => {
|
379
|
+
// You can fetch data for server-side rendering here
|
380
|
+
return {
|
381
|
+
// Your data here
|
382
|
+
};
|
592
383
|
};
|
593
384
|
|
594
|
-
|
595
|
-
export const middleware = [
|
596
|
-
// Example middleware
|
597
|
-
(req: Request, res: Response, next: Function) => {
|
598
|
-
console.log(\`\${req.method} \${req.url} - \${new Date().toISOString()}\`);
|
599
|
-
next();
|
600
|
-
}
|
601
|
-
];
|
385
|
+
export default ${pageComponentName};
|
602
386
|
`;
|
603
387
|
|
604
|
-
|
388
|
+
// Create the file
|
389
|
+
const spinner = createSpinner(`Creating ${pageName} page...`).start();
|
390
|
+
try {
|
391
|
+
await fs.ensureDir(dirPath);
|
392
|
+
await fs.writeFile(fullPath, pageTemplate);
|
393
|
+
spinner.success({ text: `Page created at ${chalk.green(fullPath)}` });
|
394
|
+
} catch (error) {
|
395
|
+
spinner.error({ text: 'Failed to create page' });
|
396
|
+
console.error(chalk.red('Error: ') + error.message);
|
397
|
+
}
|
398
|
+
}
|
605
399
|
|
606
|
-
|
400
|
+
// Add API route
|
401
|
+
async function addApiRoute(routeName, options) {
|
402
|
+
console.log(banner);
|
403
|
+
createSection('Create API Route');
|
404
|
+
|
405
|
+
// Validate route name
|
406
|
+
if (!routeName) {
|
407
|
+
const answers = await inquirer.prompt([
|
408
|
+
{
|
409
|
+
type: 'input',
|
410
|
+
name: 'routeName',
|
411
|
+
message: 'What is your API route name?',
|
412
|
+
validate: input => /^[a-zA-Z0-9-_/[\]]+$/.test(input)
|
413
|
+
? true
|
414
|
+
: 'Route name can only contain alphanumeric characters, hyphens, underscores, slashes, and square brackets for dynamic parameters ([id])'
|
415
|
+
}
|
416
|
+
]);
|
417
|
+
routeName = answers.routeName;
|
418
|
+
}
|
419
|
+
|
420
|
+
// Determine file extension preference
|
421
|
+
let extension = options.typescript ? '.ts' : '.js';
|
422
|
+
|
423
|
+
if (!options.typescript && !options.javascript) {
|
424
|
+
const answers = await inquirer.prompt([
|
425
|
+
{
|
426
|
+
type: 'list',
|
427
|
+
name: 'extension',
|
428
|
+
message: 'Select file type:',
|
429
|
+
choices: [
|
430
|
+
{ name: 'TypeScript (.ts)', value: '.ts' },
|
431
|
+
{ name: 'JavaScript (.js)', value: '.js' }
|
432
|
+
]
|
433
|
+
}
|
434
|
+
]);
|
435
|
+
extension = answers.extension;
|
436
|
+
}
|
437
|
+
|
438
|
+
// Choose HTTP methods to implement
|
439
|
+
let methods = options.methods ? options.methods.split(',') : null;
|
440
|
+
|
441
|
+
if (!methods) {
|
442
|
+
const answers = await inquirer.prompt([
|
443
|
+
{
|
444
|
+
type: 'checkbox',
|
445
|
+
name: 'methods',
|
446
|
+
message: 'Select HTTP methods to implement:',
|
447
|
+
choices: [
|
448
|
+
{ name: 'GET', value: 'get', checked: true },
|
449
|
+
{ name: 'POST', value: 'post' },
|
450
|
+
{ name: 'PUT', value: 'put' },
|
451
|
+
{ name: 'DELETE', value: 'delete' },
|
452
|
+
{ name: 'PATCH', value: 'patch' }
|
453
|
+
],
|
454
|
+
validate: input => input.length > 0 ? true : 'Please select at least one method'
|
455
|
+
}
|
456
|
+
]);
|
457
|
+
methods = answers.methods;
|
458
|
+
}
|
459
|
+
|
460
|
+
// Create API route path
|
461
|
+
const routePath = options.path || 'api';
|
462
|
+
|
463
|
+
// Determine filename from route name
|
464
|
+
// If last segment includes parameters, handle specially
|
465
|
+
const segments = routeName.split('/').filter(Boolean);
|
466
|
+
let directory = '';
|
467
|
+
let filename = 'index';
|
468
|
+
|
469
|
+
if (segments.length > 0) {
|
470
|
+
if (segments.length === 1) {
|
471
|
+
filename = segments[0];
|
472
|
+
} else {
|
473
|
+
directory = segments.slice(0, -1).join('/');
|
474
|
+
filename = segments[segments.length - 1];
|
475
|
+
}
|
476
|
+
}
|
477
|
+
|
478
|
+
const fullPath = path.join(process.cwd(), routePath, directory, `${filename}${extension}`);
|
479
|
+
const dirPath = path.dirname(fullPath);
|
480
|
+
|
481
|
+
// API route template
|
482
|
+
let apiTemplate = extension === '.ts'
|
483
|
+
? `import { Request, Response } from 'express';\n\n`
|
484
|
+
: '';
|
485
|
+
|
486
|
+
// Add method handlers
|
487
|
+
methods.forEach(method => {
|
488
|
+
if (method === 'delete' && extension === '.js') {
|
489
|
+
// In JS, "delete" is a reserved keyword, so we name it "delete_"
|
490
|
+
apiTemplate += `export const delete_ = (req, res) => {
|
607
491
|
res.json({
|
608
|
-
message: '
|
609
|
-
params: req.params,
|
610
|
-
query: req.query,
|
492
|
+
message: 'DELETE ${routeName} endpoint',
|
611
493
|
timestamp: new Date().toISOString()
|
612
494
|
});
|
613
|
-
}
|
614
|
-
|
615
|
-
|
495
|
+
};\n\n`;
|
496
|
+
} else {
|
497
|
+
apiTemplate += extension === '.ts'
|
498
|
+
? `export const ${method} = (req: Request, res: Response) => {
|
616
499
|
res.json({
|
617
|
-
message: '
|
618
|
-
params: req.params,
|
619
|
-
body: req.body,
|
500
|
+
message: '${method.toUpperCase()} ${routeName} endpoint',
|
620
501
|
timestamp: new Date().toISOString()
|
621
502
|
});
|
622
|
-
}
|
623
|
-
|
624
|
-
export const put = (req: Request, res: Response) => {
|
503
|
+
};\n\n`
|
504
|
+
: `export const ${method} = (req, res) => {
|
625
505
|
res.json({
|
626
|
-
message: '
|
627
|
-
params: req.params,
|
628
|
-
body: req.body,
|
506
|
+
message: '${method.toUpperCase()} ${routeName} endpoint',
|
629
507
|
timestamp: new Date().toISOString()
|
630
508
|
});
|
631
|
-
}
|
632
|
-
|
633
|
-
export const delete_ = (req: Request, res: Response) => {
|
634
|
-
res.json({
|
635
|
-
message: 'This is a dynamic route DELETE endpoint',
|
636
|
-
params: req.params,
|
637
|
-
timestamp: new Date().toISOString()
|
509
|
+
};\n\n`;
|
510
|
+
}
|
638
511
|
});
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
512
|
+
|
513
|
+
// Create the file
|
514
|
+
const spinner = createSpinner(`Creating ${routeName} API route...`).start();
|
515
|
+
try {
|
516
|
+
await fs.ensureDir(dirPath);
|
517
|
+
await fs.writeFile(fullPath, apiTemplate);
|
518
|
+
spinner.success({ text: `API route created at ${chalk.green(fullPath)}` });
|
519
|
+
|
520
|
+
createSection('API Route Information');
|
521
|
+
console.log(`${chalk.bold.green('✓')} Your API route is accessible at:`);
|
522
|
+
console.log(` ${chalk.cyan(`/api/${routeName}`)}\n`);
|
523
|
+
|
524
|
+
// For dynamic routes, provide example
|
525
|
+
if (routeName.includes('[') && routeName.includes(']')) {
|
526
|
+
const exampleRoute = routeName.replace(/\[([^\]]+)\]/g, 'value');
|
527
|
+
console.log(`${chalk.bold.yellow('ℹ')} For dynamic parameters, access using:`);
|
528
|
+
console.log(` ${chalk.cyan(`/api/${exampleRoute}`)}\n`);
|
529
|
+
console.log(`${chalk.bold.yellow('ℹ')} Access parameters in your handler with:`);
|
530
|
+
console.log(` ${chalk.cyan('req.params.paramName')}\n`);
|
531
|
+
}
|
532
|
+
} catch (error) {
|
533
|
+
spinner.error({ text: 'Failed to create API route' });
|
534
|
+
console.error(chalk.red('Error: ') + error.message);
|
647
535
|
}
|
648
|
-
|
649
|
-
|
536
|
+
}
|
537
|
+
|
538
|
+
// Display dev tools and tips
|
539
|
+
function showDevTools() {
|
540
|
+
console.log(banner);
|
541
|
+
createSection('Development Tools');
|
542
|
+
|
543
|
+
console.log(chalk.bold('Available Commands:') + '\n');
|
544
|
+
console.log(`${chalk.green('•')} ${chalk.bold('npm run dev')} - Start development server`);
|
545
|
+
console.log(`${chalk.green('•')} ${chalk.bold('npm run build')} - Create production build`);
|
546
|
+
console.log(`${chalk.green('•')} ${chalk.bold('npm start')} - Run production server\n`);
|
547
|
+
|
548
|
+
console.log(chalk.bold('Useful Tips:') + '\n');
|
549
|
+
console.log(`${chalk.blue('1.')} Use ${chalk.cyan('frontend-hamroun create')} to scaffold a new project`);
|
550
|
+
console.log(`${chalk.blue('2.')} Add components with ${chalk.cyan('frontend-hamroun add:component')}`);
|
551
|
+
console.log(`${chalk.blue('3.')} Create pages with ${chalk.cyan('frontend-hamroun add:page')}`);
|
552
|
+
console.log(`${chalk.blue('4.')} Add API routes with ${chalk.cyan('frontend-hamroun add:api')}\n`);
|
553
|
+
|
554
|
+
console.log(chalk.bold('Project Structure:') + '\n');
|
555
|
+
console.log(`${chalk.yellow('src/')}`);
|
556
|
+
console.log(` ${chalk.yellow('├── components/')} - Reusable UI components`);
|
557
|
+
console.log(` ${chalk.yellow('├── pages/')} - File-based route components`);
|
558
|
+
console.log(` ${chalk.yellow('├── styles/')} - CSS and styling files`);
|
559
|
+
console.log(` ${chalk.yellow('└── utils/')} - Helper functions and utilities`);
|
560
|
+
console.log(`${chalk.yellow('api/')} - API route handlers`);
|
561
|
+
console.log(`${chalk.yellow('public/')} - Static assets\n`);
|
562
|
+
}
|
563
|
+
|
564
|
+
// Register commands
|
565
|
+
program
|
566
|
+
.command('create [name]')
|
567
|
+
.description('Create a new Frontend Hamroun project')
|
568
|
+
.option('-t, --template <template>', 'Specify template (basic-app, ssr-template, fullstack-app)')
|
569
|
+
.action(createProject);
|
570
|
+
|
571
|
+
program
|
572
|
+
.command('add:component [name]')
|
573
|
+
.description('Create a new component')
|
574
|
+
.option('-p, --path <path>', 'Path where the component should be created')
|
575
|
+
.option('-ts, --typescript', 'Use TypeScript')
|
576
|
+
.option('-jsx, --jsx', 'Use JSX')
|
577
|
+
.action(addComponent);
|
578
|
+
|
579
|
+
program
|
580
|
+
.command('add:page [name]')
|
581
|
+
.description('Create a new page')
|
582
|
+
.option('-p, --path <path>', 'Path where the page should be created')
|
583
|
+
.option('-ts, --typescript', 'Use TypeScript')
|
584
|
+
.option('-jsx, --jsx', 'Use JSX')
|
585
|
+
.action(addPage);
|
586
|
+
|
587
|
+
program
|
588
|
+
.command('add:api [name]')
|
589
|
+
.description('Create a new API route')
|
590
|
+
.option('-p, --path <path>', 'Path where the API route should be created')
|
591
|
+
.option('-ts, --typescript', 'Use TypeScript')
|
592
|
+
.option('-js, --javascript', 'Use JavaScript')
|
593
|
+
.option('-m, --methods <methods>', 'HTTP methods to implement (comma-separated: get,post,put,delete,patch)')
|
594
|
+
.action(addApiRoute);
|
595
|
+
|
596
|
+
program
|
597
|
+
.command('dev:tools')
|
598
|
+
.description('Show development tools and tips')
|
599
|
+
.action(showDevTools);
|
600
|
+
|
601
|
+
// Default command when no arguments
|
602
|
+
if (process.argv.length <= 2) {
|
603
|
+
console.log(banner);
|
604
|
+
program.help();
|
605
|
+
}
|
650
606
|
|
651
|
-
|
652
|
-
const TSCONFIG_SERVER_TEMPLATE = `{
|
653
|
-
"compilerOptions": {
|
654
|
-
"target": "ES2020",
|
655
|
-
"module": "NodeNext",
|
656
|
-
"moduleResolution": "NodeNext",
|
657
|
-
"esModuleInterop": true,
|
658
|
-
"outDir": "./dist",
|
659
|
-
"declaration": true,
|
660
|
-
"sourceMap": true,
|
661
|
-
"noEmit": false,
|
662
|
-
"strict": true,
|
663
|
-
"skipLibCheck": true
|
664
|
-
},
|
665
|
-
"include": ["server.ts", "api/**/*.ts"],
|
666
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
667
|
-
}`;
|
668
|
-
|
669
|
-
init().catch(console.error);
|
607
|
+
program.parse();
|