frontend-hamroun 1.2.28 → 1.2.30

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