servcraft 0.1.6 → 0.2.0

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.
@@ -6,10 +6,12 @@ import inquirer from 'inquirer';
6
6
  import chalk from 'chalk';
7
7
  import { execSync } from 'child_process';
8
8
  import { ensureDir, writeFile, error, warn } from '../utils/helpers.js';
9
+ import { DryRunManager } from '../utils/dry-run.js';
9
10
 
10
11
  interface InitOptions {
11
12
  name: string;
12
13
  language: 'typescript' | 'javascript';
14
+ moduleSystem: 'esm' | 'commonjs';
13
15
  database: 'postgresql' | 'mysql' | 'sqlite' | 'mongodb' | 'none';
14
16
  orm: 'prisma' | 'mongoose' | 'none';
15
17
  validator: 'zod' | 'joi' | 'yup';
@@ -23,12 +25,30 @@ export const initCommand = new Command('init')
23
25
  .option('-y, --yes', 'Skip prompts and use defaults')
24
26
  .option('--ts, --typescript', 'Use TypeScript (default)')
25
27
  .option('--js, --javascript', 'Use JavaScript')
28
+ .option('--esm', 'Use ES Modules (import/export) - default')
29
+ .option('--cjs, --commonjs', 'Use CommonJS (require/module.exports)')
26
30
  .option('--db <database>', 'Database type (postgresql, mysql, sqlite, mongodb, none)')
31
+ .option('--dry-run', 'Preview changes without writing files')
27
32
  .action(
28
33
  async (
29
34
  name?: string,
30
- cmdOptions?: { yes?: boolean; typescript?: boolean; javascript?: boolean; db?: string }
35
+ cmdOptions?: {
36
+ yes?: boolean;
37
+ typescript?: boolean;
38
+ javascript?: boolean;
39
+ esm?: boolean;
40
+ commonjs?: boolean;
41
+ db?: string;
42
+ dryRun?: boolean;
43
+ }
31
44
  ) => {
45
+ // Enable dry-run mode if specified
46
+ const dryRun = DryRunManager.getInstance();
47
+ if (cmdOptions?.dryRun) {
48
+ dryRun.enable();
49
+ console.log(chalk.yellow('\n⚠ DRY RUN MODE - No files will be written\n'));
50
+ }
51
+
32
52
  console.log(
33
53
  chalk.blue(`
34
54
  ╔═══════════════════════════════════════════╗
@@ -46,6 +66,7 @@ export const initCommand = new Command('init')
46
66
  options = {
47
67
  name: name || 'my-servcraft-app',
48
68
  language: cmdOptions.javascript ? 'javascript' : 'typescript',
69
+ moduleSystem: cmdOptions.commonjs ? 'commonjs' : 'esm',
49
70
  database: db,
50
71
  orm: db === 'mongodb' ? 'mongoose' : db === 'none' ? 'none' : 'prisma',
51
72
  validator: 'zod',
@@ -75,6 +96,16 @@ export const initCommand = new Command('init')
75
96
  ],
76
97
  default: 'typescript',
77
98
  },
99
+ {
100
+ type: 'list',
101
+ name: 'moduleSystem',
102
+ message: 'Select module system:',
103
+ choices: [
104
+ { name: 'ESM (import/export) - Recommended', value: 'esm' },
105
+ { name: 'CommonJS (require/module.exports)', value: 'commonjs' },
106
+ ],
107
+ default: 'esm',
108
+ },
78
109
  {
79
110
  type: 'list',
80
111
  name: 'database',
@@ -150,10 +181,10 @@ export const initCommand = new Command('init')
150
181
 
151
182
  // Generate tsconfig or jsconfig
152
183
  if (options.language === 'typescript') {
153
- await writeFile(path.join(projectDir, 'tsconfig.json'), generateTsConfig());
154
- await writeFile(path.join(projectDir, 'tsup.config.ts'), generateTsupConfig());
184
+ await writeFile(path.join(projectDir, 'tsconfig.json'), generateTsConfig(options));
185
+ await writeFile(path.join(projectDir, 'tsup.config.ts'), generateTsupConfig(options));
155
186
  } else {
156
- await writeFile(path.join(projectDir, 'jsconfig.json'), generateJsConfig());
187
+ await writeFile(path.join(projectDir, 'jsconfig.json'), generateJsConfig(options));
157
188
  }
158
189
 
159
190
  // Generate .env files
@@ -171,7 +202,10 @@ export const initCommand = new Command('init')
171
202
  );
172
203
 
173
204
  // Create directory structure
174
- const ext = options.language === 'typescript' ? 'ts' : 'js';
205
+ // For JS: .js for ESM, .cjs for CommonJS
206
+ // For TS: always .ts (output format handled by tsup)
207
+ const ext =
208
+ options.language === 'typescript' ? 'ts' : options.moduleSystem === 'esm' ? 'js' : 'cjs';
175
209
  const dirs = [
176
210
  'src/core',
177
211
  'src/config',
@@ -207,6 +241,30 @@ export const initCommand = new Command('init')
207
241
  generateLoggerFile(options)
208
242
  );
209
243
 
244
+ // Generate config file
245
+ await writeFile(
246
+ path.join(projectDir, `src/config/index.${ext}`),
247
+ generateConfigFile(options)
248
+ );
249
+
250
+ // Generate middleware file
251
+ await writeFile(
252
+ path.join(projectDir, `src/middleware/index.${ext}`),
253
+ generateMiddlewareFile(options)
254
+ );
255
+
256
+ // Generate utils file
257
+ await writeFile(
258
+ path.join(projectDir, `src/utils/index.${ext}`),
259
+ generateUtilsFile(options)
260
+ );
261
+
262
+ // Generate types file
263
+ await writeFile(
264
+ path.join(projectDir, `src/types/index.${ext}`),
265
+ generateTypesFile(options)
266
+ );
267
+
210
268
  // Generate database files based on ORM choice
211
269
  if (options.orm === 'prisma') {
212
270
  await writeFile(
@@ -226,19 +284,23 @@ export const initCommand = new Command('init')
226
284
 
227
285
  spinner.succeed('Project files generated!');
228
286
 
229
- // Install dependencies
230
- const installSpinner = ora('Installing dependencies...').start();
231
-
232
- try {
233
- execSync('npm install', { cwd: projectDir, stdio: 'pipe' });
234
- installSpinner.succeed('Dependencies installed!');
235
- } catch {
236
- installSpinner.warn('Failed to install dependencies automatically');
237
- warn(' Run "npm install" manually in the project directory');
287
+ // Install dependencies (skip in dry-run mode)
288
+ if (!cmdOptions?.dryRun) {
289
+ const installSpinner = ora('Installing dependencies...').start();
290
+
291
+ try {
292
+ execSync('npm install', { cwd: projectDir, stdio: 'pipe' });
293
+ installSpinner.succeed('Dependencies installed!');
294
+ } catch {
295
+ installSpinner.warn('Failed to install dependencies automatically');
296
+ warn(' Run "npm install" manually in the project directory');
297
+ }
238
298
  }
239
299
 
240
300
  // Print success message
241
- console.log('\n' + chalk.green('✨ Project created successfully!'));
301
+ if (!cmdOptions?.dryRun) {
302
+ console.log('\n' + chalk.green('✨ Project created successfully!'));
303
+ }
242
304
  console.log('\n' + chalk.bold('📁 Project structure:'));
243
305
  console.log(`
244
306
  ${options.name}/
@@ -269,6 +331,11 @@ export const initCommand = new Command('init')
269
331
  ${chalk.yellow('servcraft generate service <name>')} Generate a service
270
332
  ${chalk.yellow('servcraft add auth')} Add authentication module
271
333
  `);
334
+
335
+ // Show dry-run summary if enabled
336
+ if (cmdOptions?.dryRun) {
337
+ dryRun.printSummary();
338
+ }
272
339
  } catch (err) {
273
340
  spinner.fail('Failed to create project');
274
341
  error(err instanceof Error ? err.message : String(err));
@@ -278,19 +345,46 @@ export const initCommand = new Command('init')
278
345
 
279
346
  function generatePackageJson(options: InitOptions): Record<string, unknown> {
280
347
  const isTS = options.language === 'typescript';
348
+ const isESM = options.moduleSystem === 'esm';
349
+
350
+ // Determine dev command based on language and module system
351
+ let devCommand: string;
352
+ if (isTS) {
353
+ devCommand = 'tsx watch src/index.ts';
354
+ } else if (isESM) {
355
+ devCommand = 'node --watch src/index.js';
356
+ } else {
357
+ devCommand = 'node --watch src/index.cjs';
358
+ }
359
+
360
+ // Determine start command
361
+ let startCommand: string;
362
+ if (isTS) {
363
+ startCommand = isESM ? 'node dist/index.js' : 'node dist/index.cjs';
364
+ } else if (isESM) {
365
+ startCommand = 'node src/index.js';
366
+ } else {
367
+ startCommand = 'node src/index.cjs';
368
+ }
281
369
 
282
370
  const pkg: Record<string, unknown> = {
283
371
  name: options.name,
284
372
  version: '0.1.0',
285
373
  description: 'A Servcraft application',
286
- main: isTS ? 'dist/index.js' : 'src/index.js',
287
- type: 'module',
374
+ main: isTS
375
+ ? isESM
376
+ ? 'dist/index.js'
377
+ : 'dist/index.cjs'
378
+ : isESM
379
+ ? 'src/index.js'
380
+ : 'src/index.cjs',
381
+ ...(isESM && { type: 'module' }),
288
382
  scripts: {
289
- dev: isTS ? 'tsx watch src/index.ts' : 'node --watch src/index.js',
383
+ dev: devCommand,
290
384
  build: isTS ? 'tsup' : 'echo "No build needed for JS"',
291
- start: isTS ? 'node dist/index.js' : 'node src/index.js',
385
+ start: startCommand,
292
386
  test: 'vitest',
293
- lint: isTS ? 'eslint src --ext .ts' : 'eslint src --ext .js',
387
+ lint: isTS ? 'eslint src --ext .ts' : 'eslint src --ext .js,.cjs',
294
388
  },
295
389
  dependencies: {
296
390
  fastify: '^4.28.1',
@@ -361,13 +455,15 @@ function generatePackageJson(options: InitOptions): Record<string, unknown> {
361
455
  return pkg;
362
456
  }
363
457
 
364
- function generateTsConfig(): string {
458
+ function generateTsConfig(options: InitOptions): string {
459
+ const isESM = options.moduleSystem === 'esm';
460
+
365
461
  return JSON.stringify(
366
462
  {
367
463
  compilerOptions: {
368
464
  target: 'ES2022',
369
- module: 'NodeNext',
370
- moduleResolution: 'NodeNext',
465
+ module: isESM ? 'NodeNext' : 'CommonJS',
466
+ moduleResolution: isESM ? 'NodeNext' : 'Node',
371
467
  lib: ['ES2022'],
372
468
  outDir: './dist',
373
469
  rootDir: './src',
@@ -387,12 +483,14 @@ function generateTsConfig(): string {
387
483
  );
388
484
  }
389
485
 
390
- function generateJsConfig(): string {
486
+ function generateJsConfig(options: InitOptions): string {
487
+ const isESM = options.moduleSystem === 'esm';
488
+
391
489
  return JSON.stringify(
392
490
  {
393
491
  compilerOptions: {
394
- module: 'NodeNext',
395
- moduleResolution: 'NodeNext',
492
+ module: isESM ? 'NodeNext' : 'CommonJS',
493
+ moduleResolution: isESM ? 'NodeNext' : 'Node',
396
494
  target: 'ES2022',
397
495
  checkJs: true,
398
496
  },
@@ -404,12 +502,14 @@ function generateJsConfig(): string {
404
502
  );
405
503
  }
406
504
 
407
- function generateTsupConfig(): string {
505
+ function generateTsupConfig(options: InitOptions): string {
506
+ const isESM = options.moduleSystem === 'esm';
507
+
408
508
  return `import { defineConfig } from 'tsup';
409
509
 
410
510
  export default defineConfig({
411
511
  entry: ['src/index.ts'],
412
- format: ['esm'],
512
+ format: ['${isESM ? 'esm' : 'cjs'}'],
413
513
  dts: true,
414
514
  clean: true,
415
515
  sourcemap: true,
@@ -619,8 +719,13 @@ model User {
619
719
 
620
720
  function generateEntryFile(options: InitOptions): string {
621
721
  const isTS = options.language === 'typescript';
722
+ const isESM = options.moduleSystem === 'esm';
723
+ const fileExt = isTS ? 'js' : isESM ? 'js' : 'cjs';
622
724
 
623
- return `${isTS ? "import 'dotenv/config';\nimport { createServer } from './core/server.js';\nimport { logger } from './core/logger.js';" : "require('dotenv').config();\nconst { createServer } = require('./core/server.js');\nconst { logger } = require('./core/logger.js');"}
725
+ if (isESM || isTS) {
726
+ return `import 'dotenv/config';
727
+ import { createServer } from './core/server.${fileExt}';
728
+ import { logger } from './core/logger.${fileExt}';
624
729
 
625
730
  async function main()${isTS ? ': Promise<void>' : ''} {
626
731
  const server = createServer();
@@ -635,22 +740,34 @@ async function main()${isTS ? ': Promise<void>' : ''} {
635
740
 
636
741
  main();
637
742
  `;
743
+ } else {
744
+ // CommonJS
745
+ return `require('dotenv').config();
746
+ const { createServer } = require('./core/server.cjs');
747
+ const { logger } = require('./core/logger.cjs');
748
+
749
+ async function main() {
750
+ const server = createServer();
751
+
752
+ try {
753
+ await server.start();
754
+ } catch (error) {
755
+ logger.error({ err: error }, 'Failed to start server');
756
+ process.exit(1);
757
+ }
758
+ }
759
+
760
+ main();
761
+ `;
762
+ }
638
763
  }
639
764
 
640
765
  function generateServerFile(options: InitOptions): string {
641
766
  const isTS = options.language === 'typescript';
767
+ const isESM = options.moduleSystem === 'esm';
768
+ const fileExt = isTS ? 'js' : isESM ? 'js' : 'cjs';
642
769
 
643
- return `${
644
- isTS
645
- ? `import Fastify from 'fastify';
646
- import type { FastifyInstance } from 'fastify';
647
- import { logger } from './logger.js';`
648
- : `const Fastify = require('fastify');
649
- const { logger } = require('./logger.js');`
650
- }
651
-
652
- ${isTS ? 'export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }' : 'function createServer()'} {
653
- const app = Fastify({ logger });
770
+ const serverBody = ` const app = Fastify({ logger });
654
771
 
655
772
  // Health check
656
773
  app.get('/health', async () => ({
@@ -677,37 +794,66 @@ ${isTS ? 'export function createServer(): { instance: FastifyInstance; start: ()
677
794
  logger.info(\`Server listening on \${host}:\${port}\`);
678
795
  },
679
796
  };
680
- }
797
+ }`;
798
+
799
+ if (isESM || isTS) {
800
+ return `import Fastify from 'fastify';
801
+ ${isTS ? "import type { FastifyInstance } from 'fastify';" : ''}
802
+ import { logger } from './logger.${fileExt}';
803
+
804
+ ${isTS ? 'export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }' : 'export function createServer()'} {
805
+ ${serverBody}
806
+ `;
807
+ } else {
808
+ // CommonJS
809
+ return `const Fastify = require('fastify');
810
+ const { logger } = require('./logger.cjs');
681
811
 
682
- ${isTS ? '' : 'module.exports = { createServer };'}
812
+ function createServer() {
813
+ ${serverBody}
814
+
815
+ module.exports = { createServer };
683
816
  `;
817
+ }
684
818
  }
685
819
 
686
820
  function generateLoggerFile(options: InitOptions): string {
687
821
  const isTS = options.language === 'typescript';
822
+ const isESM = options.moduleSystem === 'esm';
688
823
 
689
- return `${isTS ? "import pino from 'pino';\nimport type { Logger } from 'pino';" : "const pino = require('pino');"}
690
-
691
- ${isTS ? 'export const logger: Logger' : 'const logger'} = pino({
824
+ const loggerBody = `pino({
692
825
  level: process.env.LOG_LEVEL || 'info',
693
826
  transport: process.env.NODE_ENV !== 'production' ? {
694
827
  target: 'pino-pretty',
695
828
  options: { colorize: true },
696
829
  } : undefined,
697
- });
830
+ })`;
831
+
832
+ if (isESM || isTS) {
833
+ return `import pino from 'pino';
834
+ ${isTS ? "import type { Logger } from 'pino';" : ''}
698
835
 
699
- ${isTS ? '' : 'module.exports = { logger };'}
836
+ export const logger${isTS ? ': Logger' : ''} = ${loggerBody};
700
837
  `;
838
+ } else {
839
+ // CommonJS
840
+ return `const pino = require('pino');
841
+
842
+ const logger = ${loggerBody};
843
+
844
+ module.exports = { logger };
845
+ `;
846
+ }
701
847
  }
702
848
 
703
849
  function generateMongooseConnection(options: InitOptions): string {
704
850
  const isTS = options.language === 'typescript';
851
+ const isESM = options.moduleSystem === 'esm';
852
+ const fileExt = isTS ? 'js' : isESM ? 'js' : 'cjs';
705
853
 
706
- return `${isTS ? "import mongoose from 'mongoose';\nimport { logger } from '../core/logger.js';" : "const mongoose = require('mongoose');\nconst { logger } = require('../core/logger.js');"}
854
+ const connectionBody = `const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
707
855
 
708
- const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
709
-
710
- ${isTS ? 'export async function connectDatabase(): Promise<typeof mongoose>' : 'async function connectDatabase()'} {
856
+ async function connectDatabase()${isTS ? ': Promise<typeof mongoose>' : ''} {
711
857
  try {
712
858
  const conn = await mongoose.connect(MONGODB_URI);
713
859
  logger.info(\`MongoDB connected: \${conn.connection.host}\`);
@@ -718,41 +864,40 @@ ${isTS ? 'export async function connectDatabase(): Promise<typeof mongoose>' : '
718
864
  }
719
865
  }
720
866
 
721
- ${isTS ? 'export async function disconnectDatabase(): Promise<void>' : 'async function disconnectDatabase()'} {
867
+ async function disconnectDatabase()${isTS ? ': Promise<void>' : ''} {
722
868
  try {
723
869
  await mongoose.disconnect();
724
870
  logger.info('MongoDB disconnected');
725
871
  } catch (error) {
726
872
  logger.error({ err: error }, 'MongoDB disconnect failed');
727
873
  }
728
- }
874
+ }`;
729
875
 
730
- ${isTS ? 'export { mongoose };' : 'module.exports = { connectDatabase, disconnectDatabase, mongoose };'}
876
+ if (isESM || isTS) {
877
+ return `import mongoose from 'mongoose';
878
+ import { logger } from '../core/logger.${fileExt}';
879
+
880
+ ${connectionBody}
881
+
882
+ export { connectDatabase, disconnectDatabase, mongoose };
883
+ `;
884
+ } else {
885
+ // CommonJS
886
+ return `const mongoose = require('mongoose');
887
+ const { logger } = require('../core/logger.cjs');
888
+
889
+ ${connectionBody}
890
+
891
+ module.exports = { connectDatabase, disconnectDatabase, mongoose };
731
892
  `;
893
+ }
732
894
  }
733
895
 
734
896
  function generateMongooseUserModel(options: InitOptions): string {
735
897
  const isTS = options.language === 'typescript';
898
+ const isESM = options.moduleSystem === 'esm';
736
899
 
737
- return `${isTS ? "import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';" : "const mongoose = require('mongoose');\nconst bcrypt = require('bcryptjs');\nconst { Schema } = mongoose;"}
738
-
739
- ${
740
- isTS
741
- ? `export interface IUser extends Document {
742
- email: string;
743
- password: string;
744
- name?: string;
745
- role: 'user' | 'admin';
746
- status: 'active' | 'inactive' | 'suspended';
747
- emailVerified: boolean;
748
- createdAt: Date;
749
- updatedAt: Date;
750
- comparePassword(candidatePassword: string): Promise<boolean>;
751
- }`
752
- : ''
753
- }
754
-
755
- const userSchema = new Schema${isTS ? '<IUser>' : ''}({
900
+ const schemaBody = `const userSchema = new Schema${isTS ? '<IUser>' : ''}({
756
901
  email: {
757
902
  type: String,
758
903
  required: true,
@@ -803,8 +948,258 @@ userSchema.pre('save', async function(next) {
803
948
  // Compare password method
804
949
  userSchema.methods.comparePassword = async function(candidatePassword${isTS ? ': string' : ''})${isTS ? ': Promise<boolean>' : ''} {
805
950
  return bcrypt.compare(candidatePassword, this.password);
806
- };
951
+ };`;
952
+
953
+ const tsInterface = isTS
954
+ ? `
955
+ export interface IUser extends Document {
956
+ email: string;
957
+ password: string;
958
+ name?: string;
959
+ role: 'user' | 'admin';
960
+ status: 'active' | 'inactive' | 'suspended';
961
+ emailVerified: boolean;
962
+ createdAt: Date;
963
+ updatedAt: Date;
964
+ comparePassword(candidatePassword: string): Promise<boolean>;
965
+ }
966
+ `
967
+ : '';
968
+
969
+ if (isESM || isTS) {
970
+ return `import mongoose${isTS ? ', { Schema, Document }' : ''} from 'mongoose';
971
+ import bcrypt from 'bcryptjs';
972
+ ${!isTS ? 'const { Schema } = mongoose;' : ''}
973
+ ${tsInterface}
974
+ ${schemaBody}
975
+
976
+ export const User = mongoose.model${isTS ? '<IUser>' : ''}('User', userSchema);
977
+ `;
978
+ } else {
979
+ // CommonJS
980
+ return `const mongoose = require('mongoose');
981
+ const bcrypt = require('bcryptjs');
982
+ const { Schema } = mongoose;
983
+
984
+ ${schemaBody}
985
+
986
+ const User = mongoose.model('User', userSchema);
987
+
988
+ module.exports = { User };
989
+ `;
990
+ }
991
+ }
992
+
993
+ function generateConfigFile(options: InitOptions): string {
994
+ const isTS = options.language === 'typescript';
995
+ const isESM = options.moduleSystem === 'esm';
996
+
997
+ const configBody = `{
998
+ env: process.env.NODE_ENV || 'development',
999
+ port: parseInt(process.env.PORT || '3000', 10),
1000
+ host: process.env.HOST || '0.0.0.0',
1001
+
1002
+ jwt: {
1003
+ secret: process.env.JWT_SECRET || 'your-secret-key',
1004
+ accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '15m',
1005
+ refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
1006
+ },
807
1007
 
808
- ${isTS ? "export const User = mongoose.model<IUser>('User', userSchema);" : "const User = mongoose.model('User', userSchema);\nmodule.exports = { User };"}
1008
+ cors: {
1009
+ origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
1010
+ },
1011
+
1012
+ rateLimit: {
1013
+ max: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
1014
+ },
1015
+
1016
+ log: {
1017
+ level: process.env.LOG_LEVEL || 'info',
1018
+ },
1019
+ }${isTS ? ' as const' : ''}`;
1020
+
1021
+ if (isESM || isTS) {
1022
+ return `import 'dotenv/config';
1023
+
1024
+ export const config = ${configBody};
1025
+ `;
1026
+ } else {
1027
+ // CommonJS
1028
+ return `require('dotenv').config();
1029
+
1030
+ const config = ${configBody};
1031
+
1032
+ module.exports = { config };
1033
+ `;
1034
+ }
1035
+ }
1036
+
1037
+ function generateMiddlewareFile(options: InitOptions): string {
1038
+ const isTS = options.language === 'typescript';
1039
+ const isESM = options.moduleSystem === 'esm';
1040
+ const fileExt = isTS ? 'js' : isESM ? 'js' : 'cjs';
1041
+
1042
+ const middlewareBody = `/**
1043
+ * Error handler middleware
1044
+ */
1045
+ function errorHandler(error${isTS ? ': Error' : ''}, request${isTS ? ': FastifyRequest' : ''}, reply${isTS ? ': FastifyReply' : ''})${isTS ? ': void' : ''} {
1046
+ logger.error({ err: error, url: request.url, method: request.method }, 'Request error');
1047
+
1048
+ const statusCode = (error${isTS ? ' as any' : ''}).statusCode || 500;
1049
+ const message = statusCode === 500 ? 'Internal Server Error' : error.message;
1050
+
1051
+ reply.status(statusCode).send({
1052
+ success: false,
1053
+ error: message,
1054
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
1055
+ });
1056
+ }
1057
+
1058
+ /**
1059
+ * Request logging middleware
1060
+ */
1061
+ function requestLogger(request${isTS ? ': FastifyRequest' : ''}, reply${isTS ? ': FastifyReply' : ''}, done${isTS ? ': () => void' : ''})${isTS ? ': void' : ''} {
1062
+ logger.info({ url: request.url, method: request.method, ip: request.ip }, 'Incoming request');
1063
+ done();
1064
+ }`;
1065
+
1066
+ if (isESM || isTS) {
1067
+ return `${isTS ? "import type { FastifyRequest, FastifyReply } from 'fastify';" : ''}
1068
+ import { logger } from '../core/logger.${fileExt}';
1069
+
1070
+ ${middlewareBody}
1071
+
1072
+ export { errorHandler, requestLogger };
1073
+ `;
1074
+ } else {
1075
+ // CommonJS
1076
+ return `const { logger } = require('../core/logger.cjs');
1077
+
1078
+ ${middlewareBody}
1079
+
1080
+ module.exports = { errorHandler, requestLogger };
1081
+ `;
1082
+ }
1083
+ }
1084
+
1085
+ function generateUtilsFile(options: InitOptions): string {
1086
+ const isTS = options.language === 'typescript';
1087
+ const isESM = options.moduleSystem === 'esm';
1088
+
1089
+ const utilsBody = `/**
1090
+ * Standard API response helper
1091
+ */
1092
+ function apiResponse${isTS ? '<T>' : ''}(data${isTS ? ': T' : ''}, message = "Success")${isTS ? ': { success: boolean; message: string; data: T }' : ''} {
1093
+ return {
1094
+ success: true,
1095
+ message,
1096
+ data,
1097
+ };
1098
+ }
1099
+
1100
+ /**
1101
+ * Error response helper
1102
+ */
1103
+ function errorResponse(message${isTS ? ': string' : ''}, code${isTS ? '?: string' : ''})${isTS ? ': { success: boolean; error: string; code?: string }' : ''} {
1104
+ return {
1105
+ success: false,
1106
+ error: message,
1107
+ ...(code && { code }),
1108
+ };
1109
+ }
1110
+
1111
+ /**
1112
+ * Pagination helper
1113
+ */
1114
+ function paginate${isTS ? '<T>' : ''}(data${isTS ? ': T[]' : ''}, page${isTS ? ': number' : ''}, limit${isTS ? ': number' : ''}, total${isTS ? ': number' : ''})${isTS ? ': PaginationResult<T>' : ''} {
1115
+ const totalPages = Math.ceil(total / limit);
1116
+
1117
+ return {
1118
+ data,
1119
+ pagination: {
1120
+ page,
1121
+ limit,
1122
+ total,
1123
+ totalPages,
1124
+ hasNextPage: page < totalPages,
1125
+ hasPrevPage: page > 1,
1126
+ },
1127
+ };
1128
+ }`;
1129
+
1130
+ const tsInterface = isTS
1131
+ ? `
1132
+ /**
1133
+ * Pagination result type
1134
+ */
1135
+ export interface PaginationResult<T> {
1136
+ data: T[];
1137
+ pagination: {
1138
+ page: number;
1139
+ limit: number;
1140
+ total: number;
1141
+ totalPages: number;
1142
+ hasNextPage: boolean;
1143
+ hasPrevPage: boolean;
1144
+ };
1145
+ }
1146
+ `
1147
+ : '';
1148
+
1149
+ if (isESM || isTS) {
1150
+ return `${tsInterface}
1151
+ ${utilsBody}
1152
+
1153
+ export { apiResponse, errorResponse, paginate };
1154
+ `;
1155
+ } else {
1156
+ // CommonJS
1157
+ return `${utilsBody}
1158
+
1159
+ module.exports = { apiResponse, errorResponse, paginate };
1160
+ `;
1161
+ }
1162
+ }
1163
+
1164
+ function generateTypesFile(options: InitOptions): string {
1165
+ if (options.language !== 'typescript') {
1166
+ return '// Types file - not needed for JavaScript\n';
1167
+ }
1168
+
1169
+ return `/**
1170
+ * Common type definitions
1171
+ */
1172
+
1173
+ export interface ApiResponse<T = unknown> {
1174
+ success: boolean;
1175
+ message?: string;
1176
+ data?: T;
1177
+ error?: string;
1178
+ code?: string;
1179
+ }
1180
+
1181
+ export interface PaginatedResponse<T> {
1182
+ data: T[];
1183
+ pagination: {
1184
+ page: number;
1185
+ limit: number;
1186
+ total: number;
1187
+ totalPages: number;
1188
+ hasNextPage: boolean;
1189
+ hasPrevPage: boolean;
1190
+ };
1191
+ }
1192
+
1193
+ export interface RequestUser {
1194
+ id: string;
1195
+ email: string;
1196
+ role: string;
1197
+ }
1198
+
1199
+ declare module 'fastify' {
1200
+ interface FastifyRequest {
1201
+ user?: RequestUser;
1202
+ }
1203
+ }
809
1204
  `;
810
1205
  }