eva4j 1.0.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/QUICK_REFERENCE.md +204 -0
  3. package/README.md +912 -0
  4. package/USAGE.md +349 -0
  5. package/bin/eva4j.js +234 -0
  6. package/config/defaults.json +46 -0
  7. package/package.json +57 -0
  8. package/src/commands/add-kafka-client.js +193 -0
  9. package/src/commands/add-module.js +221 -0
  10. package/src/commands/create.js +92 -0
  11. package/src/commands/detach.js +495 -0
  12. package/src/commands/generate-http-exchange.js +309 -0
  13. package/src/commands/generate-kafka-event.js +453 -0
  14. package/src/commands/generate-kafka-listener.js +267 -0
  15. package/src/commands/generate-resource.js +265 -0
  16. package/src/commands/generate-usecase.js +198 -0
  17. package/src/commands/info.js +63 -0
  18. package/src/generators/base-generator.js +150 -0
  19. package/src/generators/module-generator.js +48 -0
  20. package/src/generators/shared-generator.js +153 -0
  21. package/src/utils/config-manager.js +156 -0
  22. package/src/utils/context-builder.js +149 -0
  23. package/src/utils/naming.js +137 -0
  24. package/src/utils/template-engine.js +55 -0
  25. package/src/utils/validator.js +159 -0
  26. package/templates/base/application/Application.java.ejs +27 -0
  27. package/templates/base/docker/docker-compose.yml.ejs +41 -0
  28. package/templates/base/gradle/build.gradle.ejs +70 -0
  29. package/templates/base/gradle/settings.gradle.ejs +1 -0
  30. package/templates/base/resources/application-develop.yml.ejs +5 -0
  31. package/templates/base/resources/application-local.yml.ejs +5 -0
  32. package/templates/base/resources/application-production.yml.ejs +9 -0
  33. package/templates/base/resources/application-test.yml.ejs +5 -0
  34. package/templates/base/resources/application.yml.ejs +31 -0
  35. package/templates/base/resources/parameters/develop/cors.yml.ejs +4 -0
  36. package/templates/base/resources/parameters/develop/db.yaml.ejs +21 -0
  37. package/templates/base/resources/parameters/develop/kafka.yml.ejs +26 -0
  38. package/templates/base/resources/parameters/local/cors.yml.ejs +4 -0
  39. package/templates/base/resources/parameters/local/db.yaml.ejs +21 -0
  40. package/templates/base/resources/parameters/local/kafka.yml.ejs +26 -0
  41. package/templates/base/resources/parameters/production/cors.yml.ejs +4 -0
  42. package/templates/base/resources/parameters/production/db.yaml.ejs +21 -0
  43. package/templates/base/resources/parameters/production/kafka.yml.ejs +26 -0
  44. package/templates/base/root/README.md.ejs +126 -0
  45. package/templates/base/root/gitignore.ejs +42 -0
  46. package/templates/http-exchange/Adapter.java.ejs +39 -0
  47. package/templates/http-exchange/Config.java.ejs +24 -0
  48. package/templates/http-exchange/FeignClient.java.ejs +23 -0
  49. package/templates/http-exchange/Port.java.ejs +14 -0
  50. package/templates/kafka-event/Event.java.ejs +10 -0
  51. package/templates/kafka-event/KafkaConfigBean.java.ejs +7 -0
  52. package/templates/kafka-event/KafkaMessageBroker.java.ejs +32 -0
  53. package/templates/kafka-event/MessageBroker.java.ejs +8 -0
  54. package/templates/kafka-event/MessageBrokerImplMethod.java.ejs +9 -0
  55. package/templates/kafka-event/MessageBrokerMethod.java.ejs +1 -0
  56. package/templates/kafka-listener/KafkaController.java.ejs +34 -0
  57. package/templates/kafka-listener/ListenerMethod.java.ejs +9 -0
  58. package/templates/kafka-listener/ValueField.java.ejs +4 -0
  59. package/templates/module/controller.java.ejs +96 -0
  60. package/templates/module/exception.java.ejs +18 -0
  61. package/templates/module/mapper.java.ejs +67 -0
  62. package/templates/module/model.java.ejs +29 -0
  63. package/templates/module/package-info.java.ejs +7 -0
  64. package/templates/module/repository.java.ejs +26 -0
  65. package/templates/module/request-dto.java.ejs +24 -0
  66. package/templates/module/response-dto.java.ejs +26 -0
  67. package/templates/module/service-impl.java.ejs +112 -0
  68. package/templates/module/service.java.ejs +45 -0
  69. package/templates/module/update-dto.java.ejs +25 -0
  70. package/templates/resource/Command.java.ejs +9 -0
  71. package/templates/resource/CommandHandler.java.ejs +22 -0
  72. package/templates/resource/Controller.java.ejs +73 -0
  73. package/templates/resource/Query.java.ejs +12 -0
  74. package/templates/resource/QueryHandler.java.ejs +31 -0
  75. package/templates/resource/ResponseDto.java.ejs +6 -0
  76. package/templates/shared/annotations/ApplicationComponent.java.ejs +9 -0
  77. package/templates/shared/annotations/DomainComponent.java.ejs +9 -0
  78. package/templates/shared/annotations/LogAfter.java.ejs +11 -0
  79. package/templates/shared/annotations/LogBefore.java.ejs +11 -0
  80. package/templates/shared/annotations/LogExceptions.java.ejs +11 -0
  81. package/templates/shared/annotations/LogTimer.java.ejs +11 -0
  82. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +49 -0
  83. package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +56 -0
  84. package/templates/shared/configurations/securityConfig/SecurityConfig.java.ejs +57 -0
  85. package/templates/shared/configurations/swaggerConfig/SwaggerConfig.java.ejs +31 -0
  86. package/templates/shared/configurations/useCaseConfig/UseCaseAutoRegister.java.ejs +51 -0
  87. package/templates/shared/configurations/useCaseConfig/UseCaseConfig.java.ejs +25 -0
  88. package/templates/shared/configurations/useCaseConfig/UseCaseContainer.java.ejs +29 -0
  89. package/templates/shared/configurations/useCaseConfig/UseCaseMediator.java.ejs +38 -0
  90. package/templates/shared/customExceptions/BadRequestException.java.ejs +11 -0
  91. package/templates/shared/customExceptions/ConflictException.java.ejs +8 -0
  92. package/templates/shared/customExceptions/ForbiddenException.java.ejs +8 -0
  93. package/templates/shared/customExceptions/ImportFileException.java.ejs +6 -0
  94. package/templates/shared/customExceptions/NotFoundException.java.ejs +8 -0
  95. package/templates/shared/customExceptions/UnauthorizedException.java.ejs +9 -0
  96. package/templates/shared/customExceptions/ValidationException.java.ejs +17 -0
  97. package/templates/shared/errorMessage/ErrorMessage.java.ejs +5 -0
  98. package/templates/shared/errorMessage/FullErrorMessage.java.ejs +9 -0
  99. package/templates/shared/errorMessage/ShortErrorMessage.java.ejs +6 -0
  100. package/templates/shared/eventEnvelope/EventEnvelope.java.ejs +13 -0
  101. package/templates/shared/eventEnvelope/EventMetadata.java.ejs +24 -0
  102. package/templates/shared/filters/CorrelationIdFilter.java.ejs +45 -0
  103. package/templates/shared/handlerException/HandlerExceptions.java.ejs +148 -0
  104. package/templates/shared/interfaces/Command.java.ejs +4 -0
  105. package/templates/shared/interfaces/CommandHandler.java.ejs +5 -0
  106. package/templates/shared/interfaces/Dispatchable.java.ejs +4 -0
  107. package/templates/shared/interfaces/Handler.java.ejs +4 -0
  108. package/templates/shared/interfaces/Query.java.ejs +4 -0
  109. package/templates/shared/interfaces/QueryHandler.java.ejs +5 -0
  110. package/templates/shared/package-info.java.ejs +8 -0
  111. package/templates/usecase/command/Command.java.ejs +7 -0
  112. package/templates/usecase/command/CommandHandler.java.ejs +21 -0
  113. package/templates/usecase/query/Query.java.ejs +10 -0
  114. package/templates/usecase/query/QueryHandler.java.ejs +22 -0
  115. package/templates/usecase/query/ResponseDto.java.ejs +5 -0
@@ -0,0 +1,156 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ class ConfigManager {
5
+ constructor(projectPath = process.cwd()) {
6
+ this.projectPath = projectPath;
7
+ this.configFile = path.join(projectPath, '.eva4j.json');
8
+ }
9
+
10
+ /**
11
+ * Save project configuration to .eva4j.json
12
+ * @param {Object} config - Project configuration
13
+ * @param {string} config.projectName - Name of the project
14
+ * @param {string} config.groupId - Maven group ID
15
+ * @param {string} config.artifactId - Maven artifact ID
16
+ * @param {string} config.packageName - Base package name
17
+ * @param {string} config.javaVersion - Java version
18
+ * @param {string} config.springBootVersion - Spring Boot version
19
+ * @param {string} config.springModulithVersion - Spring Modulith version
20
+ * @param {Array<string>} config.dependencies - Selected dependencies
21
+ * @param {string} config.createdAt - ISO timestamp of creation
22
+ */
23
+ async saveProjectConfig(config) {
24
+ const projectConfig = {
25
+ projectName: config.projectName,
26
+ groupId: config.groupId,
27
+ artifactId: config.artifactId,
28
+ packageName: config.packageName,
29
+ javaVersion: config.javaVersion,
30
+ springBootVersion: config.springBootVersion,
31
+ springModulithVersion: config.springModulithVersion,
32
+ dependencies: config.dependencies || [],
33
+ databaseType: config.databaseType,
34
+ modules: [],
35
+ createdAt: config.createdAt || new Date().toISOString(),
36
+ updatedAt: new Date().toISOString()
37
+ };
38
+
39
+ await fs.writeJson(this.configFile, projectConfig, { spaces: 2 });
40
+ return projectConfig;
41
+ }
42
+
43
+ /**
44
+ * Load project configuration from .eva4j.json
45
+ * @returns {Object|null} Project configuration or null if not found
46
+ */
47
+ async loadProjectConfig() {
48
+ try {
49
+ if (await fs.pathExists(this.configFile)) {
50
+ return await fs.readJson(this.configFile);
51
+ }
52
+ return null;
53
+ } catch (error) {
54
+ console.error('Error loading project configuration:', error.message);
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Add a module to the project configuration
61
+ * @param {string} moduleName - Name of the module
62
+ * @param {Object} options - Module options
63
+ */
64
+ async addModule(moduleName, options = {}) {
65
+ const config = await this.loadProjectConfig();
66
+ if (!config) {
67
+ throw new Error('Project configuration not found. Are you in an eva4j project?');
68
+ }
69
+
70
+ const module = {
71
+ name: moduleName,
72
+ createdAt: new Date().toISOString(),
73
+ ...options
74
+ };
75
+
76
+ config.modules.push(module);
77
+ config.updatedAt = new Date().toISOString();
78
+
79
+ await fs.writeJson(this.configFile, config, { spaces: 2 });
80
+ return config;
81
+ }
82
+
83
+ /**
84
+ * Check if a module exists in the project
85
+ * @param {string} moduleName - Name of the module to check
86
+ * @returns {boolean} True if module exists
87
+ */
88
+ async moduleExists(moduleName) {
89
+ const config = await this.loadProjectConfig();
90
+ if (!config) return false;
91
+
92
+ return config.modules.some(module => module.name === moduleName);
93
+ }
94
+
95
+ /**
96
+ * Get all modules from the project configuration
97
+ * @returns {Array} List of modules
98
+ */
99
+ async getModules() {
100
+ const config = await this.loadProjectConfig();
101
+ return config ? config.modules : [];
102
+ }
103
+
104
+ /**
105
+ * Check if .eva4j.json exists in the project
106
+ * @returns {boolean} True if config file exists
107
+ */
108
+ async exists() {
109
+ return await fs.pathExists(this.configFile);
110
+ }
111
+
112
+ /**
113
+ * Get the full project configuration path
114
+ * @returns {string} Path to .eva4j.json
115
+ */
116
+ getConfigPath() {
117
+ return this.configFile;
118
+ }
119
+
120
+ /**
121
+ * Check if a feature exists in the project
122
+ * @param {string} featureName - Name of the feature to check
123
+ * @returns {boolean} True if feature exists
124
+ */
125
+ async featureExists(featureName) {
126
+ const config = await this.loadProjectConfig();
127
+ if (!config || !config.features) return false;
128
+
129
+ return config.features.includes(featureName);
130
+ }
131
+
132
+ /**
133
+ * Add a feature to the project configuration
134
+ * @param {string} featureName - Name of the feature to add
135
+ */
136
+ async addFeature(featureName) {
137
+ const config = await this.loadProjectConfig();
138
+ if (!config) {
139
+ throw new Error('Project configuration not found. Are you in an eva4j project?');
140
+ }
141
+
142
+ if (!config.features) {
143
+ config.features = [];
144
+ }
145
+
146
+ if (!config.features.includes(featureName)) {
147
+ config.features.push(featureName);
148
+ config.updatedAt = new Date().toISOString();
149
+ await fs.writeJson(this.configFile, config, { spaces: 2 });
150
+ }
151
+
152
+ return config;
153
+ }
154
+ }
155
+
156
+ module.exports = ConfigManager;
@@ -0,0 +1,149 @@
1
+ const defaults = require('../../config/defaults.json');
2
+ const {
3
+ toPackagePath,
4
+ getApplicationClassName,
5
+ getFullPackageName,
6
+ toPascalCase,
7
+ toSnakeCase,
8
+ pluralizeWord,
9
+ getBaseEntity
10
+ } = require('./naming');
11
+
12
+ /**
13
+ * Build context for base project generation
14
+ */
15
+ function buildBaseContext(answers) {
16
+ const packageName = getFullPackageName(answers.groupId, answers.artifactId);
17
+ const packagePath = toPackagePath(packageName);
18
+ const applicationClassName = getApplicationClassName(answers.artifactId);
19
+
20
+ const context = {
21
+ projectName: answers.projectName || answers.artifactId,
22
+ artifactId: answers.artifactId,
23
+ groupId: answers.groupId,
24
+ version: defaults.version,
25
+ packageName,
26
+ packagePath,
27
+ applicationClassName,
28
+ javaVersion: answers.javaVersion || defaults.javaVersion,
29
+ springBootVersion: answers.springBootVersion || defaults.springBootVersion,
30
+ dependencyManagementVersion: defaults.dependencyManagementVersion,
31
+ springModulithVersion: defaults.springModulithVersion,
32
+ springCloudVersion: defaults.springCloudVersion,
33
+ gradleVersion: defaults.gradleVersion,
34
+ dependencies: answers.dependencies || [],
35
+ author: answers.author || 'Generated by eva4j',
36
+ createdDate: new Date().toISOString().split('T')[0],
37
+ license: 'MIT',
38
+ description: answers.description || `Spring Boot application: ${answers.artifactId}`,
39
+ ...buildDatabaseContext(answers),
40
+ ...buildServerContext(answers),
41
+ features: {
42
+ ...defaults.features,
43
+ includeAudit: answers.dependencies?.includes('data-jpa'),
44
+ ...answers.features
45
+ },
46
+ testing: defaults.testing,
47
+ loggingLevel: defaults.logging.level
48
+ };
49
+
50
+ return context;
51
+ }
52
+
53
+ /**
54
+ * Build database configuration context
55
+ */
56
+ function buildDatabaseContext(answers) {
57
+ if (!answers.dependencies?.includes('data-jpa')) {
58
+ return {};
59
+ }
60
+
61
+ const databaseType = answers.databaseType || 'h2';
62
+ const databaseName = answers.artifactId.replace(/-/g, '_');
63
+
64
+ const dbConfig = {
65
+ h2: {
66
+ driver: 'com.h2.database:h2',
67
+ driverClass: 'org.h2.Driver',
68
+ url: `jdbc:h2:mem:${databaseName}`,
69
+ username: 'sa',
70
+ password: '',
71
+ hibernateDialect: 'org.hibernate.dialect.H2Dialect',
72
+ testcontainer: 'h2'
73
+ },
74
+ postgresql: {
75
+ driver: 'org.postgresql:postgresql',
76
+ driverClass: 'org.postgresql.Driver',
77
+ url: `jdbc:postgresql://localhost:5432/${databaseName}`,
78
+ username: answers.databaseUsername || 'postgres',
79
+ password: answers.databasePassword || 'postgres',
80
+ hibernateDialect: 'org.hibernate.dialect.PostgreSQLDialect',
81
+ testcontainer: 'postgresql'
82
+ },
83
+ mysql: {
84
+ driver: 'com.mysql:mysql-connector-j',
85
+ driverClass: 'com.mysql.cj.jdbc.Driver',
86
+ url: `jdbc:mysql://localhost:3306/${databaseName}`,
87
+ username: answers.databaseUsername || 'root',
88
+ password: answers.databasePassword || 'root',
89
+ hibernateDialect: 'org.hibernate.dialect.MySQLDialect',
90
+ testcontainer: 'mysql'
91
+ }
92
+ };
93
+
94
+ const config = dbConfig[databaseType];
95
+
96
+ return {
97
+ databaseType,
98
+ databaseName,
99
+ databaseDriver: config.driver,
100
+ databaseDriverClass: config.driverClass,
101
+ databaseUrl: config.url,
102
+ databaseUsername: config.username,
103
+ databasePassword: config.password,
104
+ hibernateDialect: config.hibernateDialect,
105
+ databaseTestcontainer: config.testcontainer,
106
+ ddlAuto: defaults.database.ddlAuto,
107
+ showSql: defaults.database.showSql
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Build server configuration context
113
+ */
114
+ function buildServerContext(answers) {
115
+ return {
116
+ serverPort: answers.serverPort || defaults.server.port,
117
+ contextPath: answers.contextPath || defaults.server.contextPath
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Build context for module generation
123
+ */
124
+ function buildModuleContext(baseContext, moduleName, moduleOptions = {}) {
125
+ const className = toPascalCase(moduleName);
126
+ const tableName = toSnakeCase(pluralizeWord(moduleName));
127
+ const moduleNamePlural = pluralizeWord(moduleName);
128
+
129
+ const hasSoftDelete = moduleOptions.hasSoftDelete ?? defaults.module.hasSoftDelete;
130
+ const hasAudit = moduleOptions.hasAudit ?? defaults.module.hasAudit;
131
+ const baseEntityClass = getBaseEntity(hasSoftDelete, hasAudit);
132
+
133
+ return {
134
+ ...baseContext,
135
+ moduleName,
136
+ moduleNamePlural,
137
+ className,
138
+ tableName,
139
+ hasSoftDelete,
140
+ hasAudit,
141
+ baseEntityClass,
142
+ generateTests: moduleOptions.generateTests ?? defaults.module.generateTests
143
+ };
144
+ }
145
+
146
+ module.exports = {
147
+ buildBaseContext,
148
+ buildModuleContext
149
+ };
@@ -0,0 +1,137 @@
1
+ const pluralize = require('pluralize');
2
+
3
+ /**
4
+ * Convert string to PascalCase
5
+ * @param {string} str - Input string
6
+ * @returns {string} PascalCase string
7
+ */
8
+ function toPascalCase(str) {
9
+ return str
10
+ .replace(/[-_\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : ''))
11
+ .replace(/^(.)/, (char) => char.toUpperCase());
12
+ }
13
+
14
+ /**
15
+ * Convert string to camelCase
16
+ * @param {string} str - Input string
17
+ * @returns {string} camelCase string
18
+ */
19
+ function toCamelCase(str) {
20
+ const pascal = toPascalCase(str);
21
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
22
+ }
23
+
24
+ /**
25
+ * Convert string to snake_case
26
+ * @param {string} str - Input string
27
+ * @returns {string} snake_case string
28
+ */
29
+ function toSnakeCase(str) {
30
+ return str
31
+ .replace(/([A-Z])/g, '_$1')
32
+ .replace(/[-\s]+/g, '_')
33
+ .replace(/^_/, '')
34
+ .toLowerCase();
35
+ }
36
+
37
+ /**
38
+ * Convert string to kebab-case
39
+ * @param {string} str - Input string
40
+ * @returns {string} kebab-case string
41
+ */
42
+ function toKebabCase(str) {
43
+ return str
44
+ .replace(/([A-Z])/g, '-$1')
45
+ .replace(/[\s_]+/g, '-')
46
+ .replace(/^-/, '')
47
+ .toLowerCase();
48
+ }
49
+
50
+ /**
51
+ * Pluralize a word
52
+ * @param {string} word - Word to pluralize
53
+ * @returns {string} Pluralized word
54
+ */
55
+ function pluralizeWord(word) {
56
+ return pluralize(word);
57
+ }
58
+
59
+ /**
60
+ * Convert package name to path
61
+ * @param {string} packageName - Package name (e.g., com.company.project)
62
+ * @returns {string} Package path (e.g., com/company/project)
63
+ */
64
+ function toPackagePath(packageName) {
65
+ return packageName.replace(/\./g, '/');
66
+ }
67
+
68
+ /**
69
+ * Get base entity class based on flags
70
+ * @param {boolean} hasSoftDelete - Whether to use soft delete
71
+ * @param {boolean} hasAudit - Whether to use audit fields
72
+ * @returns {string} Base entity class name
73
+ */
74
+ function getBaseEntity(hasSoftDelete, hasAudit) {
75
+ if (hasSoftDelete) {
76
+ return 'SoftDeletableEntity';
77
+ }
78
+ if (hasAudit) {
79
+ return 'AuditableEntity';
80
+ }
81
+ return 'BaseEntity';
82
+ }
83
+
84
+ /**
85
+ * Convert artifact ID to valid Java package name
86
+ * @param {string} artifactId - Artifact ID (e.g., my-project)
87
+ * @returns {string} Valid package name (e.g., myproject)
88
+ */
89
+ function artifactIdToPackageName(artifactId) {
90
+ return artifactId
91
+ .toLowerCase()
92
+ .replace(/[^a-z0-9]/g, '');
93
+ }
94
+
95
+ /**
96
+ * Generate application class name from artifact ID
97
+ * @param {string} artifactId - Artifact ID (e.g., my-project)
98
+ * @returns {string} Application class name (e.g., MyProjectApplication)
99
+ */
100
+ function getApplicationClassName(artifactId) {
101
+ return toPascalCase(artifactId) + 'Application';
102
+ }
103
+
104
+ /**
105
+ * Get full package name
106
+ * @param {string} groupId - Group ID (e.g., com.company)
107
+ * @param {string} artifactId - Artifact ID (e.g., my-project)
108
+ * @returns {string} Full package name (e.g., com.company.myproject)
109
+ */
110
+ function getFullPackageName(groupId, artifactId) {
111
+ const packagePart = artifactIdToPackageName(artifactId);
112
+ return `${groupId}.${packagePart}`;
113
+ }
114
+
115
+ /**
116
+ * Check if a use case name represents an "all" type query that returns a list
117
+ * @param {string} usecaseName - Use case name (e.g., FindAllUsers, GetAllProducts)
118
+ * @returns {boolean} True if the use case returns a list
119
+ */
120
+ function isAllTypeQuery(usecaseName) {
121
+ const allPatterns = ['FindAll', 'GetAll', 'ListAll', 'SearchAll', 'RetrieveAll'];
122
+ return allPatterns.some(pattern => usecaseName.startsWith(pattern));
123
+ }
124
+
125
+ module.exports = {
126
+ toPascalCase,
127
+ toCamelCase,
128
+ toSnakeCase,
129
+ toKebabCase,
130
+ pluralizeWord,
131
+ toPackagePath,
132
+ getBaseEntity,
133
+ artifactIdToPackageName,
134
+ getApplicationClassName,
135
+ getFullPackageName,
136
+ isAllTypeQuery
137
+ };
@@ -0,0 +1,55 @@
1
+ const ejs = require('ejs');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Render a template file with given context
7
+ * @param {string} templatePath - Path to template file
8
+ * @param {object} context - Template variables
9
+ * @returns {Promise<string>} Rendered template
10
+ */
11
+ async function renderTemplate(templatePath, context) {
12
+ const templateContent = await fs.readFile(templatePath, 'utf-8');
13
+ return ejs.render(templateContent, context);
14
+ }
15
+
16
+ /**
17
+ * Render and write template to destination
18
+ * @param {string} templatePath - Path to template file
19
+ * @param {string} destPath - Destination file path
20
+ * @param {object} context - Template variables
21
+ */
22
+ async function renderAndWrite(templatePath, destPath, context) {
23
+ const content = await renderTemplate(templatePath, context);
24
+ await fs.ensureDir(path.dirname(destPath));
25
+ await fs.writeFile(destPath, content, 'utf-8');
26
+ }
27
+
28
+ /**
29
+ * Get template file paths recursively
30
+ * @param {string} dir - Directory to scan
31
+ * @param {Array<string>} fileList - Accumulated file list
32
+ * @returns {Promise<Array<string>>} List of template file paths
33
+ */
34
+ async function getTemplateFiles(dir, fileList = []) {
35
+ const files = await fs.readdir(dir);
36
+
37
+ for (const file of files) {
38
+ const filePath = path.join(dir, file);
39
+ const stat = await fs.stat(filePath);
40
+
41
+ if (stat.isDirectory()) {
42
+ await getTemplateFiles(filePath, fileList);
43
+ } else if (file.endsWith('.ejs')) {
44
+ fileList.push(filePath);
45
+ }
46
+ }
47
+
48
+ return fileList;
49
+ }
50
+
51
+ module.exports = {
52
+ renderTemplate,
53
+ renderAndWrite,
54
+ getTemplateFiles
55
+ };
@@ -0,0 +1,159 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Validate project name
6
+ * @param {string} name - Project name
7
+ * @returns {boolean|string} True if valid, error message otherwise
8
+ */
9
+ function validateProjectName(name) {
10
+ if (!name || name.trim() === '') {
11
+ return 'Project name cannot be empty';
12
+ }
13
+
14
+ if (!/^[a-z0-9-]+$/.test(name)) {
15
+ return 'Project name must contain only lowercase letters, numbers, and hyphens';
16
+ }
17
+
18
+ if (name.startsWith('-') || name.endsWith('-')) {
19
+ return 'Project name cannot start or end with a hyphen';
20
+ }
21
+
22
+ if (name.length < 2) {
23
+ return 'Project name must be at least 2 characters long';
24
+ }
25
+
26
+ return true;
27
+ }
28
+
29
+ /**
30
+ * Validate group ID (Java package naming convention)
31
+ * @param {string} groupId - Group ID
32
+ * @returns {boolean|string} True if valid, error message otherwise
33
+ */
34
+ function validateGroupId(groupId) {
35
+ if (!groupId || groupId.trim() === '') {
36
+ return 'Group ID cannot be empty';
37
+ }
38
+
39
+ const parts = groupId.split('.');
40
+
41
+ if (parts.length < 2) {
42
+ return 'Group ID must have at least 2 parts (e.g., com.company)';
43
+ }
44
+
45
+ for (const part of parts) {
46
+ if (!/^[a-z][a-z0-9]*$/.test(part)) {
47
+ return 'Each part of Group ID must start with a lowercase letter and contain only lowercase letters and numbers';
48
+ }
49
+ }
50
+
51
+ return true;
52
+ }
53
+
54
+ /**
55
+ * Validate module name
56
+ * @param {string} name - Module name
57
+ * @returns {boolean|string} True if valid, error message otherwise
58
+ */
59
+ function validateModuleName(name) {
60
+ if (!name || name.trim() === '') {
61
+ return 'Module name cannot be empty';
62
+ }
63
+
64
+ if (!/^[a-z][a-z0-9]*$/.test(name)) {
65
+ return 'Module name must start with a lowercase letter and contain only lowercase letters and numbers';
66
+ }
67
+
68
+ if (name.length < 2) {
69
+ return 'Module name must be at least 2 characters long';
70
+ }
71
+
72
+ // Reserved names
73
+ const reserved = ['common', 'shared', 'config', 'util', 'test'];
74
+ if (reserved.includes(name)) {
75
+ return `Module name '${name}' is reserved and cannot be used`;
76
+ }
77
+
78
+ return true;
79
+ }
80
+
81
+ /**
82
+ * Check if current directory is a valid eva4j project
83
+ * @param {string} dir - Directory path
84
+ * @returns {boolean} True if valid project
85
+ */
86
+ async function isEva4jProject(dir) {
87
+ const buildGradlePath = path.join(dir, 'build.gradle');
88
+
89
+ if (!await fs.pathExists(buildGradlePath)) {
90
+ return false;
91
+ }
92
+
93
+ // Check if build.gradle contains Spring Boot plugin
94
+ const content = await fs.readFile(buildGradlePath, 'utf-8');
95
+ return content.includes('org.springframework.boot');
96
+ }
97
+
98
+ /**
99
+ * Check if module already exists
100
+ * @param {string} projectDir - Project directory
101
+ * @param {string} packagePath - Package path
102
+ * @param {string} moduleName - Module name
103
+ * @returns {boolean} True if module exists
104
+ */
105
+ async function moduleExists(projectDir, packagePath, moduleName) {
106
+ const modulePath = path.join(projectDir, 'src', 'main', 'java', packagePath, moduleName);
107
+ return await fs.pathExists(modulePath);
108
+ }
109
+
110
+ /**
111
+ * Check if shared module exists
112
+ * @param {string} projectDir - Project directory
113
+ * @param {string} packagePath - Package path
114
+ * @returns {boolean} True if shared module exists
115
+ */
116
+ async function sharedModuleExists(projectDir, packagePath) {
117
+ const sharedPath = path.join(projectDir, 'src', 'main', 'java', packagePath, 'shared');
118
+ return await fs.pathExists(sharedPath);
119
+ }
120
+
121
+ /**
122
+ * Validate Java version
123
+ * @param {number} version - Java version
124
+ * @returns {boolean} True if valid
125
+ */
126
+ function validateJavaVersion(version) {
127
+ const validVersions = [21, 22, 23];
128
+ return validVersions.includes(version);
129
+ }
130
+
131
+ /**
132
+ * Validate port number
133
+ * @param {number} port - Port number
134
+ * @returns {boolean|string} True if valid, error message otherwise
135
+ */
136
+ function validatePort(port) {
137
+ const portNum = parseInt(port, 10);
138
+
139
+ if (isNaN(portNum)) {
140
+ return 'Port must be a number';
141
+ }
142
+
143
+ if (portNum < 1024 || portNum > 65535) {
144
+ return 'Port must be between 1024 and 65535';
145
+ }
146
+
147
+ return true;
148
+ }
149
+
150
+ module.exports = {
151
+ validateProjectName,
152
+ validateGroupId,
153
+ validateModuleName,
154
+ isEva4jProject,
155
+ moduleExists,
156
+ sharedModuleExists,
157
+ validateJavaVersion,
158
+ validatePort
159
+ };
@@ -0,0 +1,27 @@
1
+ package <%= packageName %>;
2
+
3
+ import org.springframework.boot.SpringApplication;
4
+ import org.springframework.boot.autoconfigure.SpringBootApplication;
5
+ import org.springframework.cloud.openfeign.EnableFeignClients;
6
+ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
7
+ <% if (features.enableScheduling) { %>import org.springframework.scheduling.annotation.EnableScheduling;
8
+ <% } %><% if (features.enableAsync) { %>import org.springframework.scheduling.annotation.EnableAsync;
9
+ <% } %>
10
+ /**
11
+ * Main application class for <%= projectName %>
12
+ *
13
+ * @author <%= author %>
14
+ * @version <%= version %>
15
+ * @since <%= createdDate %>
16
+ */
17
+ @SpringBootApplication
18
+ @EnableFeignClients
19
+ @EnableJpaAuditing
20
+ <% if (features.enableScheduling) { %>@EnableScheduling
21
+ <% } %><% if (features.enableAsync) { %>@EnableAsync
22
+ <% } %>public class <%= applicationClassName %> {
23
+
24
+ public static void main(String[] args) {
25
+ SpringApplication.run(<%= applicationClassName %>.class, args);
26
+ }
27
+ }