frontend-hamroun 1.2.28 → 1.2.29

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