@0kmpo/openapi-clean-arch-generator 1.3.10
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/.gitea/workflows/lint.yaml +41 -0
- package/.gitea/workflows/publish.yml +105 -0
- package/.openapi-generator-ignore +33 -0
- package/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/main.js +180 -0
- package/eslint.config.js +33 -0
- package/example-swagger.yaml +150 -0
- package/generation-config.json +24 -0
- package/main.ts +233 -0
- package/openapitools.json +23 -0
- package/package.json +70 -0
- package/src/generators/clean-arch.generator.ts +537 -0
- package/src/generators/dto.generator.ts +126 -0
- package/src/generators/lint.generator.ts +124 -0
- package/src/generators/report.generator.ts +80 -0
- package/src/swagger/analyzer.ts +32 -0
- package/src/types/cli.types.ts +36 -0
- package/src/types/generation.types.ts +50 -0
- package/src/types/index.ts +8 -0
- package/src/types/openapi.types.ts +126 -0
- package/src/types/swagger.types.ts +9 -0
- package/src/utils/config.ts +118 -0
- package/src/utils/environment-finder.ts +53 -0
- package/src/utils/filesystem.ts +31 -0
- package/src/utils/logger.ts +60 -0
- package/src/utils/mock-value-resolver.ts +70 -0
- package/src/utils/name-formatter.ts +12 -0
- package/src/utils/openapi-generator.ts +24 -0
- package/src/utils/prompt.ts +183 -0
- package/src/utils/type-mapper.ts +14 -0
- package/templates/api.repository.contract.mustache +34 -0
- package/templates/api.repository.impl.mock.mustache +21 -0
- package/templates/api.repository.impl.mustache +58 -0
- package/templates/api.repository.impl.spec.mustache +97 -0
- package/templates/api.use-cases.contract.mustache +34 -0
- package/templates/api.use-cases.impl.mustache +32 -0
- package/templates/api.use-cases.impl.spec.mustache +94 -0
- package/templates/api.use-cases.mock.mustache +21 -0
- package/templates/dto.mock.mustache +16 -0
- package/templates/mapper.mustache +28 -0
- package/templates/mapper.spec.mustache +39 -0
- package/templates/model-entity.mustache +24 -0
- package/templates/model-entity.spec.mustache +34 -0
- package/templates/model.mock.mustache +14 -0
- package/templates/model.mustache +20 -0
- package/templates/repository.provider.mock.mustache +20 -0
- package/templates/repository.provider.mustache +26 -0
- package/templates/use-cases.provider.mock.mustache +20 -0
- package/templates/use-cases.provider.mustache +26 -0
- package/tsconfig.json +17 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const mustache_1 = __importDefault(require("mustache"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const logger_1 = require("./src/utils/logger");
|
|
12
|
+
const openapi_generator_1 = require("./src/utils/openapi-generator");
|
|
13
|
+
const filesystem_1 = require("./src/utils/filesystem");
|
|
14
|
+
const analyzer_1 = require("./src/swagger/analyzer");
|
|
15
|
+
const dto_generator_1 = require("./src/generators/dto.generator");
|
|
16
|
+
const clean_arch_generator_1 = require("./src/generators/clean-arch.generator");
|
|
17
|
+
const report_generator_1 = require("./src/generators/report.generator");
|
|
18
|
+
const lint_generator_1 = require("./src/generators/lint.generator");
|
|
19
|
+
const environment_finder_1 = require("./src/utils/environment-finder");
|
|
20
|
+
const prompt_1 = require("./src/utils/prompt");
|
|
21
|
+
const config_1 = require("./src/utils/config");
|
|
22
|
+
const package_json_1 = __importDefault(require("./package.json"));
|
|
23
|
+
// Disable HTML escaping so that < and > produce valid TypeScript generic types.
|
|
24
|
+
mustache_1.default.escape = function (text) {
|
|
25
|
+
return text;
|
|
26
|
+
};
|
|
27
|
+
// ── CLI CONFIGURATION ────────────────────────────────────────────────────────
|
|
28
|
+
commander_1.program
|
|
29
|
+
.name('generate-clean-arch')
|
|
30
|
+
.description('Angular Clean Architecture code generator from OpenAPI/Swagger')
|
|
31
|
+
.version(package_json_1.default.version)
|
|
32
|
+
.option('-i, --input <file>', 'OpenAPI/Swagger file (yaml or json)', 'swagger.yaml')
|
|
33
|
+
.option('-o, --output <dir>', 'Output directory', './src/app')
|
|
34
|
+
.option('-t, --templates <dir>', 'Custom templates directory', path_1.default.join(__dirname, 'templates'))
|
|
35
|
+
.option('--skip-install', 'Skip dependency installation')
|
|
36
|
+
.option('--dry-run', 'Simulate without generating files')
|
|
37
|
+
.option('--skip-lint', 'Skip post-generation linting and formatting')
|
|
38
|
+
.option('-s, --select-endpoints', 'Interactively select which tags and endpoints to generate')
|
|
39
|
+
.option('-c, --config <file>', 'Use a JSON configuration file (skips interactive prompts)')
|
|
40
|
+
.option('--init-config [file]', 'Generate a JSON configuration file instead of generating code')
|
|
41
|
+
.parse(process.argv);
|
|
42
|
+
const options = commander_1.program.opts();
|
|
43
|
+
// ── MAIN ORCHESTRATOR ────────────────────────────────────────────────────────
|
|
44
|
+
async function main() {
|
|
45
|
+
console.log('\n' + '='.repeat(60));
|
|
46
|
+
(0, logger_1.log)(' OpenAPI Clean Architecture Generator', 'bright');
|
|
47
|
+
(0, logger_1.log)(' Angular + Clean Architecture Code Generator', 'cyan');
|
|
48
|
+
console.log('='.repeat(60) + '\n');
|
|
49
|
+
const logPath = path_1.default.join(process.cwd(), 'generation.log');
|
|
50
|
+
(0, logger_1.initGenerationLog)(logPath);
|
|
51
|
+
// ── CONFIG FILE: override CLI defaults with config values ─────────────────
|
|
52
|
+
const configFile = options.config;
|
|
53
|
+
const generationConfig = configFile ? (0, config_1.loadConfig)(configFile) : undefined;
|
|
54
|
+
if (generationConfig) {
|
|
55
|
+
if (generationConfig.input)
|
|
56
|
+
options.input = generationConfig.input;
|
|
57
|
+
if (generationConfig.output)
|
|
58
|
+
options.output = generationConfig.output;
|
|
59
|
+
if (generationConfig.templates)
|
|
60
|
+
options.templates = generationConfig.templates;
|
|
61
|
+
if (generationConfig.skipInstall !== undefined)
|
|
62
|
+
options.skipInstall = generationConfig.skipInstall;
|
|
63
|
+
if (generationConfig.skipLint !== undefined)
|
|
64
|
+
options.skipLint = generationConfig.skipLint;
|
|
65
|
+
(0, logger_1.logDetail)('config', `Using configuration file: ${configFile}`);
|
|
66
|
+
}
|
|
67
|
+
(0, logger_1.logDetail)('config', `Input: ${options.input}`);
|
|
68
|
+
(0, logger_1.logDetail)('config', `Output: ${options.output}`);
|
|
69
|
+
(0, logger_1.logDetail)('config', `Templates: ${options.templates}`);
|
|
70
|
+
if (!fs_extra_1.default.existsSync(options.input)) {
|
|
71
|
+
(0, logger_1.logError)(`File not found: ${options.input}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (options.dryRun) {
|
|
75
|
+
(0, logger_1.logWarning)('DRY RUN mode — no files will be generated');
|
|
76
|
+
}
|
|
77
|
+
if (!(0, openapi_generator_1.checkOpenApiGenerator)()) {
|
|
78
|
+
(0, logger_1.logWarning)('OpenAPI Generator CLI not found');
|
|
79
|
+
if (!options.skipInstall) {
|
|
80
|
+
(0, openapi_generator_1.installOpenApiGenerator)();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
(0, logger_1.logError)('Install openapi-generator-cli with: npm install -g @openapitools/openapi-generator-cli');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
(0, logger_1.logSuccess)('OpenAPI Generator CLI found');
|
|
89
|
+
}
|
|
90
|
+
const analysis = (0, analyzer_1.analyzeSwagger)(options.input);
|
|
91
|
+
const tagSummaries = (0, clean_arch_generator_1.extractTagsWithOperations)(analysis);
|
|
92
|
+
// ── INIT CONFIG MODE: generate config file and exit ───────────────────────
|
|
93
|
+
if (options.initConfig !== undefined) {
|
|
94
|
+
const envFile = (0, environment_finder_1.findEnvironmentFile)(process.cwd());
|
|
95
|
+
let apiKeys = [];
|
|
96
|
+
if (envFile) {
|
|
97
|
+
const envContent = fs_extra_1.default.readFileSync(envFile, 'utf8');
|
|
98
|
+
apiKeys = (0, environment_finder_1.parseApiKeys)(envContent);
|
|
99
|
+
}
|
|
100
|
+
const defaultConfig = (0, config_1.generateDefaultConfig)(analysis, tagSummaries, options, apiKeys);
|
|
101
|
+
const outputFile = typeof options.initConfig === 'string' ? options.initConfig : 'generation-config.json';
|
|
102
|
+
(0, config_1.writeConfig)(defaultConfig, outputFile);
|
|
103
|
+
(0, logger_1.logSuccess)(`Configuration file generated: ${outputFile}`);
|
|
104
|
+
(0, logger_1.logDetail)('config', 'Edit the file to customise tags, endpoints and baseUrls, then run with --config');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (options.dryRun) {
|
|
108
|
+
(0, logger_1.logWarning)('Finishing in DRY RUN mode');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
(0, filesystem_1.createDirectoryStructure)(options.output);
|
|
112
|
+
// ── SELECTION: tags and endpoints ─────────────────────────────────────────
|
|
113
|
+
let selectionFilter = {};
|
|
114
|
+
let tagApiKeyMap;
|
|
115
|
+
if (generationConfig) {
|
|
116
|
+
// Config-driven: derive everything from the JSON file
|
|
117
|
+
selectionFilter = (0, config_1.deriveSelectionFilter)(generationConfig);
|
|
118
|
+
tagApiKeyMap = (0, config_1.deriveTagApiKeyMap)(generationConfig);
|
|
119
|
+
(0, logger_1.logDetail)('config', `Tags from config: ${Object.keys(generationConfig.tags).join(', ')}`);
|
|
120
|
+
Object.entries(tagApiKeyMap).forEach(([tag, key]) => {
|
|
121
|
+
(0, logger_1.logDetail)('config', `API key for "${tag}": environment.${key}.url`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Interactive mode (original behaviour)
|
|
126
|
+
if (options.selectEndpoints) {
|
|
127
|
+
selectionFilter = await (0, prompt_1.askSelectionFilter)(tagSummaries);
|
|
128
|
+
}
|
|
129
|
+
const selectedTags = options.selectEndpoints
|
|
130
|
+
? Object.keys(selectionFilter)
|
|
131
|
+
: tagSummaries.map((t) => t.tag);
|
|
132
|
+
// ── ENVIRONMENT API KEY SELECTION ────────────────────────────────────────
|
|
133
|
+
const envFile = (0, environment_finder_1.findEnvironmentFile)(process.cwd());
|
|
134
|
+
let apiKeys = [];
|
|
135
|
+
if (envFile) {
|
|
136
|
+
const envContent = fs_extra_1.default.readFileSync(envFile, 'utf8');
|
|
137
|
+
apiKeys = (0, environment_finder_1.parseApiKeys)(envContent);
|
|
138
|
+
(0, logger_1.logSuccess)(`environment.ts found: ${logger_1.colors.cyan}${path_1.default.relative(process.cwd(), envFile)}${logger_1.colors.reset}`);
|
|
139
|
+
if (apiKeys.length === 0) {
|
|
140
|
+
(0, logger_1.logWarning)('No keys containing "api" found in environment.ts. Will be requested manually.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
(0, logger_1.logWarning)('No environment.ts found. The key will be requested manually per repository.');
|
|
145
|
+
}
|
|
146
|
+
tagApiKeyMap = await (0, prompt_1.askApiKeysForTags)(selectedTags, apiKeys);
|
|
147
|
+
Object.entries(tagApiKeyMap).forEach(([tag, key]) => {
|
|
148
|
+
(0, logger_1.logDetail)('config', `API key for "${tag}": environment.${key}.url`);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
152
|
+
const tempDir = (0, dto_generator_1.generateCode)(options.input, options.templates);
|
|
153
|
+
(0, dto_generator_1.organizeFiles)(tempDir, options.output);
|
|
154
|
+
(0, dto_generator_1.addDtoImports)(options.output);
|
|
155
|
+
(0, clean_arch_generator_1.generateCleanArchitecture)(analysis, options.output, options.templates, tagApiKeyMap, selectionFilter);
|
|
156
|
+
(0, filesystem_1.cleanup)(tempDir);
|
|
157
|
+
const noLintResult = {
|
|
158
|
+
prettier: { ran: false, filesFormatted: 0 },
|
|
159
|
+
eslint: { ran: false, filesFixed: 0 }
|
|
160
|
+
};
|
|
161
|
+
const lintResult = options.skipLint ? noLintResult : (0, lint_generator_1.lintGeneratedFiles)(options.output);
|
|
162
|
+
const report = (0, report_generator_1.generateReport)(options.output, analysis, lintResult);
|
|
163
|
+
console.log('\n' + '='.repeat(60));
|
|
164
|
+
(0, logger_1.log)(' ✨ Generation completed successfully', 'green');
|
|
165
|
+
console.log('='.repeat(60));
|
|
166
|
+
console.log(`\n📊 Summary:`);
|
|
167
|
+
console.log(` - DTOs generated: ${report.structure.dtos}`);
|
|
168
|
+
console.log(` - Repositories: ${report.structure.repositories}`);
|
|
169
|
+
console.log(` - Mappers: ${report.structure.mappers}`);
|
|
170
|
+
console.log(` - Use Cases: ${report.structure.useCases}`);
|
|
171
|
+
console.log(` - Providers: ${report.structure.providers}`);
|
|
172
|
+
console.log(` - Mocks: ${report.structure.mocks}`);
|
|
173
|
+
console.log(`\n📁 Files generated in: ${logger_1.colors.cyan}${options.output}${logger_1.colors.reset}\n`);
|
|
174
|
+
}
|
|
175
|
+
main().catch((error) => {
|
|
176
|
+
const err = error;
|
|
177
|
+
(0, logger_1.logError)(`Fatal error: ${err.message}`);
|
|
178
|
+
console.error(error);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
});
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const eslint = require('@eslint/js');
|
|
2
|
+
const tseslint = require('typescript-eslint');
|
|
3
|
+
const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended');
|
|
4
|
+
|
|
5
|
+
module.exports = tseslint.config(
|
|
6
|
+
eslint.configs.recommended,
|
|
7
|
+
...tseslint.configs.recommendedTypeChecked,
|
|
8
|
+
eslintPluginPrettierRecommended,
|
|
9
|
+
{
|
|
10
|
+
languageOptions: {
|
|
11
|
+
parserOptions: {
|
|
12
|
+
project: ['./tsconfig.json'],
|
|
13
|
+
tsconfigRootDir: __dirname
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
rules: {
|
|
17
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
18
|
+
'@typescript-eslint/explicit-function-return-type': 'warn',
|
|
19
|
+
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
20
|
+
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
21
|
+
'@typescript-eslint/no-unsafe-call': 'off',
|
|
22
|
+
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
23
|
+
'@typescript-eslint/require-await': 'off',
|
|
24
|
+
'@typescript-eslint/no-unused-vars': [
|
|
25
|
+
'warn',
|
|
26
|
+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
ignores: ['dist/', 'node_modules/', 'eslint.config.js']
|
|
32
|
+
}
|
|
33
|
+
);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
openapi: 3.0.1
|
|
2
|
+
info:
|
|
3
|
+
title: Example API
|
|
4
|
+
description: API de ejemplo para probar el generador
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
tags:
|
|
7
|
+
- name: User
|
|
8
|
+
description: Operaciones de usuario
|
|
9
|
+
- name: Product
|
|
10
|
+
description: Operaciones de productos
|
|
11
|
+
paths:
|
|
12
|
+
/v1/users:
|
|
13
|
+
get:
|
|
14
|
+
tags:
|
|
15
|
+
- User
|
|
16
|
+
summary: Obtener lista de usuarios
|
|
17
|
+
operationId: getUsers
|
|
18
|
+
parameters:
|
|
19
|
+
- name: search
|
|
20
|
+
in: query
|
|
21
|
+
required: false
|
|
22
|
+
schema:
|
|
23
|
+
type: string
|
|
24
|
+
responses:
|
|
25
|
+
'200':
|
|
26
|
+
description: OK
|
|
27
|
+
content:
|
|
28
|
+
application/json:
|
|
29
|
+
schema:
|
|
30
|
+
$ref: '#/components/schemas/UserResponse'
|
|
31
|
+
post:
|
|
32
|
+
tags:
|
|
33
|
+
- User
|
|
34
|
+
summary: Crear usuario
|
|
35
|
+
operationId: createUser
|
|
36
|
+
requestBody:
|
|
37
|
+
required: true
|
|
38
|
+
content:
|
|
39
|
+
application/json:
|
|
40
|
+
schema:
|
|
41
|
+
$ref: '#/components/schemas/CreateUserRequest'
|
|
42
|
+
responses:
|
|
43
|
+
'201':
|
|
44
|
+
description: Created
|
|
45
|
+
content:
|
|
46
|
+
application/json:
|
|
47
|
+
schema:
|
|
48
|
+
$ref: '#/components/schemas/UserSchema'
|
|
49
|
+
/v1/users/{id}:
|
|
50
|
+
get:
|
|
51
|
+
tags:
|
|
52
|
+
- User
|
|
53
|
+
summary: Obtener usuario por ID
|
|
54
|
+
operationId: getUserById
|
|
55
|
+
parameters:
|
|
56
|
+
- name: id
|
|
57
|
+
in: path
|
|
58
|
+
required: true
|
|
59
|
+
schema:
|
|
60
|
+
type: integer
|
|
61
|
+
responses:
|
|
62
|
+
'200':
|
|
63
|
+
description: OK
|
|
64
|
+
content:
|
|
65
|
+
application/json:
|
|
66
|
+
schema:
|
|
67
|
+
$ref: '#/components/schemas/UserSchema'
|
|
68
|
+
delete:
|
|
69
|
+
tags:
|
|
70
|
+
- User
|
|
71
|
+
summary: Eliminar usuario
|
|
72
|
+
operationId: deleteUser
|
|
73
|
+
parameters:
|
|
74
|
+
- name: id
|
|
75
|
+
in: path
|
|
76
|
+
required: true
|
|
77
|
+
schema:
|
|
78
|
+
type: integer
|
|
79
|
+
responses:
|
|
80
|
+
'204':
|
|
81
|
+
description: No Content
|
|
82
|
+
/v1/products:
|
|
83
|
+
get:
|
|
84
|
+
tags:
|
|
85
|
+
- Product
|
|
86
|
+
summary: Obtener lista de productos
|
|
87
|
+
operationId: getProducts
|
|
88
|
+
responses:
|
|
89
|
+
'200':
|
|
90
|
+
description: OK
|
|
91
|
+
content:
|
|
92
|
+
application/json:
|
|
93
|
+
schema:
|
|
94
|
+
$ref: '#/components/schemas/ProductResponse'
|
|
95
|
+
components:
|
|
96
|
+
schemas:
|
|
97
|
+
UserSchema:
|
|
98
|
+
type: object
|
|
99
|
+
properties:
|
|
100
|
+
id:
|
|
101
|
+
type: integer
|
|
102
|
+
example: 1
|
|
103
|
+
name:
|
|
104
|
+
type: string
|
|
105
|
+
example: Juan Pérez
|
|
106
|
+
email:
|
|
107
|
+
type: string
|
|
108
|
+
example: juan@example.com
|
|
109
|
+
active:
|
|
110
|
+
type: boolean
|
|
111
|
+
example: true
|
|
112
|
+
CreateUserRequest:
|
|
113
|
+
type: object
|
|
114
|
+
required:
|
|
115
|
+
- name
|
|
116
|
+
- email
|
|
117
|
+
properties:
|
|
118
|
+
name:
|
|
119
|
+
type: string
|
|
120
|
+
example: Juan Pérez
|
|
121
|
+
email:
|
|
122
|
+
type: string
|
|
123
|
+
example: juan@example.com
|
|
124
|
+
UserResponse:
|
|
125
|
+
type: object
|
|
126
|
+
properties:
|
|
127
|
+
users:
|
|
128
|
+
type: array
|
|
129
|
+
items:
|
|
130
|
+
$ref: '#/components/schemas/UserSchema'
|
|
131
|
+
ProductSchema:
|
|
132
|
+
type: object
|
|
133
|
+
properties:
|
|
134
|
+
id:
|
|
135
|
+
type: integer
|
|
136
|
+
example: 100
|
|
137
|
+
name:
|
|
138
|
+
type: string
|
|
139
|
+
example: Laptop HP
|
|
140
|
+
price:
|
|
141
|
+
type: number
|
|
142
|
+
format: float
|
|
143
|
+
example: 599.99
|
|
144
|
+
ProductResponse:
|
|
145
|
+
type: object
|
|
146
|
+
properties:
|
|
147
|
+
products:
|
|
148
|
+
type: array
|
|
149
|
+
items:
|
|
150
|
+
$ref: '#/components/schemas/ProductSchema'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"input": "example-swagger.yaml",
|
|
3
|
+
"output": "./src/app",
|
|
4
|
+
"skipLint": false,
|
|
5
|
+
"skipInstall": false,
|
|
6
|
+
"tags": {
|
|
7
|
+
"User": {
|
|
8
|
+
"baseUrl": "apiUrl",
|
|
9
|
+
"endpoints": [
|
|
10
|
+
"getUsers",
|
|
11
|
+
"createUser",
|
|
12
|
+
"getUserById",
|
|
13
|
+
"deleteUser"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"Product": {
|
|
17
|
+
"baseUrl": "apiUrl",
|
|
18
|
+
"endpoints": [
|
|
19
|
+
"getProducts"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"templates": "/Users/bsantome/Downloads/openapi-clean-arch-generator/templates"
|
|
24
|
+
}
|
package/main.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import mustache from 'mustache';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { program } from 'commander';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
log,
|
|
10
|
+
logSuccess,
|
|
11
|
+
logWarning,
|
|
12
|
+
logError,
|
|
13
|
+
logDetail,
|
|
14
|
+
initGenerationLog,
|
|
15
|
+
colors
|
|
16
|
+
} from './src/utils/logger';
|
|
17
|
+
import { checkOpenApiGenerator, installOpenApiGenerator } from './src/utils/openapi-generator';
|
|
18
|
+
import { createDirectoryStructure, cleanup } from './src/utils/filesystem';
|
|
19
|
+
import { analyzeSwagger } from './src/swagger/analyzer';
|
|
20
|
+
import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator';
|
|
21
|
+
import {
|
|
22
|
+
generateCleanArchitecture,
|
|
23
|
+
extractTagsWithOperations
|
|
24
|
+
} from './src/generators/clean-arch.generator';
|
|
25
|
+
import { generateReport } from './src/generators/report.generator';
|
|
26
|
+
import { lintGeneratedFiles } from './src/generators/lint.generator';
|
|
27
|
+
import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder';
|
|
28
|
+
import { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt';
|
|
29
|
+
import {
|
|
30
|
+
loadConfig,
|
|
31
|
+
generateDefaultConfig,
|
|
32
|
+
writeConfig,
|
|
33
|
+
deriveSelectionFilter,
|
|
34
|
+
deriveTagApiKeyMap
|
|
35
|
+
} from './src/utils/config';
|
|
36
|
+
import type { SelectionFilter, LintResult } from './src/types';
|
|
37
|
+
import type { CliOptions } from './src/types';
|
|
38
|
+
import packageJson from './package.json';
|
|
39
|
+
|
|
40
|
+
// Disable HTML escaping so that < and > produce valid TypeScript generic types.
|
|
41
|
+
(mustache as { escape: (text: string) => string }).escape = function (text: string): string {
|
|
42
|
+
return text;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ── CLI CONFIGURATION ────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
program
|
|
48
|
+
.name('generate-clean-arch')
|
|
49
|
+
.description('Angular Clean Architecture code generator from OpenAPI/Swagger')
|
|
50
|
+
.version(packageJson.version)
|
|
51
|
+
.option('-i, --input <file>', 'OpenAPI/Swagger file (yaml or json)', 'swagger.yaml')
|
|
52
|
+
.option('-o, --output <dir>', 'Output directory', './src/app')
|
|
53
|
+
.option('-t, --templates <dir>', 'Custom templates directory', path.join(__dirname, 'templates'))
|
|
54
|
+
.option('--skip-install', 'Skip dependency installation')
|
|
55
|
+
.option('--dry-run', 'Simulate without generating files')
|
|
56
|
+
.option('--skip-lint', 'Skip post-generation linting and formatting')
|
|
57
|
+
.option('-s, --select-endpoints', 'Interactively select which tags and endpoints to generate')
|
|
58
|
+
.option('-c, --config <file>', 'Use a JSON configuration file (skips interactive prompts)')
|
|
59
|
+
.option('--init-config [file]', 'Generate a JSON configuration file instead of generating code')
|
|
60
|
+
.parse(process.argv);
|
|
61
|
+
|
|
62
|
+
const options = program.opts<CliOptions>();
|
|
63
|
+
|
|
64
|
+
// ── MAIN ORCHESTRATOR ────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
async function main(): Promise<void> {
|
|
67
|
+
console.log('\n' + '='.repeat(60));
|
|
68
|
+
log(' OpenAPI Clean Architecture Generator', 'bright');
|
|
69
|
+
log(' Angular + Clean Architecture Code Generator', 'cyan');
|
|
70
|
+
console.log('='.repeat(60) + '\n');
|
|
71
|
+
|
|
72
|
+
const logPath = path.join(process.cwd(), 'generation.log');
|
|
73
|
+
initGenerationLog(logPath);
|
|
74
|
+
|
|
75
|
+
// ── CONFIG FILE: override CLI defaults with config values ─────────────────
|
|
76
|
+
const configFile = options.config;
|
|
77
|
+
const generationConfig = configFile ? loadConfig(configFile) : undefined;
|
|
78
|
+
|
|
79
|
+
if (generationConfig) {
|
|
80
|
+
if (generationConfig.input) options.input = generationConfig.input;
|
|
81
|
+
if (generationConfig.output) options.output = generationConfig.output;
|
|
82
|
+
if (generationConfig.templates) options.templates = generationConfig.templates;
|
|
83
|
+
if (generationConfig.skipInstall !== undefined)
|
|
84
|
+
options.skipInstall = generationConfig.skipInstall;
|
|
85
|
+
if (generationConfig.skipLint !== undefined) options.skipLint = generationConfig.skipLint;
|
|
86
|
+
logDetail('config', `Using configuration file: ${configFile}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
logDetail('config', `Input: ${options.input}`);
|
|
90
|
+
logDetail('config', `Output: ${options.output}`);
|
|
91
|
+
logDetail('config', `Templates: ${options.templates}`);
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(options.input)) {
|
|
94
|
+
logError(`File not found: ${options.input}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (options.dryRun) {
|
|
99
|
+
logWarning('DRY RUN mode — no files will be generated');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!checkOpenApiGenerator()) {
|
|
103
|
+
logWarning('OpenAPI Generator CLI not found');
|
|
104
|
+
if (!options.skipInstall) {
|
|
105
|
+
installOpenApiGenerator();
|
|
106
|
+
} else {
|
|
107
|
+
logError(
|
|
108
|
+
'Install openapi-generator-cli with: npm install -g @openapitools/openapi-generator-cli'
|
|
109
|
+
);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
logSuccess('OpenAPI Generator CLI found');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const analysis = analyzeSwagger(options.input);
|
|
117
|
+
const tagSummaries = extractTagsWithOperations(analysis);
|
|
118
|
+
|
|
119
|
+
// ── INIT CONFIG MODE: generate config file and exit ───────────────────────
|
|
120
|
+
if (options.initConfig !== undefined) {
|
|
121
|
+
const envFile = findEnvironmentFile(process.cwd());
|
|
122
|
+
let apiKeys: ReturnType<typeof parseApiKeys> = [];
|
|
123
|
+
if (envFile) {
|
|
124
|
+
const envContent = fs.readFileSync(envFile, 'utf8');
|
|
125
|
+
apiKeys = parseApiKeys(envContent);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const defaultConfig = generateDefaultConfig(analysis, tagSummaries, options, apiKeys);
|
|
129
|
+
const outputFile =
|
|
130
|
+
typeof options.initConfig === 'string' ? options.initConfig : 'generation-config.json';
|
|
131
|
+
|
|
132
|
+
writeConfig(defaultConfig, outputFile);
|
|
133
|
+
logSuccess(`Configuration file generated: ${outputFile}`);
|
|
134
|
+
logDetail(
|
|
135
|
+
'config',
|
|
136
|
+
'Edit the file to customise tags, endpoints and baseUrls, then run with --config'
|
|
137
|
+
);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.dryRun) {
|
|
142
|
+
logWarning('Finishing in DRY RUN mode');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
createDirectoryStructure(options.output);
|
|
147
|
+
|
|
148
|
+
// ── SELECTION: tags and endpoints ─────────────────────────────────────────
|
|
149
|
+
let selectionFilter: SelectionFilter = {};
|
|
150
|
+
let tagApiKeyMap: Record<string, string>;
|
|
151
|
+
|
|
152
|
+
if (generationConfig) {
|
|
153
|
+
// Config-driven: derive everything from the JSON file
|
|
154
|
+
selectionFilter = deriveSelectionFilter(generationConfig);
|
|
155
|
+
tagApiKeyMap = deriveTagApiKeyMap(generationConfig);
|
|
156
|
+
logDetail('config', `Tags from config: ${Object.keys(generationConfig.tags).join(', ')}`);
|
|
157
|
+
Object.entries(tagApiKeyMap).forEach(([tag, key]) => {
|
|
158
|
+
logDetail('config', `API key for "${tag}": environment.${key}.url`);
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
// Interactive mode (original behaviour)
|
|
162
|
+
if (options.selectEndpoints) {
|
|
163
|
+
selectionFilter = await askSelectionFilter(tagSummaries);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const selectedTags = options.selectEndpoints
|
|
167
|
+
? Object.keys(selectionFilter)
|
|
168
|
+
: tagSummaries.map((t) => t.tag);
|
|
169
|
+
|
|
170
|
+
// ── ENVIRONMENT API KEY SELECTION ────────────────────────────────────────
|
|
171
|
+
const envFile = findEnvironmentFile(process.cwd());
|
|
172
|
+
let apiKeys: ReturnType<typeof parseApiKeys> = [];
|
|
173
|
+
|
|
174
|
+
if (envFile) {
|
|
175
|
+
const envContent = fs.readFileSync(envFile, 'utf8');
|
|
176
|
+
apiKeys = parseApiKeys(envContent);
|
|
177
|
+
logSuccess(
|
|
178
|
+
`environment.ts found: ${colors.cyan}${path.relative(process.cwd(), envFile)}${colors.reset}`
|
|
179
|
+
);
|
|
180
|
+
if (apiKeys.length === 0) {
|
|
181
|
+
logWarning('No keys containing "api" found in environment.ts. Will be requested manually.');
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
logWarning('No environment.ts found. The key will be requested manually per repository.');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
tagApiKeyMap = await askApiKeysForTags(selectedTags, apiKeys);
|
|
188
|
+
Object.entries(tagApiKeyMap).forEach(([tag, key]) => {
|
|
189
|
+
logDetail('config', `API key for "${tag}": environment.${key}.url`);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
const tempDir = generateCode(options.input, options.templates);
|
|
196
|
+
organizeFiles(tempDir, options.output);
|
|
197
|
+
addDtoImports(options.output);
|
|
198
|
+
generateCleanArchitecture(
|
|
199
|
+
analysis,
|
|
200
|
+
options.output,
|
|
201
|
+
options.templates,
|
|
202
|
+
tagApiKeyMap,
|
|
203
|
+
selectionFilter
|
|
204
|
+
);
|
|
205
|
+
cleanup(tempDir);
|
|
206
|
+
|
|
207
|
+
const noLintResult: LintResult = {
|
|
208
|
+
prettier: { ran: false, filesFormatted: 0 },
|
|
209
|
+
eslint: { ran: false, filesFixed: 0 }
|
|
210
|
+
};
|
|
211
|
+
const lintResult = options.skipLint ? noLintResult : lintGeneratedFiles(options.output);
|
|
212
|
+
|
|
213
|
+
const report = generateReport(options.output, analysis, lintResult);
|
|
214
|
+
|
|
215
|
+
console.log('\n' + '='.repeat(60));
|
|
216
|
+
log(' ✨ Generation completed successfully', 'green');
|
|
217
|
+
console.log('='.repeat(60));
|
|
218
|
+
console.log(`\n📊 Summary:`);
|
|
219
|
+
console.log(` - DTOs generated: ${report.structure.dtos}`);
|
|
220
|
+
console.log(` - Repositories: ${report.structure.repositories}`);
|
|
221
|
+
console.log(` - Mappers: ${report.structure.mappers}`);
|
|
222
|
+
console.log(` - Use Cases: ${report.structure.useCases}`);
|
|
223
|
+
console.log(` - Providers: ${report.structure.providers}`);
|
|
224
|
+
console.log(` - Mocks: ${report.structure.mocks}`);
|
|
225
|
+
console.log(`\n📁 Files generated in: ${colors.cyan}${options.output}${colors.reset}\n`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
main().catch((error: unknown) => {
|
|
229
|
+
const err = error as Error;
|
|
230
|
+
logError(`Fatal error: ${err.message}`);
|
|
231
|
+
console.error(error);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
|
3
|
+
"spaces": 2,
|
|
4
|
+
"generator-cli": {
|
|
5
|
+
"version": "7.2.0",
|
|
6
|
+
"generators": {
|
|
7
|
+
"typescript-angular-clean": {
|
|
8
|
+
"generatorName": "typescript-angular",
|
|
9
|
+
"output": "./.temp-generated",
|
|
10
|
+
"glob": "**/*",
|
|
11
|
+
"additionalProperties": {
|
|
12
|
+
"ngVersion": "17.0.0",
|
|
13
|
+
"modelPropertyNaming": "camelCase",
|
|
14
|
+
"supportsES6": true,
|
|
15
|
+
"withInterfaces": true,
|
|
16
|
+
"providedInRoot": false,
|
|
17
|
+
"npmName": "api-client",
|
|
18
|
+
"npmVersion": "1.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|