servcraft 0.4.6 → 0.4.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "servcraft",
3
- "version": "0.4.6",
3
+ "version": "0.4.9",
4
4
  "description": "A modular, production-ready Node.js backend framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -681,7 +681,13 @@ async function findServercraftModules(): Promise<string | null> {
681
681
  /**
682
682
  * Helper: Generate module files - copies from servcraft package modules
683
683
  */
684
- async function generateModuleFiles(moduleName: string, moduleDir: string): Promise<void> {
684
+ async function generateModuleFiles(
685
+ moduleName: string,
686
+ moduleDir: string,
687
+ language: 'typescript' | 'javascript' = 'typescript',
688
+ moduleSystem: 'esm' | 'commonjs' = 'esm',
689
+ fileExtension: 'js' | 'cjs' | 'ts' = 'ts'
690
+ ): Promise<void> {
685
691
  // Map module names to their directory names in servcraft
686
692
  const moduleNameMap: Record<string, string> = {
687
693
  users: 'user',
@@ -701,7 +707,8 @@ async function generateModuleFiles(moduleName: string, moduleDir: string): Promi
701
707
 
702
708
  if (await fileExists(sourceModuleDir)) {
703
709
  // Copy from servcraft package
704
- await copyModuleFromSource(sourceModuleDir, moduleDir);
710
+ const jsExt = language === 'javascript' ? (fileExtension as 'js' | 'cjs') : 'js';
711
+ await copyModuleFromSource(sourceModuleDir, moduleDir, language, moduleSystem, jsExt);
705
712
  return;
706
713
  }
707
714
  }
@@ -731,21 +738,119 @@ async function generateModuleFiles(moduleName: string, moduleDir: string): Promi
731
738
  }
732
739
  }
733
740
 
741
+ /**
742
+ * Helper: Convert ESM syntax to CommonJS
743
+ */
744
+ function convertESMtoCommonJS(content: string): string {
745
+ return (
746
+ content
747
+ // Convert export class/function/const/let/var
748
+ .replace(/^export\s+class\s+/gm, 'class ')
749
+ .replace(/^export\s+function\s+/gm, 'function ')
750
+ .replace(/^export\s+const\s+/gm, 'const ')
751
+ .replace(/^export\s+let\s+/gm, 'let ')
752
+ .replace(/^export\s+var\s+/gm, 'var ')
753
+ // Convert named imports: import { a, b } from 'module'
754
+ .replace(
755
+ /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g,
756
+ "const { $1 } = require('$2')"
757
+ )
758
+ // Convert default imports: import name from 'module'
759
+ .replace(/import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/g, "const $1 = require('$2')")
760
+ // Convert mixed imports: import name, { a, b } from 'module'
761
+ .replace(
762
+ /import\s+(\w+)\s*,\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g,
763
+ "const $1 = require('$3');\nconst { $2 } = require('$3')"
764
+ )
765
+ // Add module.exports at the end for classes and main exports
766
+ .replace(/^(class\s+(\w+)\s+\{[\s\S]*?\n\})/gm, '$1\nmodule.exports.$2 = $2;')
767
+ // Handle export { ... }
768
+ .replace(/export\s*\{\s*([^}]+)\s*\}/g, (match, exports) => {
769
+ const items = exports
770
+ .split(',')
771
+ .map((item: string) => item.trim())
772
+ .filter(Boolean);
773
+ return items.map((item: string) => `module.exports.${item} = ${item};`).join('\n');
774
+ })
775
+ );
776
+ }
777
+
734
778
  /**
735
779
  * Helper: Copy module from source directory
736
780
  */
737
- async function copyModuleFromSource(sourceDir: string, targetDir: string): Promise<void> {
781
+ async function copyModuleFromSource(
782
+ sourceDir: string,
783
+ targetDir: string,
784
+ language: 'typescript' | 'javascript' = 'typescript',
785
+ moduleSystem: 'esm' | 'commonjs' = 'esm',
786
+ fileExtension: 'js' | 'cjs' = 'js'
787
+ ): Promise<void> {
738
788
  const entries = await fs.readdir(sourceDir, { withFileTypes: true });
739
789
 
740
790
  for (const entry of entries) {
741
791
  const sourcePath = path.join(sourceDir, entry.name);
742
- const targetPath = path.join(targetDir, entry.name);
792
+ let targetPath = path.join(targetDir, entry.name);
743
793
 
744
794
  if (entry.isDirectory()) {
745
795
  await fs.mkdir(targetPath, { recursive: true });
746
- await copyModuleFromSource(sourcePath, targetPath);
796
+ await copyModuleFromSource(sourcePath, targetPath, language, moduleSystem, fileExtension);
747
797
  } else {
748
- await fs.copyFile(sourcePath, targetPath);
798
+ // Convert file extension if generating JavaScript
799
+ if (language === 'javascript' && entry.name.endsWith('.ts')) {
800
+ const ext = `.${fileExtension}`;
801
+ targetPath = targetPath.replace(/\.ts$/, ext);
802
+
803
+ // Read, convert, and write the file
804
+ let content = await fs.readFile(sourcePath, 'utf-8');
805
+
806
+ // Basic TypeScript to JavaScript conversion
807
+ // Apply replacements multiple times to catch nested patterns
808
+ for (let i = 0; i < 3; i++) {
809
+ content = content
810
+ // Remove import type statements
811
+ .replace(/import\s+type\s+\{[^}]+\}\s+from\s+['"][^'"]+['"];?\s*\n/g, '')
812
+ // Remove type imports from regular imports
813
+ .replace(/import\s+\{([^}]+)\}\s+from/g, (match, imports) => {
814
+ const filtered = imports
815
+ .split(',')
816
+ .map((imp: string) => imp.trim())
817
+ .filter((imp: string) => !imp.startsWith('type '))
818
+ .join(', ');
819
+ return filtered ? `import {${filtered}} from` : '';
820
+ })
821
+ // Update import paths
822
+ .replace(/from\s+['"](.+?)\.js['"]/g, `from '$1${ext}'`)
823
+ // Remove 'private', 'public', 'protected', 'readonly' keywords
824
+ .replace(/\b(private|public|protected|readonly)\s+/g, '')
825
+ // Remove type annotations from function parameters and variables (multiple passes)
826
+ .replace(
827
+ /:\s*[A-Z]\w+(<[^>]+>)?(\[\])?(\s*[|&]\s*[A-Z]\w+(<[^>]+>)?(\[\])?)*(?=[,)\s=\n])/g,
828
+ ''
829
+ )
830
+ .replace(/(\w+)\s*:\s*[^,)=\n]+([,)])/g, '$1$2')
831
+ .replace(/(\w+)\s*:\s*[^=\n{]+(\s*=)/g, '$1$2')
832
+ // Remove return type annotations
833
+ .replace(/\)\s*:\s*[^{=\n]+\s*([{=])/g, ') $1')
834
+ // Remove interface and type definitions
835
+ .replace(/^export\s+(interface|type)\s+[^;]+;?\s*$/gm, '')
836
+ .replace(/^(interface|type)\s+[^;]+;?\s*$/gm, '')
837
+ // Remove type assertions (as Type)
838
+ .replace(/\s+as\s+\w+/g, '')
839
+ // Remove generic type parameters
840
+ .replace(/<[A-Z][\w,\s<>[\]|&]*>/g, '');
841
+ }
842
+ // Final cleanup
843
+ content = content.replace(/^\s*\n/gm, '');
844
+
845
+ // Convert to CommonJS if needed
846
+ if (moduleSystem === 'commonjs') {
847
+ content = convertESMtoCommonJS(content);
848
+ }
849
+
850
+ await fs.writeFile(targetPath, content, 'utf-8');
851
+ } else {
852
+ await fs.copyFile(sourcePath, targetPath);
853
+ }
749
854
  }
750
855
  }
751
856
  }
@@ -926,3 +1031,6 @@ async function performSmartMerge(
926
1031
 
927
1032
  InteractivePrompt.showMergeSummary(stats);
928
1033
  }
1034
+
1035
+ // Export helper functions for use in other commands
1036
+ export { generateModuleFiles, EnvManager, TemplateManager };
@@ -7,11 +7,13 @@ import chalk from 'chalk';
7
7
  import { execSync } from 'child_process';
8
8
  import { ensureDir, writeFile, error, warn } from '../utils/helpers.js';
9
9
  import { DryRunManager } from '../utils/dry-run.js';
10
+ import { generateModuleFiles } from './add-module.js';
10
11
 
11
12
  interface InitOptions {
12
13
  name: string;
13
14
  language: 'typescript' | 'javascript';
14
15
  moduleSystem: 'esm' | 'commonjs';
16
+ fileExtension: 'js' | 'cjs' | 'ts';
15
17
  database: 'postgresql' | 'mysql' | 'sqlite' | 'mongodb' | 'none';
16
18
  orm: 'prisma' | 'mongoose' | 'none';
17
19
  validator: 'zod' | 'joi' | 'yup';
@@ -54,26 +56,25 @@ export const initCommand = new Command('init')
54
56
  console.log(chalk.cyan('│') + ' ' + chalk.cyan('│'));
55
57
  console.log(
56
58
  chalk.cyan('│') +
57
- ' ' +
58
59
  chalk.bold.white('🚀 Servcraft') +
59
60
  chalk.gray(' - Project Generator') +
60
- ' ' +
61
+ ' ' +
61
62
  chalk.cyan('│')
62
63
  );
63
64
  console.log(
64
65
  chalk.cyan('│') +
65
- ' ' +
66
+ ' ' +
66
67
  chalk.gray('by ') +
67
68
  chalk.blue('Yao Logan') +
68
69
  chalk.gray(' (@Le-Sourcier)') +
69
- ' ' +
70
+ ' ' +
70
71
  chalk.cyan('│')
71
72
  );
72
73
  console.log(
73
74
  chalk.cyan('│') +
74
- ' ' +
75
+ ' ' +
75
76
  chalk.bgBlue.white(' in/yao-logan ') +
76
- ' ' +
77
+ ' ' +
77
78
  chalk.cyan('│')
78
79
  );
79
80
  console.log(chalk.cyan('│') + ' ' + chalk.cyan('│'));
@@ -84,17 +85,28 @@ export const initCommand = new Command('init')
84
85
 
85
86
  if (cmdOptions?.yes) {
86
87
  const db = (cmdOptions.db as InitOptions['database']) || 'postgresql';
88
+ const language = cmdOptions.javascript ? 'javascript' : 'typescript';
89
+ const moduleSystem = cmdOptions.commonjs ? 'commonjs' : 'esm';
90
+
91
+ // Determine file extension based on language and module system
92
+ let fileExtension: 'js' | 'cjs' | 'ts' = 'ts';
93
+ if (language === 'javascript') {
94
+ fileExtension = moduleSystem === 'commonjs' ? 'cjs' : 'js';
95
+ }
96
+
87
97
  options = {
88
98
  name: name || 'my-servcraft-app',
89
- language: cmdOptions.javascript ? 'javascript' : 'typescript',
90
- moduleSystem: cmdOptions.commonjs ? 'commonjs' : 'esm',
99
+ language,
100
+ moduleSystem,
101
+ fileExtension,
91
102
  database: db,
92
103
  orm: db === 'mongodb' ? 'mongoose' : db === 'none' ? 'none' : 'prisma',
93
104
  validator: 'zod',
94
105
  features: ['auth', 'users', 'email'],
95
106
  };
96
107
  } else {
97
- const answers = await inquirer.prompt([
108
+ // Step 1: Basic project info and language
109
+ const basicAnswers = await inquirer.prompt([
98
110
  {
99
111
  type: 'input',
100
112
  name: 'name',
@@ -117,16 +129,48 @@ export const initCommand = new Command('init')
117
129
  ],
118
130
  default: 'typescript',
119
131
  },
120
- {
121
- type: 'list',
122
- name: 'moduleSystem',
123
- message: '📦 Select module system:',
124
- choices: [
125
- { name: '✨ ESM (import/export) - Recommended', value: 'esm' },
126
- { name: ' CommonJS (require/module.exports)', value: 'commonjs' },
127
- ],
128
- default: 'esm',
129
- },
132
+ ]);
133
+
134
+ // Step 2: Module system and file extension (only for JavaScript)
135
+ let moduleSystem: 'esm' | 'commonjs' = 'esm';
136
+ let fileExtension: 'js' | 'cjs' | 'ts' = 'ts';
137
+
138
+ if (basicAnswers.language === 'javascript') {
139
+ const jsConfigAnswers = await inquirer.prompt([
140
+ {
141
+ type: 'list',
142
+ name: 'moduleSystem',
143
+ message: '📦 Select module syntax:',
144
+ choices: [
145
+ { name: '✨ ESM (import/export)', value: 'esm' },
146
+ { name: ' CommonJS (require/module.exports)', value: 'commonjs' },
147
+ ],
148
+ default: 'esm',
149
+ },
150
+ {
151
+ type: 'list',
152
+ name: 'fileExtension',
153
+ message: '📄 Select file extension:',
154
+ choices: (answers: { moduleSystem: string }) => {
155
+ if (answers.moduleSystem === 'esm') {
156
+ return [{ name: '✨ .js (standard for ESM)', value: 'js' }];
157
+ } else {
158
+ return [
159
+ { name: '✨ .cjs (explicit CommonJS)', value: 'cjs' },
160
+ { name: ' .js (CommonJS in .js)', value: 'js' },
161
+ ];
162
+ }
163
+ },
164
+ default: (answers: { moduleSystem: string }) =>
165
+ answers.moduleSystem === 'esm' ? 'js' : 'cjs',
166
+ },
167
+ ]);
168
+ moduleSystem = jsConfigAnswers.moduleSystem;
169
+ fileExtension = jsConfigAnswers.fileExtension;
170
+ }
171
+
172
+ // Step 3: Database, validator, and features
173
+ const restAnswers = await inquirer.prompt([
130
174
  {
131
175
  type: 'list',
132
176
  name: 'database',
@@ -161,11 +205,19 @@ export const initCommand = new Command('init')
161
205
  { name: 'Email Service', value: 'email', checked: true },
162
206
  { name: 'Audit Logs', value: 'audit', checked: false },
163
207
  { name: 'File Upload', value: 'upload', checked: false },
164
- { name: 'Redis Cache', value: 'redis', checked: false },
208
+ { name: 'Redis Cache', value: 'cache', checked: false },
165
209
  ],
166
210
  },
167
211
  ]);
168
212
 
213
+ // Combine all answers
214
+ const answers = {
215
+ ...basicAnswers,
216
+ moduleSystem,
217
+ fileExtension,
218
+ ...restAnswers,
219
+ };
220
+
169
221
  // Auto-determine ORM based on database choice
170
222
  const db = answers.database as InitOptions['database'];
171
223
  options = {
@@ -305,6 +357,30 @@ export const initCommand = new Command('init')
305
357
 
306
358
  spinner.succeed('Project files generated!');
307
359
 
360
+ // Install selected feature modules
361
+ if (options.features && options.features.length > 0 && !cmdOptions?.dryRun) {
362
+ const moduleSpinner = ora('Installing selected modules...').start();
363
+
364
+ try {
365
+ for (const feature of options.features) {
366
+ moduleSpinner.text = `Installing ${feature} module...`;
367
+ const moduleDir = path.join(projectDir, 'src/modules', feature);
368
+ await ensureDir(moduleDir);
369
+ await generateModuleFiles(
370
+ feature,
371
+ moduleDir,
372
+ options.language,
373
+ options.moduleSystem,
374
+ options.fileExtension
375
+ );
376
+ }
377
+ moduleSpinner.succeed(`${options.features.length} module(s) installed!`);
378
+ } catch (err) {
379
+ moduleSpinner.fail('Failed to install some modules');
380
+ warn(` Error: ${err instanceof Error ? err.message : 'Unknown error'}`);
381
+ }
382
+ }
383
+
308
384
  // Install dependencies (skip in dry-run mode)
309
385
  if (!cmdOptions?.dryRun) {
310
386
  const installSpinner = ora('Installing dependencies...').start();