frontend-hamroun 1.2.35 → 1.2.41

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.
Files changed (2) hide show
  1. package/bin/cli.js +402 -568
  2. package/package.json +2 -8
package/bin/cli.js CHANGED
@@ -3,71 +3,28 @@
3
3
  import { Command } from 'commander';
4
4
  import inquirer from 'inquirer';
5
5
  import chalk from 'chalk';
6
- import gradient from 'gradient-string';
7
6
  import { createSpinner } from 'nanospinner';
8
7
  import path from 'path';
9
8
  import fs from 'fs-extra';
10
9
  import { fileURLToPath } from 'url';
11
10
  import { exec } from 'child_process';
12
11
  import { promisify } from 'util';
13
- import boxen from 'boxen';
14
- import terminalLink from 'terminal-link';
15
- import updateNotifier from 'update-notifier';
16
- import ora from 'ora';
17
- import figlet from 'figlet';
18
12
 
19
13
  // Convert to ESM-friendly __dirname equivalent
20
14
  const __filename = fileURLToPath(import.meta.url);
21
15
  const __dirname = path.dirname(__filename);
22
16
  const execAsync = promisify(exec);
23
17
 
24
- // Check for package updates
25
- try {
26
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
27
- const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 24 });
28
-
29
- if (notifier.update) {
30
- const updateMessage = boxen(
31
- `Update available: ${chalk.dim(notifier.update.current)} → ${chalk.green(notifier.update.latest)}\n` +
32
- `Run ${chalk.cyan('npm i -g frontend-hamroun')} to update`,
33
- {
34
- padding: 1,
35
- margin: 1,
36
- borderStyle: 'round',
37
- borderColor: 'cyan'
38
- }
39
- );
40
- console.log(updateMessage);
41
- }
42
- } catch (error) {
43
- // Silently continue if update check fails
44
- }
45
-
46
18
  // CLI instance
47
19
  const program = new Command();
48
20
 
49
- // Create beautiful ASCII art banner with gradient colors
50
- const displayBanner = () => {
51
- const titleText = figlet.textSync('Frontend Hamroun', {
52
- font: 'Standard',
53
- horizontalLayout: 'default',
54
- verticalLayout: 'default'
55
- });
56
-
57
- console.log('\n' + gradient.pastel.multiline(titleText));
58
-
59
- console.log(boxen(
60
- `${chalk.bold('A lightweight full-stack JavaScript framework')}\n\n` +
61
- `${chalk.dim('Version:')} ${chalk.cyan(JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version)}\n` +
62
- `${chalk.dim('Documentation:')} ${terminalLink('frontendhamroun.io', 'https://github.com/hamroun/frontend-hamroun')}`,
63
- {
64
- padding: 1,
65
- margin: { top: 1, bottom: 1 },
66
- borderStyle: 'round',
67
- borderColor: 'cyan'
68
- }
69
- ));
70
- };
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('╚══════════════════════════════════════════════╝')}
27
+ `;
71
28
 
72
29
  // Version and description
73
30
  program
@@ -78,320 +35,149 @@ program
78
35
  // Helper for creating visually consistent sections
79
36
  const createSection = (title) => {
80
37
  console.log('\n' + chalk.bold.cyan(`◆ ${title}`));
81
- console.log(chalk.cyan('─'.repeat(60)) + '\n');
38
+ console.log(chalk.cyan('─'.repeat(50)) + '\n');
82
39
  };
83
40
 
84
- // Helper for checking dependencies with improved feedback
41
+ // Helper for checking dependencies
85
42
  async function checkDependencies() {
86
- const spinner = ora({
87
- text: 'Checking environment...',
88
- spinner: 'dots',
89
- color: 'cyan'
90
- }).start();
43
+ const spinner = createSpinner('Checking dependencies...').start();
91
44
 
92
45
  try {
93
- // Check Node version
94
- const nodeVersionOutput = await execAsync('node --version');
95
- const nodeVersion = nodeVersionOutput.stdout.trim().replace('v', '');
96
- const requiredNodeVersion = '14.0.0';
97
-
98
- if (compareVersions(nodeVersion, requiredNodeVersion) < 0) {
99
- spinner.fail(`Node.js ${requiredNodeVersion}+ required, but found ${nodeVersion}`);
100
- console.log(chalk.yellow(`Please upgrade Node.js: ${terminalLink('https://nodejs.org', 'https://nodejs.org')}`));
101
- return false;
102
- }
103
-
104
- // Check npm version
105
- const npmVersionOutput = await execAsync('npm --version');
106
- const npmVersion = npmVersionOutput.stdout.trim();
107
- const requiredNpmVersion = '6.0.0';
108
-
109
- if (compareVersions(npmVersion, requiredNpmVersion) < 0) {
110
- spinner.fail(`npm ${requiredNpmVersion}+ required, but found ${npmVersion}`);
111
- console.log(chalk.yellow(`Please upgrade npm: ${chalk.cyan('npm install -g npm')}`));
112
- return false;
113
- }
114
-
115
- spinner.succeed(`Environment ready: Node ${chalk.green(nodeVersion)}, npm ${chalk.green(npmVersion)}`);
46
+ await execAsync('npm --version');
47
+ spinner.success({ text: 'Dependencies verified' });
116
48
  return true;
117
49
  } catch (error) {
118
- spinner.fail('Environment check failed');
50
+ spinner.error({ text: 'Missing required dependencies' });
119
51
  console.log(chalk.red('Error: Node.js and npm are required to use this tool.'));
120
52
  return false;
121
53
  }
122
54
  }
123
55
 
124
- // Compare versions helper
125
- function compareVersions(a, b) {
126
- const aParts = a.split('.').map(Number);
127
- const bParts = b.split('.').map(Number);
128
-
129
- for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
130
- const aVal = aParts[i] || 0;
131
- const bVal = bParts[i] || 0;
132
-
133
- if (aVal > bVal) return 1;
134
- if (aVal < bVal) return -1;
135
- }
136
-
137
- return 0;
138
- }
139
-
140
- // Choose template interactively with improved visualization
56
+ // Choose template interactively
141
57
  async function chooseTemplate() {
142
58
  const templatesPath = path.join(__dirname, '..', 'templates');
143
- const templates = fs.readdirSync(templatesPath).filter(file =>
59
+ const templates = fs.readdirSync(templatesPath).filter(file =>
144
60
  fs.statSync(path.join(templatesPath, file)).isDirectory()
145
61
  );
146
62
 
147
- // Emoji indicators for templates
148
- const templateIcons = {
149
- 'basic-app': '🚀',
150
- 'ssr-template': '🌐',
151
- 'fullstack-app': '⚡',
152
- };
153
-
154
- // Detailed descriptions
155
- const templateDescriptions = {
156
- 'basic-app': 'Single-page application with just the essentials. Perfect for learning the framework or building simple apps.',
157
- 'ssr-template': 'Server-side rendered application with hydration. Optimized for SEO and fast initial load.',
158
- 'fullstack-app': 'Complete solution with API routes, authentication, and database integration ready to go.'
159
- };
160
-
161
- console.log(boxen(
162
- `${chalk.bold('Available Project Templates')}\n\n` +
163
- templates.map(template => {
164
- const icon = templateIcons[template] || '📦';
165
- const shortDesc = {
166
- 'basic-app': 'Simple client-side application',
167
- 'ssr-template': 'Server-side rendering with hydration',
168
- 'fullstack-app': 'Complete fullstack application with API'
169
- }[template] || 'Application template';
170
-
171
- return `${icon} ${chalk.cyan(template)}\n ${chalk.dim(shortDesc)}`;
172
- }).join('\n\n'),
173
- {
174
- padding: 1,
175
- margin: 1,
176
- borderStyle: 'round',
177
- borderColor: 'cyan'
63
+ // Create template descriptions
64
+ const templateOptions = templates.map(template => {
65
+ let description = '';
66
+
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';
178
79
  }
179
- ));
180
-
181
- const templateChoices = templates.map(template => ({
182
- name: `${templateIcons[template] || '📦'} ${chalk.bold(template)}`,
183
- value: template,
184
- description: templateDescriptions[template] || 'An application template'
185
- }));
80
+
81
+ return {
82
+ name: `${template} - ${chalk.dim(description)}`,
83
+ value: template
84
+ };
85
+ });
186
86
 
187
87
  const answers = await inquirer.prompt([
188
88
  {
189
89
  type: 'list',
190
90
  name: 'template',
191
- message: chalk.green('Select a project template:'),
192
- choices: templateChoices,
193
- loop: false,
194
- pageSize: 10
195
- },
196
- {
197
- type: 'confirm',
198
- name: 'viewDetails',
199
- message: 'Would you like to see template details before proceeding?',
200
- default: false
91
+ message: 'Select a project template:',
92
+ choices: templateOptions,
93
+ loop: false
201
94
  }
202
95
  ]);
203
96
 
204
- if (answers.viewDetails) {
205
- const detailedDescription = {
206
- 'basic-app': [
207
- `${chalk.bold('Basic App Template')} ${templateIcons['basic-app']}`,
208
- '',
209
- `${chalk.dim('A lightweight client-side application template.')}`,
210
- '',
211
- `${chalk.yellow('Features:')}`,
212
- '• No build step required for development',
213
- '• Built-in state management with hooks',
214
- '• Component-based architecture',
215
- '• Tailwind CSS integration',
216
- '',
217
- `${chalk.yellow('Best for:')}`,
218
- '• Simple web applications',
219
- '• Learning the framework',
220
- '• Quick prototyping',
221
- ],
222
- 'ssr-template': [
223
- `${chalk.bold('SSR Template')} ${templateIcons['ssr-template']}`,
224
- '',
225
- `${chalk.dim('Server-side rendering with client hydration.')}`,
226
- '',
227
- `${chalk.yellow('Features:')}`,
228
- '• React-like development experience',
229
- '• SEO-friendly rendered HTML',
230
- '• Fast initial page load',
231
- '• Smooth client-side transitions',
232
- '• Built-in dynamic meta tag generation',
233
- '',
234
- `${chalk.yellow('Best for:')}`,
235
- '• Production websites needing SEO',
236
- '• Content-focused applications',
237
- '• Sites requiring social sharing previews'
238
- ],
239
- 'fullstack-app': [
240
- `${chalk.bold('Fullstack App Template')} ${templateIcons['fullstack-app']}`,
241
- '',
242
- `${chalk.dim('Complete solution with frontend and backend.')}`,
243
- '',
244
- `${chalk.yellow('Features:')}`,
245
- '• API routes with Express integration',
246
- '• File-based routing system',
247
- '• Authentication system with JWT',
248
- '• Database connectors (MongoDB, MySQL, PostgreSQL)',
249
- '• Server-side rendering with hydration',
250
- '• WebSocket support',
251
- '',
252
- `${chalk.yellow('Best for:')}`,
253
- '• Full production applications',
254
- '• Apps needing authentication',
255
- '• Projects requiring database integration'
256
- ]
257
- }[answers.template] || ['No detailed information available for this template'];
258
-
259
- console.log(boxen(detailedDescription.join('\n'), {
260
- padding: 1,
261
- margin: 1,
262
- title: answers.template,
263
- titleAlignment: 'center',
264
- borderStyle: 'round',
265
- borderColor: 'yellow'
266
- }));
267
-
268
- const proceed = await inquirer.prompt([{
269
- type: 'confirm',
270
- name: 'continue',
271
- message: 'Continue with this template?',
272
- default: true
273
- }]);
274
-
275
- if (!proceed.continue) {
276
- return chooseTemplate();
277
- }
278
- }
279
-
280
97
  return answers.template;
281
98
  }
282
99
 
283
- // Create a new project with enhanced visuals and status updates
100
+ // Create a new project
284
101
  async function createProject(projectName, options) {
285
- displayBanner();
102
+ console.log(banner);
286
103
  createSection('Project Setup');
287
-
104
+
105
+ // Validate project name
288
106
  if (!projectName) {
289
107
  const answers = await inquirer.prompt([
290
108
  {
291
109
  type: 'input',
292
110
  name: 'projectName',
293
- message: chalk.green('What is your project name?'),
111
+ message: 'What is your project name?',
294
112
  default: 'my-frontend-app',
295
- validate: input =>
296
- /^[a-z0-9-_]+$/.test(input)
297
- ? true
298
- : 'Project name can only contain lowercase letters, numbers, hyphens, and underscores'
113
+ validate: input => /^[a-z0-9-_]+$/.test(input) ? true : 'Project name can only contain lowercase letters, numbers, hyphens and underscores'
299
114
  }
300
115
  ]);
301
116
  projectName = answers.projectName;
302
117
  }
303
-
304
- if (!await checkDependencies()) return;
305
-
306
- let template = options.template || await chooseTemplate();
307
-
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
308
131
  const targetDir = path.resolve(projectName);
309
132
  const templateDir = path.join(__dirname, '..', 'templates', template);
310
-
133
+
311
134
  if (fs.existsSync(targetDir)) {
312
135
  const answers = await inquirer.prompt([
313
136
  {
314
137
  type: 'confirm',
315
138
  name: 'overwrite',
316
- message: chalk.yellow(`Directory ${projectName} already exists. Overwrite?`),
139
+ message: `Directory ${projectName} already exists. Continue and overwrite existing files?`,
317
140
  default: false
318
141
  }
319
142
  ]);
143
+
320
144
  if (!answers.overwrite) {
321
- console.log(chalk.red('✖ Operation cancelled'));
145
+ console.log(chalk.yellow('✖ Operation cancelled'));
322
146
  return;
323
147
  }
324
148
  }
325
-
326
- // Multi-step execution with progress reporting
327
- console.log(chalk.dim('\nCreating your new project...'));
328
149
 
329
- // Step 1: Create directory
330
- const step1 = ora({text: 'Creating project directory', color: 'cyan'}).start();
150
+ // Copy template
151
+ const spinner = createSpinner('Creating project...').start();
331
152
  try {
332
153
  await fs.ensureDir(targetDir);
333
- step1.succeed();
334
- } catch (error) {
335
- step1.fail();
336
- console.error(chalk.red(`Error creating directory: ${error.message}`));
337
- return;
338
- }
339
-
340
- // Step 2: Copy template files
341
- const step2 = ora({text: 'Copying template files', color: 'cyan'}).start();
342
- try {
343
154
  await fs.copy(templateDir, targetDir, { overwrite: true });
344
- step2.succeed();
345
- } catch (error) {
346
- step2.fail();
347
- console.error(chalk.red(`Error copying files: ${error.message}`));
348
- return;
349
- }
350
-
351
- // Step 3: Update package.json
352
- const step3 = ora({text: 'Configuring package.json', color: 'cyan'}).start();
353
- try {
155
+
156
+ // Create package.json with project name
354
157
  const pkgJsonPath = path.join(targetDir, 'package.json');
355
158
  if (fs.existsSync(pkgJsonPath)) {
356
159
  const pkgJson = await fs.readJson(pkgJsonPath);
357
160
  pkgJson.name = projectName;
358
161
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
359
- step3.succeed();
360
- } else {
361
- step3.warn('No package.json found in template');
362
162
  }
163
+
164
+ spinner.success({ text: `Project created successfully at ${chalk.green(targetDir)}` });
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`);
363
172
  } catch (error) {
364
- step3.fail();
365
- console.error(chalk.red(`Error updating package.json: ${error.message}`));
366
- return;
173
+ spinner.error({ text: 'Failed to create project' });
174
+ console.error(chalk.red('Error: ') + error.message);
367
175
  }
368
-
369
- // Final success message with helpful instructions
370
- console.log('\n' + boxen(
371
- `${chalk.bold.green('Project created successfully!')}\n\n` +
372
- `${chalk.bold('Project:')} ${chalk.cyan(projectName)}\n` +
373
- `${chalk.bold('Template:')} ${chalk.cyan(template)}\n` +
374
- `${chalk.bold('Location:')} ${chalk.cyan(targetDir)}\n\n` +
375
- `${chalk.bold.yellow('Next steps:')}\n\n` +
376
- ` ${chalk.dim('1.')} ${chalk.cyan(`cd ${projectName}`)}\n` +
377
- ` ${chalk.dim('2.')} ${chalk.cyan('npm install')}\n` +
378
- ` ${chalk.dim('3.')} ${chalk.cyan('npm run dev')}\n\n` +
379
- `${chalk.dim('For help and documentation:')} ${terminalLink('frontendhamroun.io', 'https://github.com/hamroun/frontend-hamroun')}`,
380
- {
381
- padding: 1,
382
- margin: 1,
383
- borderStyle: 'round',
384
- borderColor: 'green'
385
- }
386
- ));
387
-
388
- // Suggest next command
389
- console.log(`\n${chalk.cyan('Tip:')} Run ${chalk.bold.green(`cd ${projectName} && npm install`)} to get started right away.\n`);
390
176
  }
391
177
 
392
- // Add component with improved interactive experience
178
+ // Add component to existing project
393
179
  async function addComponent(componentName, options) {
394
- displayBanner();
180
+ console.log(banner);
395
181
  createSection('Create Component');
396
182
 
397
183
  // Validate component name
@@ -400,7 +186,7 @@ async function addComponent(componentName, options) {
400
186
  {
401
187
  type: 'input',
402
188
  name: 'componentName',
403
- message: chalk.green('What is your component name?'),
189
+ message: 'What is your component name?',
404
190
  validate: input => /^[A-Z][A-Za-z0-9]*$/.test(input)
405
191
  ? true
406
192
  : 'Component name must start with uppercase letter and only contain alphanumeric characters'
@@ -417,309 +203,365 @@ async function addComponent(componentName, options) {
417
203
  {
418
204
  type: 'list',
419
205
  name: 'extension',
420
- message: chalk.green('Select file type:'),
206
+ message: 'Select file type:',
421
207
  choices: [
422
- {
423
- name: 'TypeScript (.tsx)',
424
- value: '.tsx',
425
- description: 'TypeScript with JSX support'
426
- },
427
- {
428
- name: 'JavaScript (.jsx)',
429
- value: '.jsx',
430
- description: 'JavaScript with JSX support'
431
- }
208
+ { name: 'TypeScript (.tsx)', value: '.tsx' },
209
+ { name: 'JavaScript (.jsx)', value: '.jsx' }
432
210
  ]
433
211
  }
434
212
  ]);
435
213
  extension = answers.extension;
436
214
  }
437
215
 
438
- // Determine component directory with auto-detection
439
- let componentPath = options.path;
440
-
441
- if (!componentPath) {
442
- // Try to auto-detect common component directories
443
- const potentialPaths = ['src/components', 'components', 'src/app/components', 'app/components'];
444
- const existingPaths = potentialPaths.filter(p => fs.existsSync(path.join(process.cwd(), p)));
445
-
446
- if (existingPaths.length > 0) {
447
- const answers = await inquirer.prompt([
448
- {
449
- type: 'list',
450
- name: 'path',
451
- message: chalk.green('Where do you want to create this component?'),
452
- choices: [
453
- ...existingPaths.map(p => ({ name: `${p} ${chalk.dim('(detected)')}`, value: p })),
454
- { name: 'Custom location...', value: 'custom' }
455
- ]
456
- }
457
- ]);
458
-
459
- if (answers.path === 'custom') {
460
- const customPath = await inquirer.prompt([
461
- {
462
- type: 'input',
463
- name: 'customPath',
464
- message: chalk.green('Enter the component path:'),
465
- default: 'src/components',
466
- validate: input => /^[a-zA-Z0-9-_/\\]+$/.test(input)
467
- ? true
468
- : 'Path can only contain letters, numbers, slashes, hyphens and underscores'
469
- }
470
- ]);
471
- componentPath = customPath.customPath;
472
- } else {
473
- componentPath = answers.path;
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'
474
226
  }
475
- } else {
476
- const answers = await inquirer.prompt([
477
- {
478
- type: 'input',
479
- name: 'path',
480
- message: chalk.green('Where do you want to create this component?'),
481
- default: 'src/components',
482
- validate: input => /^[a-zA-Z0-9-_/\\]+$/.test(input)
483
- ? true
484
- : 'Path can only contain letters, numbers, slashes, hyphens and underscores'
485
- }
486
- ]);
487
- componentPath = answers.path;
488
- }
227
+ ]);
228
+ componentPath = answers.path;
489
229
  }
490
230
 
491
- // Component template features
492
- const features = await inquirer.prompt([
493
- {
494
- type: 'checkbox',
495
- name: 'features',
496
- message: chalk.green('Select component features:'),
497
- choices: [
498
- { name: 'useState Hook', value: 'useState', checked: true },
499
- { name: 'useEffect Hook', value: 'useEffect' },
500
- { name: 'CSS Module', value: 'cssModule' },
501
- { name: 'PropTypes', value: 'propTypes' },
502
- { name: 'Default Props', value: 'defaultProps' }
503
- ]
504
- }
505
- ]);
506
-
507
- // Create component content based on type and selected features
231
+ // Create component content based on type
508
232
  const fullPath = path.join(process.cwd(), componentPath, `${componentName}${extension}`);
509
233
  const dirPath = path.dirname(fullPath);
510
234
 
511
- // CSS Module path if selected
512
- let cssPath = null;
513
- if (features.features.includes('cssModule')) {
514
- cssPath = path.join(dirPath, `${componentName}.module.css`);
515
- }
516
-
517
- // Build component template
518
- let imports = [];
519
- let hooks = [];
520
- let props = [];
521
- let propsInterface = [];
522
- let renders = [];
523
- let exports = [];
524
-
525
- // Base imports
526
- imports.push(`import { jsx } from 'frontend-hamroun';`);
527
-
528
- // Add selected features
529
- if (features.features.includes('useState')) {
530
- imports[0] = imports[0].replace('jsx', 'jsx, useState');
531
- hooks.push(` const [state, setState] = useState('initial state');`);
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;
242
+ }
243
+
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';
256
+
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();
271
+ try {
272
+ await fs.ensureDir(dirPath);
273
+ await fs.writeFile(fullPath, componentTemplate);
274
+ spinner.success({ text: `Component created at ${chalk.green(fullPath)}` });
275
+ } catch (error) {
276
+ spinner.error({ text: 'Failed to create component' });
277
+ console.error(chalk.red('Error: ') + error.message);
532
278
  }
279
+ }
280
+
281
+ // Create a page component
282
+ async function addPage(pageName, options) {
283
+ console.log(banner);
284
+ createSection('Create Page');
533
285
 
534
- if (features.features.includes('useEffect')) {
535
- imports[0] = imports[0].replace('jsx', 'jsx, useEffect').replace(', useEffect, useEffect', ', useEffect');
536
- hooks.push(` useEffect(() => {
537
- // Side effect code here
538
- console.log('Component mounted');
539
-
540
- return () => {
541
- // Cleanup code here
542
- console.log('Component unmounted');
543
- };
544
- }, []);`);
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'
296
+ }
297
+ ]);
298
+ pageName = answers.pageName;
545
299
  }
546
300
 
547
- if (features.features.includes('cssModule')) {
548
- imports.push(`import styles from './${componentName}.module.css';`);
549
- }
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';
550
306
 
551
- if (features.features.includes('propTypes')) {
552
- imports.push(`import PropTypes from 'prop-types';`);
553
- exports.push(`
554
- ${componentName}.propTypes = {
555
- title: PropTypes.string,
556
- children: PropTypes.node
557
- };`);
558
- }
307
+ // Determine file extension preference
308
+ let extension = options.typescript ? '.tsx' : options.jsx ? '.jsx' : null;
559
309
 
560
- if (features.features.includes('defaultProps')) {
561
- exports.push(`
562
- ${componentName}.defaultProps = {
563
- title: '${componentName} Title'
564
- };`);
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;
565
323
  }
566
324
 
567
- // Build props
568
- if (extension === '.tsx') {
569
- propsInterface.push(`interface ${componentName}Props {
570
- title?: string;
571
- children?: React.ReactNode;
572
- }`);
573
- props.push(`{ title, children }: ${componentName}Props`);
574
- } else {
575
- props.push(`{ title, children }`);
576
- }
325
+ // Create page file path
326
+ const pagePath = options.path || 'src/pages';
577
327
 
578
- // Build render content
579
- const className = features.features.includes('cssModule') ? 'styles.component' : 'component';
580
- const titleClass = features.features.includes('cssModule') ? 'styles.title' : 'title';
581
- const contentClass = features.features.includes('cssModule') ? 'styles.content' : 'content';
582
-
583
- renders.push(` return (
584
- <div className="${className}">
585
- {title && <h2 className="${titleClass}">{title}</h2>}
586
- ${features.features.includes('useState') ? `<p>State value: {state}</p>
587
- <button onClick={() => setState('updated state')}>Update State</button>` : ''}
588
- <div className="${contentClass}">
589
- {children}
590
- </div>
591
- </div>
592
- );`);
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);
593
332
 
594
- // Assemble final component template
595
- const componentTemplate = `${imports.join('\n')}
333
+ // Page template
334
+ const pageTemplate = extension === '.tsx'
335
+ ? `import { jsx } from 'frontend-hamroun';
336
+ import Layout from '../components/Layout';
596
337
 
597
- ${propsInterface.join('\n')}
338
+ interface ${pageComponentName}Props {
339
+ initialState?: any;
340
+ }
598
341
 
599
- export default function ${componentName}(${props.join(', ')}) {
600
- ${hooks.join('\n\n')}
601
- ${renders.join('\n')}
602
- }${exports.join('')}
603
- `;
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
+ );
351
+ };
604
352
 
605
- // CSS Module template if selected
606
- const cssTemplate = `.component {
607
- border: 1px solid #eaeaea;
608
- border-radius: 8px;
609
- padding: 16px;
610
- margin: 16px 0;
611
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
612
- }
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
+ };
359
+ };
613
360
 
614
- .title {
615
- font-size: 1.25rem;
616
- margin-bottom: 12px;
617
- color: #333;
618
- }
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
+ );
375
+ };
619
376
 
620
- .content {
621
- font-size: 1rem;
622
- line-height: 1.5;
623
- }
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
+ };
383
+ };
384
+
385
+ export default ${pageComponentName};
624
386
  `;
625
387
 
626
- // Create the files
627
- const spinner = ora({
628
- text: `Creating ${componentName} component...`,
629
- color: 'cyan'
630
- }).start();
631
-
388
+ // Create the file
389
+ const spinner = createSpinner(`Creating ${pageName} page...`).start();
632
390
  try {
633
- // Create component file
634
391
  await fs.ensureDir(dirPath);
635
- await fs.writeFile(fullPath, componentTemplate);
636
-
637
- // Create CSS module if selected
638
- if (cssPath) {
639
- await fs.writeFile(cssPath, cssTemplate);
640
- }
641
-
642
- spinner.succeed(`Component created at ${chalk.green(fullPath)}`);
643
-
644
- // Show component usage example
645
- console.log(boxen(
646
- `${chalk.bold.green('Component created successfully!')}\n\n` +
647
- `${chalk.bold('Import and use your component:')}\n\n` +
648
- chalk.cyan(`import ${componentName} from './${path.relative(process.cwd(), fullPath).replace(/\\/g, '/').replace(/\.(jsx|tsx)$/, '')}';\n\n`) +
649
- chalk.cyan(`<${componentName} title="My Title">
650
- <p>Child content goes here</p>
651
- </${componentName}>`),
652
- {
653
- padding: 1,
654
- margin: 1,
655
- borderStyle: 'round',
656
- borderColor: 'green'
657
- }
658
- ));
392
+ await fs.writeFile(fullPath, pageTemplate);
393
+ spinner.success({ text: `Page created at ${chalk.green(fullPath)}` });
659
394
  } catch (error) {
660
- spinner.fail(`Failed to create component`);
661
- console.error(chalk.red(`Error: ${error.message}`));
395
+ spinner.error({ text: 'Failed to create page' });
396
+ console.error(chalk.red('Error: ') + error.message);
662
397
  }
663
398
  }
664
399
 
665
- // Format help text for better readability
666
- function formatHelp(commandName, options) {
667
- let help = `\n ${chalk.bold.cyan('Usage:')} ${chalk.yellow(`frontend-hamroun ${commandName} [options]`)}\n\n`;
400
+ // Add API route
401
+ async function addApiRoute(routeName, options) {
402
+ console.log(banner);
403
+ createSection('Create API Route');
668
404
 
669
- help += ` ${chalk.bold.cyan('Options:')}\n`;
670
- options.forEach(opt => {
671
- help += ` ${chalk.green(opt.flags.padEnd(20))} ${opt.description}\n`;
672
- });
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
+ }
673
419
 
674
- help += `\n ${chalk.bold.cyan('Examples:')}\n`;
675
- help += ` ${chalk.yellow(`frontend-hamroun ${commandName} MyComponent`)}\n`;
676
- help += ` ${chalk.yellow(`frontend-hamroun ${commandName} NavBar --typescript`)}\n`;
420
+ // Determine file extension preference
421
+ let extension = options.typescript ? '.ts' : '.js';
677
422
 
678
- return help;
679
- }
680
-
681
- // Dashboard when no commands are specified
682
- function showDashboard() {
683
- displayBanner();
684
-
685
- const commands = [
686
- { name: 'create', description: 'Create a new project', command: 'frontend-hamroun create my-app' },
687
- { name: 'add:component', description: 'Add a new UI component', command: 'frontend-hamroun add:component Button' },
688
- { name: 'add:page', description: 'Create a new page', command: 'frontend-hamroun add:page home' },
689
- { name: 'add:api', description: 'Create a new API endpoint', command: 'frontend-hamroun add:api users' },
690
- { name: 'dev:tools', description: 'Show development tools', command: 'frontend-hamroun dev:tools' }
691
- ];
692
-
693
- console.log(boxen(
694
- `${chalk.bold.cyan('Welcome to Frontend Hamroun CLI!')}\n\n` +
695
- `Select a command to get started or run ${chalk.yellow('frontend-hamroun <command> --help')} for more info.\n`,
696
- {
697
- padding: 1,
698
- margin: { top: 1, bottom: 1 },
699
- borderStyle: 'round',
700
- borderColor: 'cyan'
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];
701
475
  }
702
- ));
476
+ }
477
+
478
+ const fullPath = path.join(process.cwd(), routePath, directory, `${filename}${extension}`);
479
+ const dirPath = path.dirname(fullPath);
703
480
 
704
- console.log(`${chalk.bold.cyan('Available Commands:')}\n`);
705
- commands.forEach((cmd, i) => {
706
- console.log(` ${chalk.green((i+1) + '.')} ${chalk.bold(cmd.name.padEnd(15))} ${chalk.dim(cmd.description)}`);
707
- console.log(` ${chalk.yellow(cmd.command)}\n`);
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) => {
491
+ res.json({
492
+ message: 'DELETE ${routeName} endpoint',
493
+ timestamp: new Date().toISOString()
494
+ });
495
+ };\n\n`;
496
+ } else {
497
+ apiTemplate += extension === '.ts'
498
+ ? `export const ${method} = (req: Request, res: Response) => {
499
+ res.json({
500
+ message: '${method.toUpperCase()} ${routeName} endpoint',
501
+ timestamp: new Date().toISOString()
502
+ });
503
+ };\n\n`
504
+ : `export const ${method} = (req, res) => {
505
+ res.json({
506
+ message: '${method.toUpperCase()} ${routeName} endpoint',
507
+ timestamp: new Date().toISOString()
508
+ });
509
+ };\n\n`;
510
+ }
708
511
  });
709
512
 
710
- console.log(boxen(
711
- `${chalk.bold('Quick Start:')} ${chalk.yellow('frontend-hamroun create my-app')}\n` +
712
- `${chalk.dim('More info:')} ${terminalLink('Documentation', 'https://github.com/hamroun/frontend-hamroun')}`,
713
- {
714
- padding: 0.5,
715
- margin: { top: 1 },
716
- borderStyle: 'round',
717
- borderColor: 'blue'
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`);
718
531
  }
719
- ));
532
+ } catch (error) {
533
+ spinner.error({ text: 'Failed to create API route' });
534
+ console.error(chalk.red('Error: ') + error.message);
535
+ }
720
536
  }
721
537
 
722
- // Register commands with improved descriptions
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
723
565
  program
724
566
  .command('create [name]')
725
567
  .description('Create a new Frontend Hamroun project')
@@ -740,10 +582,7 @@ program
740
582
  .option('-p, --path <path>', 'Path where the page should be created')
741
583
  .option('-ts, --typescript', 'Use TypeScript')
742
584
  .option('-jsx, --jsx', 'Use JSX')
743
- .action((pageName, options) => {
744
- // We'll keep the existing implementation for now
745
- console.log("The add:page command has been improved in your version.");
746
- });
585
+ .action(addPage);
747
586
 
748
587
  program
749
588
  .command('add:api [name]')
@@ -752,22 +591,17 @@ program
752
591
  .option('-ts, --typescript', 'Use TypeScript')
753
592
  .option('-js, --javascript', 'Use JavaScript')
754
593
  .option('-m, --methods <methods>', 'HTTP methods to implement (comma-separated: get,post,put,delete,patch)')
755
- .action((routeName, options) => {
756
- // We'll keep the existing implementation for now
757
- console.log("The add:api command has been improved in your version.");
758
- });
594
+ .action(addApiRoute);
759
595
 
760
596
  program
761
597
  .command('dev:tools')
762
598
  .description('Show development tools and tips')
763
- .action(() => {
764
- // We'll keep the existing implementation for now
765
- console.log("The dev:tools command has been improved in your version.");
766
- });
599
+ .action(showDevTools);
767
600
 
768
601
  // Default command when no arguments
769
602
  if (process.argv.length <= 2) {
770
- showDashboard();
771
- } else {
772
- program.parse(process.argv);
773
- }
603
+ console.log(banner);
604
+ program.help();
605
+ }
606
+
607
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontend-hamroun",
3
- "version": "1.2.35",
3
+ "version": "1.2.41",
4
4
  "description": "A lightweight full-stack JavaScript framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -125,24 +125,18 @@
125
125
  },
126
126
  "dependencies": {
127
127
  "bcryptjs": "^2.4.3",
128
- "boxen": "^7.1.1",
129
128
  "chalk": "^5.3.0",
130
129
  "commander": "^11.0.0",
131
130
  "cors": "^2.8.5",
132
131
  "express": "^4.18.2",
133
- "figlet": "^1.7.0",
134
132
  "fs-extra": "^11.1.1",
135
- "gradient-string": "^3.0.0",
136
133
  "inquirer": "^9.2.10",
137
134
  "jsonwebtoken": "^9.0.2",
138
135
  "mongodb": "^5.7.0",
139
136
  "mysql2": "^3.6.1",
140
137
  "nanospinner": "^1.1.0",
141
- "ora": "^7.0.1",
142
138
  "pg": "^8.11.3",
143
- "terminal-link": "^3.0.0",
144
- "ts-node": "^10.9.2",
145
- "update-notifier": "^6.0.2"
139
+ "ts-node": "^10.9.2"
146
140
  },
147
141
  "peerDependencies": {
148
142
  "react": ">=16.8.0",