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 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../src/index.js";
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,8 @@
1
+ package {{packageName}}.{{featureName}}.service;
2
+
3
+ import org.springframework.stereotype.Service;
4
+
5
+ @Service
6
+ public class {{featureName}}Service {
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,5 @@
1
+ export const runValidations = (validations, value) => {
2
+ for (const validation of validations) {
3
+ validation(value);
4
+ }
5
+ };
@@ -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;