28 Commits

Author SHA1 Message Date
7c5af2f3ab chore: add dist to package.json
All checks were successful
Publish / publish (push) Successful in 1m44s
2026-03-27 14:45:53 +01:00
cbef98a077 chore: add dist to package.json
Some checks failed
Publish / publish (push) Has been cancelled
2026-03-27 14:26:09 +01:00
ddca01e4e9 chore: bump to version v1.3.10 2026-03-27 08:37:33 +00:00
59ff941fda Merge pull request 'chore: update installation instructions and add mock files to README' (#65) from feat/update-docu into main
Reviewed-on: #65
Reviewed-by: blas <me@blassanto.me>
2026-03-27 08:33:50 +00:00
8881e9494c chore: update installation instructions and add mock files to README
All checks were successful
Lint / lint (pull_request) Successful in 13s
Publish / publish (push) Successful in 2m48s
2026-03-27 09:28:51 +01:00
720748b73d chore: bump to version v1.3.9 2026-03-27 08:03:10 +00:00
7063796e28 Merge pull request 'chore: update installation instructions for npm registry in release body' (#64) from chore/change-registry-to-npm into main
All checks were successful
Publish / publish (push) Successful in 2m21s
Reviewed-on: #64
2026-03-27 08:02:13 +00:00
f349b7b2a3 chore: update installation instructions for npm registry in release body
All checks were successful
Lint / lint (pull_request) Successful in 19s
2026-03-27 08:59:28 +01:00
b59084dec6 chore: bump to version v1.3.8 2026-03-26 20:39:57 +00:00
5c83520f01 Merge pull request 'refactor: streamline npm registry authentication setup in publish workflow' (#63) from chore/update-publish into main
Some checks failed
Publish / publish (push) Failing after 32s
Reviewed-on: #63
2026-03-26 20:38:41 +00:00
cc0439e26e refactor: streamline npm registry authentication setup in publish workflow
All checks were successful
Lint / lint (pull_request) Successful in 14s
2026-03-26 21:32:24 +01:00
b5b3632f5b chore: bump to version v1.3.7 2026-03-26 20:24:26 +00:00
d78bc303fa Merge pull request 'chore: update-publish' (#62) from chore/update-publish into main
Some checks failed
Publish / publish (push) Failing after 5m32s
Reviewed-on: #62
Reviewed-by: blas <me@blassanto.me>
2026-03-26 20:23:35 +00:00
df9283556b Merge branch 'main' into chore/update-publish
All checks were successful
Lint / lint (pull_request) Successful in 14s
2026-03-26 21:21:07 +01:00
909f709659 fix: update environment variable for npm registry publishing 2026-03-26 21:20:50 +01:00
5878331abf chore: bump to version v1.3.6 2026-03-26 20:14:26 +00:00
7e8e6d7058 Merge pull request 'fix: update package name and installation instructions in publish workflow' (#61) from chore/update-publish into main
Some checks failed
Publish / publish (push) Failing after 5m30s
Reviewed-on: #61
Reviewed-by: blas <me@blassanto.me>
2026-03-26 20:12:32 +00:00
469697f636 fix: update package name and installation instructions in publish workflow
All checks were successful
Lint / lint (pull_request) Successful in 23s
2026-03-26 20:51:50 +01:00
2257e2141e Merge pull request 'feat/add-config-file' (#60) from feat/add-config-file into main
Reviewed-on: #60
2026-03-26 18:09:18 +00:00
0f64b51b63 Merge remote-tracking branch 'origin/main'
All checks were successful
Lint / lint (pull_request) Successful in 20s
# Conflicts:
#	main.ts
2026-03-26 19:02:16 +01:00
9c385191e2 feat: add configuration file strategy 2026-03-26 18:52:34 +01:00
d2f9eaa933 chore: bump to version v1.3.5 2026-03-26 16:08:21 +00:00
a600a60678 Merge pull request 'chore: update npm registry configuration in publish workflow' (#59) from chore/change-registry-to-npm into main
Some checks failed
Publish / publish (push) Failing after 5m27s
Reviewed-on: #59
2026-03-26 16:07:47 +00:00
a42063c1d9 chore: update npm registry configuration in publish workflow
All checks were successful
Lint / lint (pull_request) Successful in 14s
2026-03-26 17:05:10 +01:00
7f6feda81d chore: bump to version v1.3.4 2026-03-26 15:55:38 +00:00
12b2dd6b51 Merge pull request 'chore: update npm registry configuration in Bun settings' (#58) from chore/change-registry-to-npm into main
Some checks failed
Publish / publish (push) Has been cancelled
Reviewed-on: #58
2026-03-26 15:55:08 +00:00
84486e816a chore: update npm registry configuration in Bun settings
All checks were successful
Lint / lint (pull_request) Successful in 14s
2026-03-26 16:54:33 +01:00
942cf7f092 chore: bump to version v1.3.3 2026-03-26 15:52:35 +00:00
7 changed files with 319 additions and 67 deletions

View File

@@ -64,18 +64,15 @@ jobs:
- name: Configure npm registry auth - name: Configure npm registry auth
run: | run: |
echo "registry=https://registry.npmjs.org" >> ~/.npmrc
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
cat >> ~/.bunfig.toml << EOF
[install.scopes]
"@0kmpo" = { registry = "https://registry.npmjs.org", token = "${NODE_AUTH_TOKEN}" }
EOF
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to npm registry - name: Publish to npm registry
run: bun publish --access public run: bun publish --access public
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} BUN_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create Gitea release and upload binaries - name: Create Gitea release and upload binaries
run: | run: |
@@ -88,7 +85,7 @@ jobs:
-d "{ -d "{
\"tag_name\": \"${GITHUB_REF_NAME}\", \"tag_name\": \"${GITHUB_REF_NAME}\",
\"name\": \"v${VERSION}\", \"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, \"draft\": false,
\"prerelease\": false \"prerelease\": false
}" | bun -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).id))") }" | bun -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).id))")

View File

@@ -15,29 +15,29 @@ Download the binary for your platform from the releases page and run it directly
```bash ```bash
# macOS (Apple Silicon) # 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 chmod +x generate-clean-arch && ./generate-clean-arch -i swagger.yaml
# macOS (Intel) # 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 chmod +x generate-clean-arch && ./generate-clean-arch -i swagger.yaml
# Linux x64 # 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 chmod +x generate-clean-arch && ./generate-clean-arch -i swagger.yaml
# Windows (PowerShell) # 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 .\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 ```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 ```bash
generate-clean-arch -i swagger.yaml generate-clean-arch -i swagger.yaml
@@ -109,48 +109,54 @@ src/app/
├── data/ # Data layer ├── data/ # Data layer
│ ├── dtos/ # Data Transfer Objects │ ├── dtos/ # Data Transfer Objects
│ │ ├── node/ │ │ ├── node/
│ │ │ ── node.dto.ts │ │ │ ── node.dto.ts
│ │ │ └── node.dto.mock.ts
│ │ ├── order-type/ │ │ ├── order-type/
│ │ │ ── order-type.dto.ts │ │ │ ── order-type.dto.ts
│ │ │ └── order-type.dto.mock.ts
│ │ └── supply-mode/ │ │ └── supply-mode/
│ │ ── supply-mode.dto.ts │ │ ── supply-mode.dto.ts
│ │ └── supply-mode.dto.mock.ts
│ ├── repositories/ # Repository implementations │ ├── repositories/ # Repository implementations
│ │ ├── node.repository.impl.ts │ │ ├── node.repository.impl.ts
│ │ ├── node.repository.impl.mock.ts
│ │ ├── node.repository.impl.spec.ts
│ │ ├── order-type.repository.impl.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 │ └── mappers/ # DTO → Entity transformers
│ ├── node.mapper.ts │ ├── node.mapper.ts
│ ├── node.mapper.spec.ts
│ ├── order-type.mapper.ts │ ├── order-type.mapper.ts
── supply-mode.mapper.ts ── order-type.mapper.spec.ts
│ └── ...
├── domain/ # Domain layer ├── domain/ # Domain layer
│ ├── repositories/ # Repository contracts │ ├── repositories/ # Repository contracts
│ │ ├── node.repository.contract.ts │ │ ├── node.repository.contract.ts
│ │ ── order-type.repository.contract.ts │ │ ── ...
│ │ └── supply-mode.repository.contract.ts
│ └── use-cases/ # Use cases │ └── use-cases/ # Use cases
│ ├── node/ │ ├── node/
│ │ ├── node.use-cases.contract.ts │ │ ├── node.use-cases.contract.ts
│ │ ── node.use-cases.impl.ts │ │ ── node.use-cases.impl.ts
├── order-type/ │ ├── node.use-cases.mock.ts
│ │ ── order-type.use-cases.contract.ts │ │ ── node.use-cases.impl.spec.ts
│ └── order-type.use-cases.impl.ts └── ...
│ └── supply-mode/
│ ├── supply-mode.use-cases.contract.ts
│ └── supply-mode.use-cases.impl.ts
├── di/ # Dependency injection ├── di/ # Dependency injection
│ ├── repositories/ # Repository providers │ ├── repositories/ # Repository providers
│ │ ├── node.repository.provider.ts │ │ ├── node.repository.provider.ts
│ │ ├── order-type.repository.provider.ts │ │ ├── node.repository.provider.mock.ts
│ │ └── supply-mode.repository.provider.ts │ │ └── ...
│ └── use-cases/ # Use case providers │ └── use-cases/ # Use case providers
│ ├── node.use-cases.provider.ts │ ├── node.use-cases.provider.ts
│ ├── order-type.use-cases.provider.ts │ ├── node.use-cases.provider.mock.ts
│ └── supply-mode.use-cases.provider.ts │ └── ...
└── entities/ # Domain entities └── entities/ # Domain entities
└── models/ └── models/
├── node.model.ts ├── node.model.ts
├── order-type.model.ts ├── node.model.mock.ts
── supply-mode.model.ts ── node.model.spec.ts
└── ...
``` ```
## 🔧 Template Customization ## 🔧 Template Customization
@@ -160,14 +166,24 @@ Templates live in `templates/` and use [Mustache](https://mustache.github.io/) s
| Template | Generates | | Template | Generates |
|---|---| |---|---|
| `model.mustache` | DTOs | | `model.mustache` | DTOs |
| `model.mock.mustache` | DTO mocks |
| `dto.mock.mustache` | DTO mocks (alternative) |
| `model-entity.mustache` | Domain entity models | | `model-entity.mustache` | Domain entity models |
| `model-entity.spec.mustache` | Entity model specs |
| `mapper.mustache` | DTO → Entity mappers | | `mapper.mustache` | DTO → Entity mappers |
| `mapper.spec.mustache` | Mapper specs |
| `api.repository.contract.mustache` | Repository contracts | | `api.repository.contract.mustache` | Repository contracts |
| `api.repository.impl.mustache` | Repository implementations | | `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.contract.mustache` | Use case contracts |
| `api.use-cases.impl.mustache` | Use case implementations | | `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.mustache` | Repository DI providers |
| `repository.provider.mock.mustache` | Repository provider mocks |
| `use-cases.provider.mustache` | Use case DI providers | | `use-cases.provider.mustache` | Use case DI providers |
| `use-cases.provider.mock.mustache` | Use case provider mocks |
### Available Mustache variables ### Available Mustache variables
@@ -194,12 +210,23 @@ After each run a `generation-report.json` file is created:
"timestamp": "2025-01-15T10:30:00.000Z", "timestamp": "2025-01-15T10:30:00.000Z",
"tags": 3, "tags": 3,
"endpoints": 8, "endpoints": 8,
"tagDetails": [
{ "name": "User", "description": "User operations", "endpoints": 3 },
{ "name": "Product", "description": "Product operations", "endpoints": 2 }
],
"outputDirectory": "./src/app", "outputDirectory": "./src/app",
"linting": {
"prettier": { "ran": true, "filesFormatted": 42 },
"eslint": { "ran": true, "filesFixed": 42 }
},
"structure": { "structure": {
"dtos": 15, "dtos": 15,
"repositories": 9, "repositories": 9,
"mappers": 3, "mappers": 5,
"useCases": 6 "useCases": 6,
"providers": 12,
"mocks": 18,
"specs": 14
} }
} }
``` ```

24
generation-config.json Normal file
View 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
View File

@@ -26,6 +26,13 @@ import { generateReport } from './src/generators/report.generator';
import { lintGeneratedFiles } from './src/generators/lint.generator'; import { lintGeneratedFiles } from './src/generators/lint.generator';
import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder'; import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder';
import { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt'; 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 { SelectionFilter, LintResult } from './src/types';
import type { CliOptions } from './src/types'; import type { CliOptions } from './src/types';
import packageJson from './package.json'; import packageJson from './package.json';
@@ -48,6 +55,8 @@ program
.option('--dry-run', 'Simulate without generating files') .option('--dry-run', 'Simulate without generating files')
.option('--skip-lint', 'Skip post-generation linting and formatting') .option('--skip-lint', 'Skip post-generation linting and formatting')
.option('-s, --select-endpoints', 'Interactively select which tags and endpoints to generate') .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); .parse(process.argv);
const options = program.opts<CliOptions>(); const options = program.opts<CliOptions>();
@@ -62,6 +71,21 @@ async function main(): Promise<void> {
const logPath = path.join(process.cwd(), 'generation.log'); const logPath = path.join(process.cwd(), 'generation.log');
initGenerationLog(logPath); 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', `Input: ${options.input}`);
logDetail('config', `Output: ${options.output}`); logDetail('config', `Output: ${options.output}`);
logDetail('config', `Templates: ${options.templates}`); logDetail('config', `Templates: ${options.templates}`);
@@ -90,6 +114,29 @@ async function main(): Promise<void> {
} }
const analysis = analyzeSwagger(options.input); 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) { if (options.dryRun) {
logWarning('Finishing in DRY RUN mode'); logWarning('Finishing in DRY RUN mode');
@@ -99,38 +146,50 @@ async function main(): Promise<void> {
createDirectoryStructure(options.output); createDirectoryStructure(options.output);
// ── SELECTION: tags and endpoints ───────────────────────────────────────── // ── SELECTION: tags and endpoints ─────────────────────────────────────────
const tagSummaries = extractTagsWithOperations(analysis);
let selectionFilter: SelectionFilter = {}; let selectionFilter: SelectionFilter = {};
let tagApiKeyMap: Record<string, string>;
if (options.selectEndpoints) { if (generationConfig) {
selectionFilter = await askSelectionFilter(tagSummaries); // Config-driven: derive everything from the JSON file
} selectionFilter = deriveSelectionFilter(generationConfig);
tagApiKeyMap = deriveTagApiKeyMap(generationConfig);
const selectedTags = options.selectEndpoints logDetail('config', `Tags from config: ${Object.keys(generationConfig.tags).join(', ')}`);
? Object.keys(selectionFilter) Object.entries(tagApiKeyMap).forEach(([tag, key]) => {
: tagSummaries.map((t) => t.tag); logDetail('config', `API key for "${tag}": environment.${key}.url`);
});
// ── 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 { } 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); const tempDir = generateCode(options.input, options.templates);

View File

@@ -1,6 +1,6 @@
{ {
"name": "@blas/openapi-clean-arch-generator", "name": "@0kmpo/openapi-clean-arch-generator",
"version": "1.3.2", "version": "1.3.10",
"description": "Angular Clean Architecture generator from OpenAPI/Swagger", "description": "Angular Clean Architecture generator from OpenAPI/Swagger",
"main": "dist/main.js", "main": "dist/main.js",
"bin": { "bin": {
@@ -39,9 +39,13 @@
} }
], ],
"license": "MIT", "license": "MIT",
"publishConfig": { "files": [
"registry": "https://git.blassanto.me/api/packages/blas/npm/" "dist/main.js",
}, "dist/src/",
"dist/templates/",
"README.md",
"LICENSE"
],
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
"commander": "^11.1.0", "commander": "^11.1.0",

View File

@@ -10,4 +10,27 @@ export interface CliOptions {
dryRun?: boolean; dryRun?: boolean;
selectEndpoints?: boolean; selectEndpoints?: boolean;
skipLint?: 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
View 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;
}