Compare commits
44 Commits
04962e32f5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 902c0fd5a2 | |||
| ef804b74c2 | |||
| 98cd7a5edb | |||
| 3ede53ae3b | |||
| aa7c6cf338 | |||
| 0075717a1f | |||
| 7c5af2f3ab | |||
| e667c7bf04 | |||
| 2e9aecdffe | |||
| 5304ed7047 | |||
| 56bd6f4e9f | |||
| cbef98a077 | |||
| 3eb604c95f | |||
| 4293857a7d | |||
| 07e4559133 | |||
| bdd0e4873e | |||
| ddca01e4e9 | |||
| 59ff941fda | |||
| 8881e9494c | |||
| 720748b73d | |||
| 7063796e28 | |||
| f349b7b2a3 | |||
| b59084dec6 | |||
| 5c83520f01 | |||
| cc0439e26e | |||
| b5b3632f5b | |||
| d78bc303fa | |||
| df9283556b | |||
| 909f709659 | |||
| 5878331abf | |||
| 7e8e6d7058 | |||
| 469697f636 | |||
| 2257e2141e | |||
| 0f64b51b63 | |||
| 9c385191e2 | |||
| d2f9eaa933 | |||
| a600a60678 | |||
| a42063c1d9 | |||
| 7f6feda81d | |||
| 12b2dd6b51 | |||
| 84486e816a | |||
| 942cf7f092 | |||
| e0fb12a6c6 | |||
| 2402b40059 |
@@ -64,6 +64,7 @@ jobs:
|
||||
|
||||
- name: Configure npm registry auth
|
||||
run: |
|
||||
echo "registry=https://registry.npmjs.org" >> ~/.npmrc
|
||||
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -71,7 +72,7 @@ jobs:
|
||||
- name: Publish to npm registry
|
||||
run: bun publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
BUN_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Create Gitea release and upload binaries
|
||||
run: |
|
||||
@@ -84,7 +85,7 @@ jobs:
|
||||
-d "{
|
||||
\"tag_name\": \"${GITHUB_REF_NAME}\",
|
||||
\"name\": \"v${VERSION}\",
|
||||
\"body\": \"## Installation\n\nDownload the binary for your platform or install via the npm registry:\n\n\`\`\`bash\nbun add -g @blas/openapi-clean-arch-generator --registry https://git.blassanto.me/api/packages/blas/npm/\n\`\`\`\",
|
||||
\"body\": \"## Installation\n\nDownload the binary for your platform or install via the npm registry:\n\n\`\`\`bash\nbun add -g @0kmpo/openapi-clean-arch-generator\n\`\`\`\",
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}" | bun -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).id))")
|
||||
|
||||
50
NOTICE
Normal file
50
NOTICE
Normal file
@@ -0,0 +1,50 @@
|
||||
OpenAPI Clean Architecture Generator
|
||||
Copyright (c) 2026 Blas Santomé Ocampo
|
||||
|
||||
This product includes software developed by third parties.
|
||||
All third-party packages listed below are distributed under the MIT License.
|
||||
|
||||
---
|
||||
|
||||
chalk v4.1.2
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
License: MIT
|
||||
https://github.com/chalk/chalk
|
||||
|
||||
---
|
||||
|
||||
commander v11.1.0
|
||||
Copyright (c) TJ Holowaychuk <tj@vision-media.ca>
|
||||
License: MIT
|
||||
https://github.com/tj/commander.js
|
||||
|
||||
---
|
||||
|
||||
fs-extra v11.3.4
|
||||
Copyright (c) JP Richardson <jprichardson@gmail.com>
|
||||
License: MIT
|
||||
https://github.com/jprichardson/node-fs-extra
|
||||
|
||||
---
|
||||
|
||||
js-yaml v4.1.1
|
||||
Copyright (c) Vladimir Zapparov <dervus.grim@gmail.com>
|
||||
Copyright (c) Aleksey V Zapparov <ixti@member.fsf.org>
|
||||
Copyright (c) Vitaly Puzrin <vitaly@rcdesign.ru>
|
||||
Copyright (c) Martin Grenfell <martin.grenfell@gmail.com>
|
||||
License: MIT
|
||||
https://github.com/nodeca/js-yaml
|
||||
|
||||
---
|
||||
|
||||
mustache v4.2.0
|
||||
Copyright (c) mustache.js Authors (http://github.com/janl/mustache.js)
|
||||
License: MIT
|
||||
https://github.com/janl/mustache.js
|
||||
|
||||
---
|
||||
|
||||
prompts v2.4.2
|
||||
Copyright (c) Terkel Gjervig
|
||||
License: MIT
|
||||
https://github.com/terkelg/prompts
|
||||
97
README.md
97
README.md
@@ -15,29 +15,29 @@ Download the binary for your platform from the releases page and run it directly
|
||||
|
||||
```bash
|
||||
# macOS (Apple Silicon)
|
||||
curl -L <release-url>/generate-clean-arch-macos-arm64 -o generate-clean-arch
|
||||
curl -L https://git.blassanto.me/blas/openapi-clean-arch-gen/releases/latest/download/generate-clean-arch-macos-arm64 -o generate-clean-arch
|
||||
chmod +x generate-clean-arch && ./generate-clean-arch -i swagger.yaml
|
||||
|
||||
# macOS (Intel)
|
||||
curl -L <release-url>/generate-clean-arch-macos-x64 -o generate-clean-arch
|
||||
curl -L https://git.blassanto.me/blas/openapi-clean-arch-gen/releases/latest/download/generate-clean-arch-macos-x64 -o generate-clean-arch
|
||||
chmod +x generate-clean-arch && ./generate-clean-arch -i swagger.yaml
|
||||
|
||||
# Linux x64
|
||||
curl -L <release-url>/generate-clean-arch-linux-x64 -o generate-clean-arch
|
||||
curl -L https://git.blassanto.me/blas/openapi-clean-arch-gen/releases/latest/download/generate-clean-arch-linux-x64 -o generate-clean-arch
|
||||
chmod +x generate-clean-arch && ./generate-clean-arch -i swagger.yaml
|
||||
|
||||
# Windows (PowerShell)
|
||||
curl -L <release-url>/generate-clean-arch-windows-x64.exe -o generate-clean-arch.exe
|
||||
curl -L https://git.blassanto.me/blas/openapi-clean-arch-gen/releases/latest/download/generate-clean-arch-windows-x64.exe -o generate-clean-arch.exe
|
||||
.\generate-clean-arch.exe -i swagger.yaml
|
||||
```
|
||||
|
||||
### Option 1: Install as a global CLI from the registry
|
||||
### Option 1: Install as a global CLI from npm
|
||||
|
||||
```bash
|
||||
bun add -g @blas/openapi-clean-arch-generator --registry https://git.blassanto.me/api/packages/blas/npm/
|
||||
bun add -g @0kmpo/openapi-clean-arch-generator
|
||||
```
|
||||
|
||||
Or configure the registry in your `.npmrc` / `bunfig.toml` and then run:
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
generate-clean-arch -i swagger.yaml
|
||||
@@ -76,7 +76,10 @@ Options:
|
||||
-o, --output <dir> Output directory [default: ./src/app]
|
||||
-t, --templates <dir> Custom templates directory [default: ./templates]
|
||||
-s, --select-endpoints Interactively select tags and endpoints to generate
|
||||
-c, --config <file> Use a JSON configuration file (skips interactive prompts)
|
||||
--init-config [file] Generate a JSON configuration file instead of generating code
|
||||
--skip-install Skip dependency installation
|
||||
--skip-lint Skip post-generation linting and formatting
|
||||
--dry-run Simulate without writing files
|
||||
-h, --help Show help
|
||||
```
|
||||
@@ -96,6 +99,15 @@ generate-clean-arch -i api.yaml -t ./my-templates
|
||||
# Dry run (no files written)
|
||||
generate-clean-arch -i swagger.yaml --dry-run
|
||||
|
||||
# Skip linting after generation
|
||||
generate-clean-arch -i swagger.yaml --skip-lint
|
||||
|
||||
# Generate a config file to reuse later
|
||||
generate-clean-arch --init-config generation-config.json
|
||||
|
||||
# Run using a config file (no interactive prompts)
|
||||
generate-clean-arch -c generation-config.json
|
||||
|
||||
# Full example with all options
|
||||
generate-clean-arch -i ./docs/api.yaml -o ./frontend/src/app -t ./custom-templates
|
||||
```
|
||||
@@ -109,48 +121,54 @@ src/app/
|
||||
├── data/ # Data layer
|
||||
│ ├── dtos/ # Data Transfer Objects
|
||||
│ │ ├── node/
|
||||
│ │ │ └── node.dto.ts
|
||||
│ │ │ ├── node.dto.ts
|
||||
│ │ │ └── node.dto.mock.ts
|
||||
│ │ ├── order-type/
|
||||
│ │ │ └── order-type.dto.ts
|
||||
│ │ │ ├── order-type.dto.ts
|
||||
│ │ │ └── order-type.dto.mock.ts
|
||||
│ │ └── supply-mode/
|
||||
│ │ └── supply-mode.dto.ts
|
||||
│ │ ├── supply-mode.dto.ts
|
||||
│ │ └── supply-mode.dto.mock.ts
|
||||
│ ├── repositories/ # Repository implementations
|
||||
│ │ ├── node.repository.impl.ts
|
||||
│ │ ├── node.repository.impl.mock.ts
|
||||
│ │ ├── node.repository.impl.spec.ts
|
||||
│ │ ├── order-type.repository.impl.ts
|
||||
│ │ └── supply-mode.repository.impl.ts
|
||||
│ │ ├── order-type.repository.impl.mock.ts
|
||||
│ │ ├── order-type.repository.impl.spec.ts
|
||||
│ │ └── ...
|
||||
│ └── mappers/ # DTO → Entity transformers
|
||||
│ ├── node.mapper.ts
|
||||
│ ├── node.mapper.spec.ts
|
||||
│ ├── order-type.mapper.ts
|
||||
│ └── supply-mode.mapper.ts
|
||||
│ ├── order-type.mapper.spec.ts
|
||||
│ └── ...
|
||||
├── domain/ # Domain layer
|
||||
│ ├── repositories/ # Repository contracts
|
||||
│ │ ├── node.repository.contract.ts
|
||||
│ │ ├── order-type.repository.contract.ts
|
||||
│ │ └── supply-mode.repository.contract.ts
|
||||
│ │ └── ...
|
||||
│ └── use-cases/ # Use cases
|
||||
│ ├── node/
|
||||
│ │ ├── node.use-cases.contract.ts
|
||||
│ │ └── node.use-cases.impl.ts
|
||||
│ ├── order-type/
|
||||
│ │ ├── order-type.use-cases.contract.ts
|
||||
│ │ └── order-type.use-cases.impl.ts
|
||||
│ └── supply-mode/
|
||||
│ ├── supply-mode.use-cases.contract.ts
|
||||
│ └── supply-mode.use-cases.impl.ts
|
||||
│ │ ├── node.use-cases.impl.ts
|
||||
│ │ ├── node.use-cases.mock.ts
|
||||
│ │ └── node.use-cases.impl.spec.ts
|
||||
│ └── ...
|
||||
├── di/ # Dependency injection
|
||||
│ ├── repositories/ # Repository providers
|
||||
│ │ ├── node.repository.provider.ts
|
||||
│ │ ├── order-type.repository.provider.ts
|
||||
│ │ └── supply-mode.repository.provider.ts
|
||||
│ │ ├── node.repository.provider.mock.ts
|
||||
│ │ └── ...
|
||||
│ └── use-cases/ # Use case providers
|
||||
│ ├── node.use-cases.provider.ts
|
||||
│ ├── order-type.use-cases.provider.ts
|
||||
│ └── supply-mode.use-cases.provider.ts
|
||||
│ ├── node.use-cases.provider.mock.ts
|
||||
│ └── ...
|
||||
└── entities/ # Domain entities
|
||||
└── models/
|
||||
├── node.model.ts
|
||||
├── order-type.model.ts
|
||||
└── supply-mode.model.ts
|
||||
├── node.model.mock.ts
|
||||
├── node.model.spec.ts
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 🔧 Template Customization
|
||||
@@ -160,14 +178,24 @@ Templates live in `templates/` and use [Mustache](https://mustache.github.io/) s
|
||||
| Template | Generates |
|
||||
|---|---|
|
||||
| `model.mustache` | DTOs |
|
||||
| `model.mock.mustache` | DTO mocks |
|
||||
| `dto.mock.mustache` | DTO mocks (alternative) |
|
||||
| `model-entity.mustache` | Domain entity models |
|
||||
| `model-entity.spec.mustache` | Entity model specs |
|
||||
| `mapper.mustache` | DTO → Entity mappers |
|
||||
| `mapper.spec.mustache` | Mapper specs |
|
||||
| `api.repository.contract.mustache` | Repository contracts |
|
||||
| `api.repository.impl.mustache` | Repository implementations |
|
||||
| `api.repository.impl.mock.mustache` | Repository mocks |
|
||||
| `api.repository.impl.spec.mustache` | Repository specs |
|
||||
| `api.use-cases.contract.mustache` | Use case contracts |
|
||||
| `api.use-cases.impl.mustache` | Use case implementations |
|
||||
| `api.use-cases.mock.mustache` | Use case mocks |
|
||||
| `api.use-cases.impl.spec.mustache` | Use case specs |
|
||||
| `repository.provider.mustache` | Repository DI providers |
|
||||
| `repository.provider.mock.mustache` | Repository provider mocks |
|
||||
| `use-cases.provider.mustache` | Use case DI providers |
|
||||
| `use-cases.provider.mock.mustache` | Use case provider mocks |
|
||||
|
||||
### Available Mustache variables
|
||||
|
||||
@@ -194,12 +222,23 @@ After each run a `generation-report.json` file is created:
|
||||
"timestamp": "2025-01-15T10:30:00.000Z",
|
||||
"tags": 3,
|
||||
"endpoints": 8,
|
||||
"tagDetails": [
|
||||
{ "name": "User", "description": "User operations", "endpoints": 3 },
|
||||
{ "name": "Product", "description": "Product operations", "endpoints": 2 }
|
||||
],
|
||||
"outputDirectory": "./src/app",
|
||||
"linting": {
|
||||
"prettier": { "ran": true, "filesFormatted": 42 },
|
||||
"eslint": { "ran": true, "filesFixed": 42 }
|
||||
},
|
||||
"structure": {
|
||||
"dtos": 15,
|
||||
"repositories": 9,
|
||||
"mappers": 3,
|
||||
"useCases": 6
|
||||
"mappers": 5,
|
||||
"useCases": 6,
|
||||
"providers": 12,
|
||||
"mocks": 18,
|
||||
"specs": 14
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
24
generation-config.json
Normal file
24
generation-config.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"input": "example-swagger.yaml",
|
||||
"output": "./src/app",
|
||||
"skipLint": false,
|
||||
"skipInstall": false,
|
||||
"tags": {
|
||||
"User": {
|
||||
"baseUrl": "apiUrl",
|
||||
"endpoints": [
|
||||
"getUsers",
|
||||
"createUser",
|
||||
"getUserById",
|
||||
"deleteUser"
|
||||
]
|
||||
},
|
||||
"Product": {
|
||||
"baseUrl": "apiUrl",
|
||||
"endpoints": [
|
||||
"getProducts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"templates": "/Users/bsantome/Downloads/openapi-clean-arch-generator/templates"
|
||||
}
|
||||
113
main.ts
113
main.ts
@@ -26,6 +26,13 @@ import { generateReport } from './src/generators/report.generator';
|
||||
import { lintGeneratedFiles } from './src/generators/lint.generator';
|
||||
import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder';
|
||||
import { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt';
|
||||
import {
|
||||
loadConfig,
|
||||
generateDefaultConfig,
|
||||
writeConfig,
|
||||
deriveSelectionFilter,
|
||||
deriveTagApiKeyMap
|
||||
} from './src/utils/config';
|
||||
import type { SelectionFilter, LintResult } from './src/types';
|
||||
import type { CliOptions } from './src/types';
|
||||
import packageJson from './package.json';
|
||||
@@ -48,6 +55,8 @@ program
|
||||
.option('--dry-run', 'Simulate without generating files')
|
||||
.option('--skip-lint', 'Skip post-generation linting and formatting')
|
||||
.option('-s, --select-endpoints', 'Interactively select which tags and endpoints to generate')
|
||||
.option('-c, --config <file>', 'Use a JSON configuration file (skips interactive prompts)')
|
||||
.option('--init-config [file]', 'Generate a JSON configuration file instead of generating code')
|
||||
.parse(process.argv);
|
||||
|
||||
const options = program.opts<CliOptions>();
|
||||
@@ -62,6 +71,21 @@ async function main(): Promise<void> {
|
||||
|
||||
const logPath = path.join(process.cwd(), 'generation.log');
|
||||
initGenerationLog(logPath);
|
||||
|
||||
// ── CONFIG FILE: override CLI defaults with config values ─────────────────
|
||||
const configFile = options.config;
|
||||
const generationConfig = configFile ? loadConfig(configFile) : undefined;
|
||||
|
||||
if (generationConfig) {
|
||||
if (generationConfig.input) options.input = generationConfig.input;
|
||||
if (generationConfig.output) options.output = generationConfig.output;
|
||||
if (generationConfig.templates) options.templates = generationConfig.templates;
|
||||
if (generationConfig.skipInstall !== undefined)
|
||||
options.skipInstall = generationConfig.skipInstall;
|
||||
if (generationConfig.skipLint !== undefined) options.skipLint = generationConfig.skipLint;
|
||||
logDetail('config', `Using configuration file: ${configFile}`);
|
||||
}
|
||||
|
||||
logDetail('config', `Input: ${options.input}`);
|
||||
logDetail('config', `Output: ${options.output}`);
|
||||
logDetail('config', `Templates: ${options.templates}`);
|
||||
@@ -90,6 +114,29 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
const analysis = analyzeSwagger(options.input);
|
||||
const tagSummaries = extractTagsWithOperations(analysis);
|
||||
|
||||
// ── INIT CONFIG MODE: generate config file and exit ───────────────────────
|
||||
if (options.initConfig !== undefined) {
|
||||
const envFile = findEnvironmentFile(process.cwd());
|
||||
let apiKeys: ReturnType<typeof parseApiKeys> = [];
|
||||
if (envFile) {
|
||||
const envContent = fs.readFileSync(envFile, 'utf8');
|
||||
apiKeys = parseApiKeys(envContent);
|
||||
}
|
||||
|
||||
const defaultConfig = generateDefaultConfig(analysis, tagSummaries, options, apiKeys);
|
||||
const outputFile =
|
||||
typeof options.initConfig === 'string' ? options.initConfig : 'generation-config.json';
|
||||
|
||||
writeConfig(defaultConfig, outputFile);
|
||||
logSuccess(`Configuration file generated: ${outputFile}`);
|
||||
logDetail(
|
||||
'config',
|
||||
'Edit the file to customise tags, endpoints and baseUrls, then run with --config'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
logWarning('Finishing in DRY RUN mode');
|
||||
@@ -99,38 +146,50 @@ async function main(): Promise<void> {
|
||||
createDirectoryStructure(options.output);
|
||||
|
||||
// ── SELECTION: tags and endpoints ─────────────────────────────────────────
|
||||
const tagSummaries = extractTagsWithOperations(analysis);
|
||||
let selectionFilter: SelectionFilter = {};
|
||||
let tagApiKeyMap: Record<string, string>;
|
||||
|
||||
if (options.selectEndpoints) {
|
||||
selectionFilter = await askSelectionFilter(tagSummaries);
|
||||
}
|
||||
|
||||
const selectedTags = options.selectEndpoints
|
||||
? Object.keys(selectionFilter)
|
||||
: tagSummaries.map((t) => t.tag);
|
||||
|
||||
// ── ENVIRONMENT API KEY SELECTION ──────────────────────────────────────────
|
||||
const envFile = findEnvironmentFile(process.cwd());
|
||||
let apiKeys: ReturnType<typeof parseApiKeys> = [];
|
||||
|
||||
if (envFile) {
|
||||
const envContent = fs.readFileSync(envFile, 'utf8');
|
||||
apiKeys = parseApiKeys(envContent);
|
||||
logSuccess(
|
||||
`environment.ts found: ${colors.cyan}${path.relative(process.cwd(), envFile)}${colors.reset}`
|
||||
);
|
||||
if (apiKeys.length === 0) {
|
||||
logWarning('No keys containing "api" found in environment.ts. Will be requested manually.');
|
||||
}
|
||||
if (generationConfig) {
|
||||
// Config-driven: derive everything from the JSON file
|
||||
selectionFilter = deriveSelectionFilter(generationConfig);
|
||||
tagApiKeyMap = deriveTagApiKeyMap(generationConfig);
|
||||
logDetail('config', `Tags from config: ${Object.keys(generationConfig.tags).join(', ')}`);
|
||||
Object.entries(tagApiKeyMap).forEach(([tag, key]) => {
|
||||
logDetail('config', `API key for "${tag}": environment.${key}.url`);
|
||||
});
|
||||
} else {
|
||||
logWarning('No environment.ts found. The key will be requested manually per repository.');
|
||||
// Interactive mode (original behaviour)
|
||||
if (options.selectEndpoints) {
|
||||
selectionFilter = await askSelectionFilter(tagSummaries);
|
||||
}
|
||||
|
||||
const selectedTags = options.selectEndpoints
|
||||
? Object.keys(selectionFilter)
|
||||
: tagSummaries.map((t) => t.tag);
|
||||
|
||||
// ── ENVIRONMENT API KEY SELECTION ────────────────────────────────────────
|
||||
const envFile = findEnvironmentFile(process.cwd());
|
||||
let apiKeys: ReturnType<typeof parseApiKeys> = [];
|
||||
|
||||
if (envFile) {
|
||||
const envContent = fs.readFileSync(envFile, 'utf8');
|
||||
apiKeys = parseApiKeys(envContent);
|
||||
logSuccess(
|
||||
`environment.ts found: ${colors.cyan}${path.relative(process.cwd(), envFile)}${colors.reset}`
|
||||
);
|
||||
if (apiKeys.length === 0) {
|
||||
logWarning('No keys containing "api" found in environment.ts. Will be requested manually.');
|
||||
}
|
||||
} else {
|
||||
logWarning('No environment.ts found. The key will be requested manually per repository.');
|
||||
}
|
||||
|
||||
tagApiKeyMap = await askApiKeysForTags(selectedTags, apiKeys);
|
||||
Object.entries(tagApiKeyMap).forEach(([tag, key]) => {
|
||||
logDetail('config', `API key for "${tag}": environment.${key}.url`);
|
||||
});
|
||||
}
|
||||
|
||||
const tagApiKeyMap = await askApiKeysForTags(selectedTags, apiKeys);
|
||||
Object.entries(tagApiKeyMap).forEach(([tag, key]) => {
|
||||
logDetail('config', `API key for "${tag}": environment.${key}.url`);
|
||||
});
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const tempDir = generateCode(options.input, options.templates);
|
||||
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@blas/openapi-clean-arch-generator",
|
||||
"version": "1.3.2",
|
||||
"name": "@0kmpo/openapi-clean-arch-generator",
|
||||
"version": "1.3.14",
|
||||
"description": "Angular Clean Architecture generator from OpenAPI/Swagger",
|
||||
"main": "dist/main.js",
|
||||
"bin": {
|
||||
@@ -39,9 +39,14 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"registry": "https://git.blassanto.me/api/packages/blas/npm/"
|
||||
},
|
||||
"files": [
|
||||
"dist/main.js",
|
||||
"dist/package.json",
|
||||
"dist/src/",
|
||||
"dist/templates/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^11.1.0",
|
||||
|
||||
@@ -10,4 +10,27 @@ export interface CliOptions {
|
||||
dryRun?: boolean;
|
||||
selectEndpoints?: boolean;
|
||||
skipLint?: boolean;
|
||||
config?: string;
|
||||
initConfig?: string | boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-tag configuration inside the generation config file.
|
||||
*/
|
||||
export interface TagConfig {
|
||||
baseUrl: string;
|
||||
endpoints: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON configuration file schema.
|
||||
* Allows full non-interactive control of the generation process.
|
||||
*/
|
||||
export interface GenerationConfig {
|
||||
input: string;
|
||||
output: string;
|
||||
templates?: string;
|
||||
skipInstall?: boolean;
|
||||
skipLint?: boolean;
|
||||
tags: Record<string, TagConfig>;
|
||||
}
|
||||
|
||||
118
src/utils/config.ts
Normal file
118
src/utils/config.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import fs from 'fs-extra';
|
||||
import { logInfo, logError } from './logger';
|
||||
import type { GenerationConfig, TagConfig } from '../types';
|
||||
import type { SwaggerAnalysis } from '../types';
|
||||
import type { TagSummary } from '../types';
|
||||
import type { ApiKeyInfo } from './environment-finder';
|
||||
|
||||
/**
|
||||
* Loads and validates a GenerationConfig from a JSON file.
|
||||
*/
|
||||
export function loadConfig(filePath: string): GenerationConfig {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
logError(`Configuration file not found: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
let config: GenerationConfig;
|
||||
|
||||
try {
|
||||
config = JSON.parse(raw) as GenerationConfig;
|
||||
} catch {
|
||||
logError(`Invalid JSON in configuration file: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!config.tags || typeof config.tags !== 'object') {
|
||||
logError('Configuration file must contain a "tags" object');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const [tag, tagConfig] of Object.entries(config.tags)) {
|
||||
if (!tagConfig.baseUrl || typeof tagConfig.baseUrl !== 'string') {
|
||||
logError(`Tag "${tag}" must have a "baseUrl" string`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!Array.isArray(tagConfig.endpoints) || tagConfig.endpoints.length === 0) {
|
||||
logError(`Tag "${tag}" must have a non-empty "endpoints" array`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a default GenerationConfig from a swagger analysis, including all tags
|
||||
* and all endpoints. Useful for --init-config to scaffold a config template.
|
||||
*/
|
||||
export function generateDefaultConfig(
|
||||
analysis: SwaggerAnalysis,
|
||||
tagSummaries: TagSummary[],
|
||||
cliOptions: {
|
||||
input: string;
|
||||
output: string;
|
||||
templates?: string;
|
||||
skipLint?: boolean;
|
||||
skipInstall?: boolean;
|
||||
},
|
||||
apiKeys: ApiKeyInfo[]
|
||||
): GenerationConfig {
|
||||
const tags: Record<string, TagConfig> = {};
|
||||
|
||||
for (const summary of tagSummaries) {
|
||||
const matchingKey = apiKeys.find((k) =>
|
||||
k.key.toLowerCase().includes(summary.tag.toLowerCase())
|
||||
);
|
||||
|
||||
tags[summary.tag] = {
|
||||
baseUrl: matchingKey?.key || 'apiUrl',
|
||||
endpoints: summary.operations.map((op) => op.nickname)
|
||||
};
|
||||
}
|
||||
|
||||
const config: GenerationConfig = {
|
||||
input: cliOptions.input,
|
||||
output: cliOptions.output,
|
||||
skipLint: cliOptions.skipLint ?? false,
|
||||
skipInstall: cliOptions.skipInstall ?? false,
|
||||
tags
|
||||
};
|
||||
|
||||
if (cliOptions.templates) {
|
||||
config.templates = cliOptions.templates;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a GenerationConfig to a JSON file.
|
||||
*/
|
||||
export function writeConfig(config: GenerationConfig, filePath: string): void {
|
||||
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||
logInfo(`Configuration file written to: ${filePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the selectionFilter (tag → endpoint nicknames) from a GenerationConfig.
|
||||
*/
|
||||
export function deriveSelectionFilter(config: GenerationConfig): Record<string, string[]> {
|
||||
const filter: Record<string, string[]> = {};
|
||||
for (const [tag, tagConfig] of Object.entries(config.tags)) {
|
||||
filter[tag] = [...tagConfig.endpoints];
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the tagApiKeyMap (tag → baseUrl key) from a GenerationConfig.
|
||||
*/
|
||||
export function deriveTagApiKeyMap(config: GenerationConfig): Record<string, string> {
|
||||
const map: Record<string, string> = {};
|
||||
for (const [tag, tagConfig] of Object.entries(config.tags)) {
|
||||
map[tag] = tagConfig.baseUrl;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
Reference in New Issue
Block a user