frontend-hamroun 1.2.40 → 1.2.42

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