spring-feature-cli 0.2.1
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/README.md +68 -0
- package/bin/index.js +3 -0
- package/package.json +29 -0
- package/src/actions/generateActions.js +61 -0
- package/src/commands/generate.js +21 -0
- package/src/index.js +14 -0
- package/src/templates/controller/Controller.hbs +10 -0
- package/src/templates/domain/Entity.hbs +26 -0
- package/src/templates/repository/Repository.hbs +8 -0
- package/src/templates/service/Service.hbs +8 -0
- package/src/utils/javaPackageResolver.js +18 -0
- package/src/utils/logger.js +8 -0
- package/src/utils/projectScanner.js +55 -0
- package/src/utils/runValidations.js +5 -0
- package/src/utils/templateCompiler.js +9 -0
- package/src/validations/generateValidations.js +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# spring-feature-cli
|
|
2
|
+
|
|
3
|
+
`spring-feature-cli` is a personal project to learn how to build command-line tools with Node.js. It is designed to generate basic Spring Boot artifacts using a feature-based structure and clean architecture principles.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
- Learn Node.js and the CLI ecosystem
|
|
8
|
+
- Practice using `commander`, `inquirer`, and `handlebars`
|
|
9
|
+
- Automate repetitive code generation for Spring Boot projects
|
|
10
|
+
- Generate entities, services, repositories, and controllers with a simple workflow
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Executable CLI via the `spfc` command
|
|
15
|
+
- Template generation using `handlebars`
|
|
16
|
+
- Input validation and project detection
|
|
17
|
+
- Supports generating:
|
|
18
|
+
- `Entity`
|
|
19
|
+
- `Repository`
|
|
20
|
+
- `Service`
|
|
21
|
+
- `Controller`
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
To install globally (optional):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Run the tool from the project directory:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
spfc generate
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or in development mode:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run dev
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Project Structure
|
|
50
|
+
|
|
51
|
+
- `bin/index.js`: executable CLI entry point
|
|
52
|
+
- `src/index.js`: main CLI logic
|
|
53
|
+
- `src/commands/generate.js`: generate command
|
|
54
|
+
- `src/actions/generateActions.js`: file creation actions
|
|
55
|
+
- `src/validations/generateValidations.js`: input validations
|
|
56
|
+
- `src/utils/`: utilities for Java package resolution, project scanning, and template compilation
|
|
57
|
+
- `src/templates/`: Handlebars templates for generated artifacts
|
|
58
|
+
|
|
59
|
+
## Key Dependencies
|
|
60
|
+
|
|
61
|
+
- `commander` for defining CLI commands
|
|
62
|
+
- `inquirer` for interactive prompts
|
|
63
|
+
- `handlebars` for template generation
|
|
64
|
+
- `chalk` for console styling
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
This project is licensed under `MIT`.
|
package/bin/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spring-feature-cli",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "CLI para generar features de Spring Boot",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"spfc": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"bin"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "node src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"spring",
|
|
18
|
+
"spring-boot",
|
|
19
|
+
"cli",
|
|
20
|
+
"generator"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"chalk": "^5.4.1",
|
|
25
|
+
"commander": "^13.1.0",
|
|
26
|
+
"handlebars": "^4.7.9",
|
|
27
|
+
"inquirer": "^12.1.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { runValidations } from '../utils/runValidations.js';
|
|
2
|
+
import { getMainClassDirectory, getRootPackage } from '../utils/javaPackageResolver.js'
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { renderTemplate } from '../utils/templateCompiler.js'
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import validations from '../validations/generateValidations.js';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
export function generateFeature(featureName) {
|
|
14
|
+
const javaPath = getMainClassDirectory();
|
|
15
|
+
const packageName = getRootPackage();
|
|
16
|
+
|
|
17
|
+
const input_context = { featureName, javaPath, packageName };
|
|
18
|
+
|
|
19
|
+
runValidations(validations, featureName);
|
|
20
|
+
|
|
21
|
+
generateFeatureStructure(input_context)
|
|
22
|
+
logger.success(`\n` + 'Feature created successfully');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generateFeatureStructure({ featureName, javaPath, packageName }) {
|
|
26
|
+
const templatesBasePath = path.join(__dirname, '..', 'templates');
|
|
27
|
+
const template_context = { featureName, packageName };
|
|
28
|
+
const structure = {
|
|
29
|
+
domain: {
|
|
30
|
+
featureName: `${featureName}.java`,
|
|
31
|
+
templatePath: path.join(templatesBasePath, 'domain', 'Entity.hbs')
|
|
32
|
+
},
|
|
33
|
+
service: {
|
|
34
|
+
featureName: `${featureName}Service.java`,
|
|
35
|
+
templatePath: path.join(templatesBasePath, 'service', 'Service.hbs')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
},
|
|
39
|
+
controller: {
|
|
40
|
+
featureName: `${featureName}Controller.java`,
|
|
41
|
+
templatePath: path.join(templatesBasePath, 'controller', 'Controller.hbs')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
},
|
|
45
|
+
repository: {
|
|
46
|
+
featureName: `${featureName}Repository.java`,
|
|
47
|
+
templatePath: path.join(templatesBasePath, 'repository', 'Repository.hbs')
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
for (const [folder, file] of Object.entries(structure)) {
|
|
53
|
+
const folderPath = path.join(javaPath, featureName, folder);
|
|
54
|
+
|
|
55
|
+
logger.info(`Creating ${folder}...`);
|
|
56
|
+
fs.mkdirSync(folderPath, { recursive: true });
|
|
57
|
+
|
|
58
|
+
const content = renderTemplate(file.templatePath, template_context);
|
|
59
|
+
fs.writeFileSync(path.join(folderPath, file.featureName), content);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { generateFeature } from '../actions/generateActions.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
const generate = new Command('generate');
|
|
6
|
+
|
|
7
|
+
generate
|
|
8
|
+
.description('Create a new feature (entity, service, controller)')
|
|
9
|
+
.alias('g')
|
|
10
|
+
.argument('<featureName>')
|
|
11
|
+
.action((featureName) => {
|
|
12
|
+
try {
|
|
13
|
+
const name = featureName?.trim();
|
|
14
|
+
generateFeature(name);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
logger.error(error.message);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export default generate;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import generateCommand from './commands/generate.js';
|
|
4
|
+
|
|
5
|
+
const program = new Command();
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name('spring-feature-cli')
|
|
9
|
+
.description('CLI tool to generate feature-based structure for Spring Boot applications, including entities, services and controllers, following clean architecture principles.')
|
|
10
|
+
.version('0.2.0');
|
|
11
|
+
|
|
12
|
+
program.addCommand(generateCommand);
|
|
13
|
+
|
|
14
|
+
program.parse();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
package {{packageName}}.{{featureName}}.controller;
|
|
2
|
+
|
|
3
|
+
import org.springframework.web.bind.annotation.RestController;
|
|
4
|
+
import org.springframework.web.bind.annotation.RequestMapping;
|
|
5
|
+
|
|
6
|
+
@RestController
|
|
7
|
+
@RequestMapping("/api/{{featureName}}")
|
|
8
|
+
public class {{featureName}}Controller{
|
|
9
|
+
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package {{packageName}}.{{featureName}}.domain;
|
|
2
|
+
|
|
3
|
+
import jakarta.persistence.Entity;
|
|
4
|
+
import jakarta.persistence.Id;
|
|
5
|
+
import jakarta.persistence.GeneratedValue;
|
|
6
|
+
import jakarta.persistence.GenerationType;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@Entity
|
|
10
|
+
public class {{featureName}} {
|
|
11
|
+
|
|
12
|
+
@Id
|
|
13
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
14
|
+
private Long id;
|
|
15
|
+
|
|
16
|
+
public {{featureName}}() {
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public Long getId() {
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public void setId(Long id) {
|
|
24
|
+
this.id = id;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
package {{packageName}}.{{featureName}}.repository;
|
|
2
|
+
|
|
3
|
+
import org.springframework.data.jpa.repository.JpaRepository;
|
|
4
|
+
import {{packageName}}.{{featureName}}.domain.{{featureName}};
|
|
5
|
+
|
|
6
|
+
public interface {{featureName}}Repository extends JpaRepository<{{featureName}}, Long> {
|
|
7
|
+
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getMainClassFile } from '../utils/projectScanner.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export function getMainClassDirectory() {
|
|
6
|
+
return path.dirname(getMainClassFile());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getRootPackage() {
|
|
10
|
+
const file = getMainClassFile();
|
|
11
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
12
|
+
|
|
13
|
+
const packageLine = content.split('\n').find(line => line.trim().startsWith('package'));
|
|
14
|
+
|
|
15
|
+
if (!packageLine) throw new Error("Root Package not found");
|
|
16
|
+
|
|
17
|
+
return packageLine.replace('package', '').replace(';', '').trim();
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
success: (msg) => console.log(`\n` + chalk.green(`${msg}`)),
|
|
5
|
+
error: (msg) => console.error(`\n` + chalk.red(`${msg}`)),
|
|
6
|
+
info: (msg) => console.log(chalk.cyan(`${msg}`)),
|
|
7
|
+
warn: (msg) => console.log(chalk.yellow(`${msg}`)),
|
|
8
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function getMainClassFile() {
|
|
5
|
+
const rootPath = findProjectRoot();
|
|
6
|
+
let current = path.join(rootPath, 'src', 'main', 'java');
|
|
7
|
+
|
|
8
|
+
const FilePath = searchInDirectory(current);
|
|
9
|
+
if(!FilePath) throw new Error('@SpringBootApplication not found');
|
|
10
|
+
return FilePath;
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function searchInDirectory(DirectoryPath) {
|
|
15
|
+
const files = fs.readdirSync(DirectoryPath);
|
|
16
|
+
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const filePath = path.join(DirectoryPath, file)
|
|
19
|
+
const stats = fs.statSync(filePath);
|
|
20
|
+
|
|
21
|
+
if (stats.isFile() && file.endsWith(".java")) {
|
|
22
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
23
|
+
|
|
24
|
+
if (content.includes('@SpringBootApplication')) {
|
|
25
|
+
return filePath;
|
|
26
|
+
}
|
|
27
|
+
} else if (stats.isDirectory()) {
|
|
28
|
+
const result = searchInDirectory(filePath);
|
|
29
|
+
if (result) return result;
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findProjectRoot(start = process.cwd()) {
|
|
37
|
+
let current = start;
|
|
38
|
+
|
|
39
|
+
while (current !== path.dirname(current)) {
|
|
40
|
+
if (isSpringProject(current)) return current;
|
|
41
|
+
current = path.dirname(current);
|
|
42
|
+
}
|
|
43
|
+
throw new Error('Project Home not found');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
function isSpringProject(projectPath) {
|
|
48
|
+
const pomPath = path.join(projectPath, 'pom.xml');
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(pomPath)) return false;
|
|
51
|
+
|
|
52
|
+
const pomContent = fs.readFileSync(pomPath, 'utf-8');
|
|
53
|
+
return pomContent.includes('spring-boot-starter');
|
|
54
|
+
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import Handlebars from 'handlebars';
|
|
3
|
+
|
|
4
|
+
export function renderTemplate(templatePath,context){
|
|
5
|
+
const templateSource = fs.readFileSync(templatePath, 'utf-8');
|
|
6
|
+
const template = Handlebars.compile(templateSource);
|
|
7
|
+
|
|
8
|
+
return template(context);
|
|
9
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getMainClassDirectory } from '../utils/javaPackageResolver.js'
|
|
4
|
+
|
|
5
|
+
const NotNullName = (name) => {
|
|
6
|
+
if (!name) throw new Error('Feature name is required');
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const minLengthName = (name) => {
|
|
10
|
+
if (name.length < 3) throw new Error('Feature name must be at least 3 characters');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const alphabeticalName = (name) => {
|
|
14
|
+
if (!name.match(/^[a-zA-Z]+$/)) throw new Error('Feature name must be alphabetical');
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const featureExists = (name) => {
|
|
18
|
+
const featurePath = path.join(getMainClassDirectory(), name);
|
|
19
|
+
|
|
20
|
+
if (fs.existsSync(featurePath)) throw new Error('Feature already exists');
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const validations = [
|
|
24
|
+
NotNullName,
|
|
25
|
+
minLengthName,
|
|
26
|
+
alphabeticalName,
|
|
27
|
+
featureExists
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export default validations;
|