87 Commits

Author SHA1 Message Date
aa7c6cf338 chore: add dist to package.json
All checks were successful
Publish / publish (push) Successful in 1m47s
Lint / lint (pull_request) Successful in 13s
2026-03-27 14:53:39 +01:00
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
e0fb12a6c6 Merge pull request 'chore: add npm registry configuration to Bun settings' (#57) from chore/change-registry-to-npm into main
Some checks failed
Publish / publish (push) Failing after 27s
Reviewed-on: #57
2026-03-26 15:51:57 +00:00
2402b40059 chore: add npm registry configuration to Bun settings
All checks were successful
Lint / lint (pull_request) Successful in 14s
2026-03-26 16:48:15 +01:00
04962e32f5 chore: bump to version v1.3.2 2026-03-26 15:36:08 +00:00
144629bed6 chore: bump to version v1.3.2 2026-03-26 15:34:44 +00:00
0c58a63d01 chore: bump to version v1.3.2
Some checks failed
Publish / publish (push) Failing after 5m31s
2026-03-26 15:31:47 +00:00
74ac1c26a1 Merge pull request 'chore: update registry configuration from Gitea to npm' (#56) from chore/change-registry-to-npm into main
Reviewed-on: #56
2026-03-26 15:30:50 +00:00
db70f47bb7 chore: update registry configuration from Gitea to npm
All checks were successful
Lint / lint (pull_request) Successful in 18s
2026-03-26 16:29:34 +01:00
91e608415f chore: bump to version v1.3.1
Some checks failed
Publish / publish (push) Has been cancelled
2026-03-26 12:23:55 +00:00
058abf59c4 Merge pull request 'fix: assert repository call with specific parameters in observable completion' (#55) from fix/spec-without-expect into main
Reviewed-on: #55
Reviewed-by: blas <me@blassanto.me>
2026-03-26 12:22:55 +00:00
1d52da3805 test: assert repository call with specific parameters in observable completion
All checks were successful
Lint / lint (pull_request) Successful in 14s
2026-03-26 13:16:41 +01:00
32cb3d476f Merge pull request 'feat: enhance logging and linting functionality with detailed reports' (#54) from feat/move-logs into main
All checks were successful
Publish / publish (push) Successful in 2m5s
Reviewed-on: #54
Reviewed-by: blas <me@blassanto.me>
2026-03-26 12:06:46 +00:00
79ea7dfc7e feat: enhance logging and linting functionality with detailed reports
All checks were successful
Lint / lint (pull_request) Successful in 16s
2026-03-26 13:03:10 +01:00
b54a94c6d3 chore: bump to version v1.3.0 2026-03-26 10:59:19 +00:00
77e3cbc0e9 Merge pull request 'chore: fix publish workflow' (#52) from fix/fix-publish into main
All checks were successful
Publish / publish (push) Successful in 2m23s
Reviewed-on: #52
2026-03-26 10:58:15 +00:00
16d4c8e0bb chore: fix publish workflow
All checks were successful
Lint / lint (pull_request) Successful in 15s
2026-03-26 11:57:43 +01:00
e28443ce45 chore: bump to version v1.2.0 2026-03-26 10:51:33 +00:00
5707abf6bb Merge pull request 'feat: add specs generator & fix body params' (#49) from feat/add-specs into main
Some checks failed
Publish / publish (push) Failing after 30s
Reviewed-on: #49
Reviewed-by: blas <me@blassanto.me>
2026-03-26 10:33:07 +00:00
didavila
d47afb6ff1 fix: body param and 2xx response codes in repository generation
All checks were successful
Lint / lint (pull_request) Successful in 31s
Bug 1 - Body as positional argument (api.repository.impl.mustache):
MRepository HTTP methods accept a single RequestOptions object as second
argument. The template was incorrectly passing body as a separate positional
argument (e.g. this.post('/url', body)), causing:
  'Type X has no properties in common with type RequestOptions'
Fix: merge body into the options object using ES6 shorthand { body }, and
introduce hasOptions / hasBothParamsAndBody flags to build a single unified
options literal covering all scenarios:
  - no options    → this.post('/url')
  - params only   → this.get('/url', { params: { search } })
  - body only     → this.post('/url', { body })
  - params + body → this.post('/url', { params: { search }, body })

Bug 2 - Only 200 responses read (clean-arch.generator.ts):
The generator was hardcoded to read op.responses['200'], silently ignoring
201 Created, 202 Accepted, etc. POST endpoints returning 201 were generated
as Observable<void> instead of their actual return type.
Fix: resolve the first available success code from [200, 201, 202, 203].

New fields added to TagOperation type:
  - uppercaseHttpMethod: string
  - hasOptions: boolean
  - hasBothParamsAndBody: boolean

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 11:17:37 +01:00
didavila
463626da0c feat: add .spec.ts generation for models, mappers, repositories and use-cases
Add 4 new Mustache templates for generating unit test specs:
- model-entity.spec.mustache: tests instantiation, property setting, mock builder
- mapper.spec.mustache: tests per-property DTO→Entity mapping, instanceof, all fields
- api.repository.impl.spec.mustache: tests HTTP method, response mapping, error propagation
- api.use-cases.impl.spec.mustache: tests repository delegation, observable forwarding

Generator changes:
- Add uppercaseHttpMethod to TagOperation for spec HTTP assertions
- Add testValue to TagOperationParam for auto-generated test arguments
- Add resolveTestParamValue utility for primitive/complex type test literals
- Add specs counter to GeneratedCount and GenerationReport
- Wire 4 new renderTemplate calls in schema and tag loops
- Update report generator to count .spec.ts files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 10:52:58 +01:00
05a58c4254 chore: bump to version v1.1.0 2026-03-25 10:52:48 +00:00
d4d6148b25 Merge pull request 'feat: add mocks-generator' (#47) from feat/mocks-generator into main
Some checks failed
Publish / publish (push) Failing after 57s
Reviewed-on: #47
Reviewed-by: blas <me@blassanto.me>
2026-03-25 10:49:34 +00:00
didavila
cc650f9635 Merge branch 'main' into feat/mocks-generator
All checks were successful
Lint / lint (pull_request) Successful in 15s
2026-03-25 11:15:53 +01:00
didavila
99658800ed feat: ensure required fields are marked with '!' in generated model entities 2026-03-25 11:15:37 +01:00
0c162e30b8 Merge pull request 'chore: migrate from node to bun' (#46) from test/bun into main
Reviewed-on: #46
Reviewed-by: didavila <diego.davilafreitas@gmail.com>
2026-03-25 10:14:22 +00:00
didavila
9c14a070c6 feat: add linting option and implement linting for generated TypeScript files 2026-03-25 11:10:08 +01:00
94263d0329 chore: fix bun action
All checks were successful
Lint / lint (pull_request) Successful in 4m16s
2026-03-25 11:04:48 +01:00
06c4356f16 chore: fix bun action
All checks were successful
Lint / lint (pull_request) Successful in 15s
2026-03-25 11:01:50 +01:00
didavila
917cc3b9cf feat: add mock generation for DTOs, models, and repositories with corresponding templates 2026-03-25 11:01:21 +01:00
c6fbc4c47c chore: fix bun action
Some checks failed
Lint / lint (pull_request) Failing after 16s
2026-03-25 10:57:17 +01:00
1b342b871b chore: fix bun action
Some checks failed
Lint / lint (pull_request) Failing after 5s
2026-03-25 10:55:01 +01:00
a01b5a2d82 chore: fix bun install
Some checks failed
Lint / lint (pull_request) Failing after 14s
2026-03-25 10:53:45 +01:00
16fe772bd2 chore: migrate from node to bun
Some checks failed
Lint / lint (pull_request) Failing after 7s
2026-03-25 10:52:30 +01:00
2c1163809a chore: migrate from node to bun
Some checks failed
Lint / lint (pull_request) Failing after 19s
2026-03-25 10:51:14 +01:00
e4e88d184e chore: migrate from node to bun
Some checks failed
Lint / lint (pull_request) Failing after 5s
2026-03-25 10:46:24 +01:00
f51eaf7c78 chore: migrate from node to bun
Some checks failed
Lint / lint (pull_request) Failing after 6s
2026-03-25 10:43:19 +01:00
e1ef10f317 chore: migrate from node to bun
Some checks failed
Lint / lint (pull_request) Failing after 6s
2026-03-25 10:39:36 +01:00
77b77a17f4 chore: migrate from node to bun
Some checks failed
Lint / lint (pull_request) Failing after 5s
2026-03-25 10:38:27 +01:00
e69826b824 Merge branch 'main' into test/bun
Some checks failed
Lint / lint (pull_request) Failing after 2m51s
2026-03-25 10:25:32 +01:00
21b0333788 chore: migrate from node to bun 2026-03-25 10:25:23 +01:00
04d2299a4c chore: migrate from node to bun 2026-03-25 10:17:14 +01:00
a0a8ba28f5 Merge pull request 'feat: translate-to-english' (#41) from feat/translate-to-english into main
Reviewed-on: #41
Reviewed-by: blas <me@blassanto.me>
2026-03-25 09:13:24 +00:00
didavila
3bec87ba6b Merge branch 'main' into feat/translate-to-english
All checks were successful
Lint / lint (pull_request) Successful in 13s
2026-03-25 10:09:46 +01:00
didavila
c86c6bece6 feat: translate application messages and logs to English for improved accessibility 2026-03-25 10:09:30 +01:00
e776d49a77 Merge pull request 'feat: translate comments and logs to English for better accessibility' (#40) from feat/translate-to-english into main
Reviewed-on: #40
Reviewed-by: didavila <diego.davilafreitas@gmail.com>
2026-03-25 08:33:30 +00:00
f09e19265b Merge branch 'main' into feat/translate-to-english
All checks were successful
Lint / lint (pull_request) Successful in 5m58s
2026-03-25 08:25:23 +00:00
7d51b6b3db Merge pull request 'fix: fix lint in files and add a pipe for checking' (#38) from fix/fix-lint into main
Reviewed-on: #38
Reviewed-by: didavila <diego.davilafreitas@gmail.com>
2026-03-25 08:10:48 +00:00
0eb918ed71 fix: fix lint in files and add a pipe for checking
All checks were successful
Lint / lint (pull_request) Successful in 10m22s
2026-03-25 08:53:03 +01:00
didavila
5f34aa2f89 feat: translate comments and logs to English for better accessibility 2026-03-25 08:44:39 +01:00
dd6bb3e755 Merge branch 'main' into fix/fix-lint
Some checks failed
Lint / lint (pull_request) Failing after 5m58s
# Conflicts:
#	main.ts
2026-03-25 08:35:49 +01:00
700597a9e8 fix: fix lint in files and add a pipe for checking
All checks were successful
Lint / lint (pull_request) Successful in 10m28s
2026-03-25 08:33:10 +01:00
ad9a957be4 chore: bump to version v1.0.1 2026-03-25 07:23:39 +00:00
f85b981ceb Merge pull request 'feat: add tag/endpoint selection mechanism' (#35) from feat/#11 into main
Reviewed-on: #35
Reviewed-by: didavila <diego.davilafreitas@gmail.com>
2026-03-24 19:07:38 +00:00
1cd25d2b90 Merge branch 'main' into feat/#11
Some checks failed
Publish npm package / publish (push) Failing after 4m22s
2026-03-24 19:59:16 +01:00
bada7ba0e9 Merge pull request 'feat: add base url mechanism' (#34) from feat/#10 into main
Reviewed-on: #34
Reviewed-by: didavila <diego.davilafreitas@gmail.com>
2026-03-24 18:53:04 +00:00
b8d2fd8582 feat: add tag/endpoint selection mechanism 2026-03-24 19:51:48 +01:00
a90f7ba078 feat: add base url mechanism 2026-03-24 19:28:28 +01:00
4aeb108c55 feat: add base url mechanism 2026-03-24 19:15:47 +01:00
a9bbf21317 chore: bump to version v1.0.0 2026-03-24 15:52:12 +00:00
40 changed files with 2166 additions and 2094 deletions

View File

@@ -0,0 +1,41 @@
name: Lint
on:
pull_request:
branches:
- '**'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache Bun binary
uses: actions/cache@v4
with:
path: ~/.bun
key: bun-v1.3.3-${{ runner.os }}
- name: Setup Bun
run: |
if ! [ -f "$HOME/.bun/bin/bun" ]; then
curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.3"
fi
echo "$HOME/.bun/bin" >> $GITHUB_PATH
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-deps-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-deps-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run lint
run: bun run lint

View File

@@ -1,4 +1,4 @@
name: Publish npm package
name: Publish
on:
push:
@@ -12,19 +12,27 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Cache Bun binary
uses: actions/cache@v4
with:
node-version: '20'
path: ~/.bun
key: bun-v1.3.3-${{ runner.os }}
- name: Setup Bun
run: |
if ! [ -f "$HOME/.bun/bin/bun" ]; then
curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.3"
fi
echo "$HOME/.bun/bin" >> $GITHUB_PATH
- name: Set version from tag
run: |
VERSION=${GITHUB_REF_NAME#v}
echo "Setting package version to $VERSION"
npm pkg set version="$VERSION"
bun -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); pkg.version='${VERSION}'; fs.writeFileSync('package.json',JSON.stringify(pkg,null,2)+'\n');"
SHA=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"https://git.blassanto.me/api/v1/repos/blas/openapi-clean-arch-gen/contents/package.json?ref=main" \
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).sha))")
| bun -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).sha))")
CONTENT=$(base64 -w 0 package.json)
curl -s -X PUT \
-H "Authorization: token ${GITEA_TOKEN}" \
@@ -34,22 +42,64 @@ jobs:
env:
GITEA_TOKEN: ${{ secrets.TOKEN }}
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-deps-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-deps-
- name: Install dependencies
run: npm ci
run: bun install --frozen-lockfile
- name: Lint
run: npm run lint
run: bun run lint
- name: Build
run: npm run build
run: bun run build
- name: Configure Gitea registry auth
- name: Build binaries
run: bun run binaries
- name: Configure npm registry auth
run: |
echo "//git.blassanto.me/api/packages/blas/npm/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
echo "registry=https://registry.npmjs.org" >> ~/.npmrc
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
env:
NODE_AUTH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to Gitea
run: npm publish
- name: Publish to npm registry
run: bun publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
BUN_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create Gitea release and upload binaries
run: |
VERSION=${GITHUB_REF_NAME#v}
RELEASE_ID=$(curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"https://git.blassanto.me/api/v1/repos/blas/openapi-clean-arch-gen/releases" \
-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 @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))")
echo "Created release ID: $RELEASE_ID"
for BINARY in dist/bin/*; do
FILENAME=$(basename "$BINARY")
echo "Uploading $FILENAME..."
curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${BINARY};filename=${FILENAME}" \
"https://git.blassanto.me/api/v1/repos/blas/openapi-clean-arch-gen/releases/${RELEASE_ID}/assets"
done
env:
GITEA_TOKEN: ${{ secrets.TOKEN }}

2
.gitignore vendored
View File

@@ -1,8 +1,10 @@
node_modules/
package-lock.json
.temp-generated/
temp-generated/
dist/
dist/bin/
generation-report.json

366
README.md
View File

@@ -1,239 +1,277 @@
# OpenAPI Clean Architecture Generator
Generador de código Angular con Clean Architecture desde archivos OpenAPI/Swagger.
Angular code generator that creates clean architecture boilerplate from OpenAPI/Swagger specifications. Automates the creation of DTOs, repositories, mappers, use cases and dependency injection providers.
## 🚀 Instalación
## 📦 Requirements
### Opción 1: Instalación Global
- [Bun](https://bun.sh) >= 1.0.0
- [Java](https://www.java.com) (required by `openapi-generator-cli`)
## 🚀 Installation
### Option 0: Prebuilt binary (no dependencies)
Download the binary for your platform from the releases page and run it directly — no Node, Bun or Java required:
```bash
npm install -g @openapitools/openapi-generator-cli
npm install
# macOS (Apple Silicon)
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 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 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 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
```
### Opción 2: Usar directamente
### Option 1: Install as a global CLI from npm
```bash
npm install
npm run setup
bun add -g @0kmpo/openapi-clean-arch-generator
```
## 📖 Uso
### Comando básico
Then run:
```bash
# Versión compilada
npm run generate -- -i swagger.yaml
# Versión desarrollo (ts-node)
npm run generate:dev -- -i swagger.yaml
# Versión link global (si hiciste npm link)
generate-clean-arch -i swagger.yaml
```
### Opciones disponibles
### Option 2: Clone and use locally
```bash
npm run generate -- [opciones]
Opciones:
-V, --version Mostrar versión
-i, --input <file> Archivo OpenAPI/Swagger (yaml o json) [default: swagger.yaml]
-o, --output <dir> Directorio de salida [default: ./src/app]
-t, --templates <dir> Directorio de templates personalizados [default: ./templates]
--skip-install No instalar dependencias
--dry-run Simular sin generar archivos
-h, --help Mostrar ayuda
git clone <repo>
cd openapi-clean-arch-generator
bun install
bun run setup # installs openapi-generator-cli globally
```
### Ejemplos
## 📖 Usage
### Basic command
```bash
# Generar desde swagger.yaml en src/app
npm run generate -- -i swagger.yaml -o ./src/app
# Installed globally
generate-clean-arch -i swagger.yaml
# Usar templates personalizados
npm run generate -- -i api.yaml -t ./mis-templates
# From the repository (compiled)
bun run generate -- -i swagger.yaml
# Modo de prueba (no genera archivos)
npm run generate -- -i swagger.yaml --dry-run
# Especificar todos los parámetros
npm run generate -- -i ./docs/api.yaml -o ./frontend/src/app -t ./custom-templates
# From the repository (development, no compilation needed)
bun run generate:dev -- -i swagger.yaml
```
## 📁 Estructura Generada
### Available options
El generador crea la siguiente estructura siguiendo Clean Architecture:
```
Options:
-V, --version Show version
-i, --input <file> OpenAPI/Swagger file (yaml or json) [default: swagger.yaml]
-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
--skip-install Skip dependency installation
--dry-run Simulate without writing files
-h, --help Show help
```
### Examples
```bash
# Generate from swagger.yaml into src/app
generate-clean-arch -i swagger.yaml -o ./src/app
# Interactively select tags/endpoints
generate-clean-arch -i api.yaml -s
# Use custom templates
generate-clean-arch -i api.yaml -t ./my-templates
# Dry run (no files written)
generate-clean-arch -i swagger.yaml --dry-run
# Full example with all options
generate-clean-arch -i ./docs/api.yaml -o ./frontend/src/app -t ./custom-templates
```
## 📁 Generated Structure
The generator creates the following structure following Clean Architecture:
```
src/app/
├── data/ # Capa de datos
├── 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
├── repositories/ # Implementaciones de repositorios
│ │ ── 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
└── mappers/ # Transformadores DTO → Entidad
│ │ ── 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
├── domain/ # Capa de dominio
│ ├── repositories/ # Contratos de repositorios
── 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/ # Casos de uso
│ │ ── ...
│ └── 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
├── di/ # Inyección de dependencias
│ ├── repositories/ # Providers de repositorios
│ │ ── 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
│ └── use-cases/ # Providers de use cases
│ │ ├── 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
└── entities/ # Entidades de dominio
│ ├── 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
└── ...
```
## 🔧 Personalización
## 🔧 Template Customization
### Modificar Templates
Templates live in `templates/` and use [Mustache](https://mustache.github.io/) syntax. Override them by passing your own directory with `-t`.
Los templates están en la carpeta `templates/`. Cada archivo `.mustache` define cómo se genera un tipo de archivo.
| 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 |
Templates disponibles:
- `model.mustache` - DTOs
- `model-entity.mustache` - Entidades del modelo
- `mapper.mustache` - Mappers
- `api.repository.contract.mustache` - Contratos de repositorio
- `api.repository.impl.mustache` - Implementaciones de repositorio
- `api.use-cases.contract.mustache` - Contratos de use cases
- `api.use-cases.impl.mustache` - Implementaciones de use cases
- `repository.provider.mustache` - Providers de repositorio
- `use-cases.provider.mustache` - Providers de use cases
### Variables Mustache Disponibles
### Available Mustache variables
```mustache
{{classname}} - Nombre de la clase (ej: "OrderType")
{{classVarName}} - Nombre en camelCase (ej: "orderType")
{{classFilename}} - Nombre del archivo (ej: "order-type")
{{constantName}} - Constante (ej: "ORDER_TYPE")
{{description}} - Descripción del schema
{{httpMethod}} - Método HTTP (get, post, etc)
{{path}} - Path del endpoint
{{nickname}} - Nombre del método
{{allParams}} - Todos los parámetros
{{returnType}} - Tipo de retorno
{{vars}} - Variables del modelo
{{classname}} - Class name (e.g. "OrderType")
{{classVarName}} - camelCase name (e.g. "orderType")
{{classFilename}} - File name (e.g. "order-type")
{{constantName}} - UPPER_SNAKE_CASE constant (e.g. "ORDER_TYPE")
{{description}} - Schema description
{{httpMethod}} - HTTP method (get, post, put, delete…)
{{path}} - Endpoint path
{{nickname}} - Method name
{{allParams}} - All endpoint parameters
{{returnType}} - Return type
{{vars}} - Model variables
```
## 📊 Reporte de Generación
## 📊 Generation Report
Después de cada generación, se crea un archivo `generation-report.json` con estadísticas:
After each run a `generation-report.json` file is created:
```json
{
"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
}
}
```
## 🎯 Ejemplo Completo
## 🎯 Angular Integration Example
### 1. Preparar tu proyecto
### 1. Generate code
```bash
# Clonar o copiar el generador
cd mi-proyecto-angular
mkdir generator
cd generator
# Copiar archivos del generador aquí
generate-clean-arch -i ./docs/api.yaml -o ./src/app
```
### 2. Copiar tu Swagger
### 2. Register providers
```bash
cp ../docs/api.yaml ./swagger.yaml
```
### 3. Generar código
```bash
npm run generate -- -i swagger.yaml
```
### 4. Registrar providers en Angular
En tu `app.module.ts` o `app.config.ts`:
In your `app.config.ts` (Angular 17+ standalone):
```typescript
import { NodeRepositoryProvider } from '@/di/repositories/node.repository.provider';
import { NodeUseCasesProvider } from '@/di/use-cases/node.use-cases.provider';
// ... importar otros providers
import { ApplicationConfig } from '@angular/core';
import { NodeRepositoryProvider } from './di/repositories/node.repository.provider';
import { NodeUseCasesProvider } from './di/use-cases/node.use-cases.provider';
@NgModule({
export const appConfig: ApplicationConfig = {
providers: [
// Repositories
NodeRepositoryProvider,
OrderTypeRepositoryProvider,
SupplyModeRepositoryProvider,
// Use Cases
NodeUseCasesProvider,
OrderTypeUseCasesProvider,
SupplyModeUseCasesProvider
// ... rest of generated providers
]
})
export class AppModule {}
};
```
### 5. Usar en componentes
### 3. Use in components
```typescript
import { Component, inject } from '@angular/core';
import { NODE_USE_CASES, NodeUseCases } from '@/domain/use-cases/node/node.use-cases.contract';
import { NODE_USE_CASES } from './domain/use-cases/node/node.use-cases.contract';
@Component({
selector: 'app-nodes',
template: `...`
})
export class NodesComponent {
#nodeUseCases = inject(NODE_USE_CASES);
readonly #nodeUseCases = inject(NODE_USE_CASES);
loadNodes() {
this.#nodeUseCases.getNodes('TI').subscribe((nodes) => {
loadNodes(): void {
this.#nodeUseCases.getNodes().subscribe((nodes) => {
console.log(nodes);
});
}
@@ -242,54 +280,54 @@ export class NodesComponent {
## 🐛 Troubleshooting
### Error: openapi-generator-cli no encontrado
### `openapi-generator-cli` not found
```bash
npm install -g @openapitools/openapi-generator-cli
# o
npm run setup
bun run setup
# or manually:
bun add -g @openapitools/openapi-generator-cli
```
### Error: Archivo swagger.yaml no encontrado
### swagger.yaml file not found
Asegúrate de especificar la ruta correcta:
Specify the correct path with `-i`:
```bash
npm run generate -- -i ./ruta/a/tu/swagger.yaml
generate-clean-arch -i ./path/to/your/api.yaml
```
### Los imports no se resuelven (@/ no funciona)
### Path aliases `@/` not resolving
Configura los path aliases en tu `tsconfig.json`:
Configure the aliases in your Angular project's `tsconfig.json`:
```json
{
"compilerOptions": {
"paths": {
"@/*": ["src/app/*"],
"@environment": ["src/environments/environment"]
"@/*": ["src/app/*"]
}
}
}
```
### Los templates no generan el código esperado
### Templates not generating expected code
1. Verifica que tus templates están en `./templates/`
2. Revisa la sintaxis Mustache
3. Usa `--dry-run` para verificar sin generar archivos
1. Make sure your templates are in `./templates/` or pass the path with `-t`
2. Check the Mustache syntax
3. Use `--dry-run` to simulate without writing files
## 📝 Notas
## 📝 Notes
- El generador crea archivos `.ts`, no los compila
- Los providers deben registrarse manualmente en tu módulo Angular
- Asegúrate de tener configurado `@mercadona/common` o ajusta los imports en los templates
- El generador asume Angular 17+ con inject() function
- The generator produces ready-to-use `.ts` files, it does not compile them
- Providers must be registered manually in your Angular module or config
- Requires Angular 17+ (uses the `inject()` function)
- Compatible with both standalone and module-based projects
## 🤝 Contribuir
## 🤝 Contributing
Si encuentras bugs o mejoras, siéntete libre de modificar los templates y el script según tus necesidades.
Found a bug or have an improvement? Open an issue or PR in the repository.
## 📄 Licencia
## 📄 License
MIT

277
bun.lock Normal file
View File

@@ -0,0 +1,277 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "@blas/openapi-clean-arch-generator",
"dependencies": {
"chalk": "^4.1.2",
"commander": "^11.1.0",
"fs-extra": "^11.2.0",
"js-yaml": "^4.1.0",
"mustache": "^4.2.0",
"prompts": "^2.4.2",
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/fs-extra": "^11.0.4",
"@types/js-yaml": "^4.0.9",
"@types/mustache": "^4.2.6",
"@types/node": "^25.5.0",
"@types/prompts": "^2.4.9",
"@typescript-eslint/eslint-plugin": "^8.57.1",
"@typescript-eslint/parser": "^8.57.1",
"eslint": "^10.1.0",
"eslint-config-prettier": "^10.1.8",
"eslint-formatter-unix": "^9.0.1",
"eslint-plugin-prettier": "^5.5.5",
"prettier": "^3.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.1",
},
},
},
"packages": {
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.23.3", "", { "dependencies": { "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.5.3", "", { "dependencies": { "@eslint/core": "^1.1.1" } }, "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw=="],
"@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="],
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" } }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
"@eslint/object-schema": ["@eslint/object-schema@3.0.3", "", {}, "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="],
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="],
"@types/mustache": ["@types/mustache@4.2.6", "", {}, "sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/prompts": ["@types/prompts@2.4.9", "", { "dependencies": { "@types/node": "*", "kleur": "^3.0.3" } }, "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/type-utils": "8.57.2", "@typescript-eslint/utils": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.2", "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.2", "@typescript-eslint/tsconfig-utils": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="],
"acorn": ["acorn@8.16.0", "", { "bin": "bin/acorn" }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.3", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": "bin/eslint.js" }, "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA=="],
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": "bin/cli.js" }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
"eslint-formatter-unix": ["eslint-formatter-unix@9.0.1", "", {}, "sha512-6trzj/OL0Q2B5mw3dqryAmQWzo5vVfL9YkaJdw3laouSgbs83TsSz9GFN+1/7lMUlUkBY+8mVEWelkAQoKnlcA=="],
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.12" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint"] }, "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
"fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mustache": ["mustache@4.2.0", "", { "bin": "bin/mustache" }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.8.1", "", { "bin": "bin/prettier.cjs" }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.57.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"espree/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
}
}

View File

@@ -21,7 +21,10 @@ module.exports = tseslint.config(
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_', 'caughtErrorsIgnorePattern': '^_' }]
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }
]
}
},
{

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"
}

193
main.ts
View File

@@ -5,16 +5,39 @@ import mustache from 'mustache';
import path from 'path';
import { program } from 'commander';
import { log, logSuccess, logInfo, logWarning, logError, colors } from './src/utils/logger';
import {
log,
logSuccess,
logWarning,
logError,
logDetail,
initGenerationLog,
colors
} from './src/utils/logger';
import { checkOpenApiGenerator, installOpenApiGenerator } from './src/utils/openapi-generator';
import { createDirectoryStructure, cleanup } from './src/utils/filesystem';
import { analyzeSwagger } from './src/swagger/analyzer';
import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator';
import { generateCleanArchitecture } from './src/generators/clean-arch.generator';
import {
generateCleanArchitecture,
extractTagsWithOperations
} from './src/generators/clean-arch.generator';
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';
// Desactivar escape HTML para que los literales < y > generen tipos genéricos válidos de TS.
// Disable HTML escaping so that < and > produce valid TypeScript generic types.
(mustache as { escape: (text: string) => string }).escape = function (text: string): string {
return text;
};
@@ -23,17 +46,17 @@ import type { CliOptions } from './src/types';
program
.name('generate-clean-arch')
.description('Generador de código Angular con Clean Architecture desde OpenAPI/Swagger')
.version('1.0.0')
.option('-i, --input <file>', 'Archivo OpenAPI/Swagger (yaml o json)', 'swagger.yaml')
.option('-o, --output <dir>', 'Directorio de salida', './src/app')
.option(
'-t, --templates <dir>',
'Directorio de templates personalizados',
path.join(__dirname, 'templates')
)
.option('--skip-install', 'No instalar dependencias')
.option('--dry-run', 'Simular sin generar archivos')
.description('Angular Clean Architecture code generator from OpenAPI/Swagger')
.version(packageJson.version)
.option('-i, --input <file>', 'OpenAPI/Swagger file (yaml or json)', 'swagger.yaml')
.option('-o, --output <dir>', 'Output directory', './src/app')
.option('-t, --templates <dir>', 'Custom templates directory', path.join(__dirname, 'templates'))
.option('--skip-install', 'Skip dependency installation')
.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>();
@@ -46,65 +69,165 @@ async function main(): Promise<void> {
log(' Angular + Clean Architecture Code Generator', 'cyan');
console.log('='.repeat(60) + '\n');
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}`);
if (!fs.existsSync(options.input)) {
logError(`Archivo no encontrado: ${options.input}`);
logError(`File not found: ${options.input}`);
process.exit(1);
}
logInfo(`Archivo de entrada: ${options.input}`);
logInfo(`Directorio de salida: ${options.output}`);
logInfo(`Templates: ${options.templates}`);
if (options.dryRun) {
logWarning('Modo DRY RUN - No se generarán archivos');
logWarning('DRY RUN mode — no files will be generated');
}
if (!checkOpenApiGenerator()) {
logWarning('OpenAPI Generator CLI no encontrado');
logWarning('OpenAPI Generator CLI not found');
if (!options.skipInstall) {
installOpenApiGenerator();
} else {
logError(
'Instala openapi-generator-cli con: npm install -g @openapitools/openapi-generator-cli'
'Install openapi-generator-cli with: npm install -g @openapitools/openapi-generator-cli'
);
process.exit(1);
}
} else {
logSuccess('OpenAPI Generator CLI encontrado');
logSuccess('OpenAPI Generator CLI found');
}
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) {
logInfo('Finalizando en modo DRY RUN');
logWarning('Finishing in DRY RUN mode');
return;
}
createDirectoryStructure(options.output);
// ── SELECTION: tags and endpoints ─────────────────────────────────────────
let selectionFilter: SelectionFilter = {};
let tagApiKeyMap: Record<string, string>;
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 {
// 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 tempDir = generateCode(options.input, options.templates);
organizeFiles(tempDir, options.output);
addDtoImports(options.output);
generateCleanArchitecture(analysis, options.output, options.templates);
generateCleanArchitecture(
analysis,
options.output,
options.templates,
tagApiKeyMap,
selectionFilter
);
cleanup(tempDir);
const report = generateReport(options.output, analysis);
const noLintResult: LintResult = {
prettier: { ran: false, filesFormatted: 0 },
eslint: { ran: false, filesFixed: 0 }
};
const lintResult = options.skipLint ? noLintResult : lintGeneratedFiles(options.output);
const report = generateReport(options.output, analysis, lintResult);
console.log('\n' + '='.repeat(60));
log(' ✨ Generación completada con éxito', 'green');
log(' ✨ Generation completed successfully', 'green');
console.log('='.repeat(60));
console.log(`\n📊 Resumen:`);
console.log(` - DTOs generados: ${report.structure.dtos}`);
console.log(` - Repositories: ${report.structure.repositories}`);
console.log(` - Mappers: ${report.structure.mappers}`);
console.log(` - Use Cases: ${report.structure.useCases}`);
console.log(` - Providers: ${report.structure.providers}`);
console.log(`\n📁 Archivos generados en: ${colors.cyan}${options.output}${colors.reset}\n`);
console.log(`\n📊 Summary:`);
console.log(` - DTOs generated: ${report.structure.dtos}`);
console.log(` - Repositories: ${report.structure.repositories}`);
console.log(` - Mappers: ${report.structure.mappers}`);
console.log(` - Use Cases: ${report.structure.useCases}`);
console.log(` - Providers: ${report.structure.providers}`);
console.log(` - Mocks: ${report.structure.mocks}`);
console.log(`\n📁 Files generated in: ${colors.cyan}${options.output}${colors.reset}\n`);
}
main().catch((error: unknown) => {
const err = error as Error;
logError(`Error fatal: ${err.message}`);
logError(`Fatal error: ${err.message}`);
console.error(error);
process.exit(1);
});

1776
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,27 @@
{
"name": "@blas/openapi-clean-arch-generator",
"version": "0.0.1-alpha",
"description": "Generador de código Angular con Clean Architecture desde OpenAPI/Swagger",
"name": "@0kmpo/openapi-clean-arch-generator",
"version": "1.3.10",
"description": "Angular Clean Architecture generator from OpenAPI/Swagger",
"main": "dist/main.js",
"bin": {
"generate-clean-arch": "./dist/main.js"
},
"scripts": {
"build": "tsc && cp -r templates dist/",
"postbuild": "node -e \"const fs=require('fs'); const f='dist/main.js'; const c=fs.readFileSync(f,'utf8'); if(!c.startsWith('#!/usr/bin/env node')) fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c); fs.chmodSync(f, '755');\"",
"prepublishOnly": "npm run build",
"generate": "node dist/main.js",
"generate:dev": "ts-node main.ts",
"lint": "eslint 'main.ts' 'src/**/*.ts' -f unix",
"lint:fix": "eslint 'main.ts' 'src/**/*.ts' --fix -f unix",
"postbuild": "bun -e \"const fs=require('fs'); const f='dist/main.js'; const c=fs.readFileSync(f,'utf8'); if(!c.startsWith('#!/usr/bin/env node')) fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c); fs.chmodSync(f, '755');\"",
"prepublishOnly": "bun run build",
"generate": "bun dist/main.js",
"generate:dev": "bun main.ts",
"binaries": "bun run binary:mac-arm64 && bun run binary:mac-x64 && bun run binary:linux-x64 && bun run binary:linux-arm64 && bun run binary:windows",
"binary:mac-arm64": "bun build --compile --target=bun-darwin-arm64 --outfile dist/bin/generate-clean-arch-macos-arm64 main.ts",
"binary:mac-x64": "bun build --compile --target=bun-darwin-x64 --outfile dist/bin/generate-clean-arch-macos-x64 main.ts",
"binary:linux-x64": "bun build --compile --target=bun-linux-x64 --outfile dist/bin/generate-clean-arch-linux-x64 main.ts",
"binary:linux-arm64": "bun build --compile --target=bun-linux-arm64 --outfile dist/bin/generate-clean-arch-linux-arm64 main.ts",
"binary:windows": "bun build --compile --target=bun-windows-x64 --outfile dist/bin/generate-clean-arch-windows-x64.exe main.ts",
"lint": "bunx --bun eslint 'main.ts' 'src/**/*.ts' -f unix",
"lint:fix": "bunx --bun eslint 'main.ts' 'src/**/*.ts' --fix -f unix",
"format": "prettier --write .",
"setup": "npm install -g @openapitools/openapi-generator-cli"
"setup": "bun add -g @openapitools/openapi-generator-cli"
},
"keywords": [
"openapi",
@@ -33,18 +39,24 @@
}
],
"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",
"fs-extra": "^11.2.0",
"js-yaml": "^4.1.0",
"mustache": "^4.2.0"
"mustache": "^4.2.0",
"prompts": "^2.4.2"
},
"engines": {
"node": ">=14.0.0"
"bun": ">=1.0.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -52,6 +64,7 @@
"@types/js-yaml": "^4.0.9",
"@types/mustache": "^4.2.6",
"@types/node": "^25.5.0",
"@types/prompts": "^2.4.9",
"@typescript-eslint/eslint-plugin": "^8.57.1",
"@typescript-eslint/parser": "^8.57.1",
"eslint": "^10.1.0",
@@ -59,7 +72,6 @@
"eslint-formatter-unix": "^9.0.1",
"eslint-plugin-prettier": "^5.5.5",
"prettier": "^3.8.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.1"
}

View File

@@ -1,37 +1,88 @@
import fs from 'fs-extra';
import path from 'path';
import mustache from 'mustache';
import { logStep, logSuccess, logInfo } from '../utils/logger';
import { logStep, logSuccess, logDetail } from '../utils/logger';
import { mapSwaggerTypeToTs } from '../utils/type-mapper';
import { toCamelCase } from '../utils/name-formatter';
import { resolveMockValue } from '../utils/mock-value-resolver';
import type {
SwaggerAnalysis,
OpenApiSchema,
OpenApiOperation,
TagOperation,
TagSummary,
SelectionFilter,
GeneratedCount
} from '../types';
/** Genera todos los artefactos de Clean Architecture (modelos, mappers, repos, use cases, providers) usando Mustache. */
/**
* Extracts the unique tags (in order of appearance) from a SwaggerAnalysis.
* Only endpoints that have at least one tag are considered; the first tag is used.
*/
export function extractTagsFromAnalysis(analysis: SwaggerAnalysis): string[] {
const seen = new Set<string>();
const tags: string[] = [];
Object.values(analysis.paths).forEach((pathObj) => {
Object.values(pathObj as Record<string, unknown>).forEach((opRaw) => {
const op = opRaw as OpenApiOperation;
if (op.tags && op.tags.length > 0) {
const tag = op.tags[0];
if (!seen.has(tag)) {
seen.add(tag);
tags.push(tag);
}
}
});
});
return tags;
}
/**
* Extracts all tags with their operations summary for the interactive selection screen.
*/
export function extractTagsWithOperations(analysis: SwaggerAnalysis): TagSummary[] {
const map = new Map<string, TagSummary>();
Object.entries(analysis.paths).forEach(([pathKey, pathObj]) => {
Object.entries(pathObj as Record<string, unknown>).forEach(([method, opRaw]) => {
const op = opRaw as OpenApiOperation;
if (!op.tags?.length) return;
const tag = op.tags[0];
if (!map.has(tag)) map.set(tag, { tag, operations: [] });
map.get(tag)!.operations.push({
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
method: method.toUpperCase(),
path: pathKey,
summary: op.summary || ''
});
});
});
return [...map.values()];
}
/** Generates all Clean Architecture artefacts (models, mappers, repos, use cases, providers) using Mustache. */
export function generateCleanArchitecture(
analysis: SwaggerAnalysis,
outputDir: string,
templatesDir: string
templatesDir: string,
tagApiKeyMap: Record<string, string> = {},
selectionFilter: SelectionFilter = {}
): GeneratedCount {
logStep('Generando artefactos de Clean Architecture usando Mustache...');
logStep('Generating Clean Architecture artefacts using Mustache...');
const generatedCount: GeneratedCount = {
models: 0,
repositories: 0,
mappers: 0,
useCases: 0,
providers: 0
providers: 0,
mocks: 0,
specs: 0
};
const schemas =
(analysis.swagger as { components?: { schemas?: Record<string, unknown> } }).components
?.schemas || {};
// 1. Generar Modelos, Entidades y Mappers a partir de Schemas
// 1. Generate Models, Entities and Mappers from Schemas
Object.keys(schemas).forEach((schemaName) => {
const baseName = schemaName.replace(/Dto$/, '');
@@ -106,7 +157,7 @@ export function generateCleanArchitecture(
const destPath = path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.ts`);
fs.writeFileSync(destPath, output);
generatedCount.models++;
logInfo(` ${toCamelCase(baseName)}.model.ts${path.relative(process.cwd(), destPath)}`);
logDetail('generate', `model-entity${path.relative(process.cwd(), destPath)}`);
}
// Mapper
@@ -118,9 +169,71 @@ export function generateCleanArchitecture(
fs.writeFileSync(destPath, output);
generatedCount.mappers++;
}
// DTO mock — values resolved from raw schema (example, format, type)
const dtoMockVarsMap = Object.keys(rawProperties).map((k) => ({
name: k,
mockValue: resolveMockValue(k, rawProperties[k], 'dto')
}));
const dtoMockImports = [...referencedTypes]
.filter(Boolean)
.map((name) => ({ classname: name, classFilename: toCamelCase(name) }));
const dtoMockViewData = {
models: [
{
model: {
classname: baseName,
classFilename: toCamelCase(baseName),
classVarName: toCamelCase(baseName),
mockImports: dtoMockImports,
vars: dtoMockVarsMap
}
}
]
};
renderTemplate(
templatesDir,
'dto.mock.mustache',
dtoMockViewData,
path.join(outputDir, 'data/dtos', `${toCamelCase(baseName)}.dto.mock.ts`),
generatedCount,
'mocks'
);
// Model mock — delegates to mapper + DTO mock (no property values needed)
renderTemplate(
templatesDir,
'model.mock.mustache',
modelViewData,
path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.mock.ts`),
generatedCount,
'mocks'
);
// Model spec
renderTemplate(
templatesDir,
'model-entity.spec.mustache',
modelViewData,
path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.spec.ts`),
generatedCount,
'specs'
);
// Mapper spec
renderTemplate(
templatesDir,
'mapper.spec.mustache',
mapperViewData,
path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.spec.ts`),
generatedCount,
'specs'
);
});
// 2. Generar Casos de Uso y Repositorios a partir de Paths/Tags
// 2. Generate Use Cases and Repositories from Paths/Tags
const tagsMap: Record<string, TagOperation[]> = {};
Object.keys(analysis.paths).forEach((pathKey) => {
@@ -135,7 +248,8 @@ export function generateCleanArchitecture(
paramName: p.name,
dataType: mapSwaggerTypeToTs(p.schema?.type || ''),
description: p.description || '',
required: p.required
required: p.required,
testValue: resolveTestParamValue(mapSwaggerTypeToTs(p.schema?.type || ''))
}));
if (op.requestBody) {
@@ -149,14 +263,19 @@ export function generateCleanArchitecture(
paramName: 'body',
dataType: bodyType,
description: op.requestBody.description || '',
required: true
required: true,
testValue: resolveTestParamValue(bodyType)
});
}
let returnType = 'void';
let returnBaseType = 'void';
let isListContainer = false;
const responseSchema = op.responses?.['200']?.content?.['application/json']?.schema;
const successCode = ['200', '201', '202', '203'].find((code) => op.responses?.[code]);
const responseSchema =
successCode !== undefined
? op.responses?.[successCode]?.content?.['application/json']?.schema
: undefined;
if (responseSchema) {
if (responseSchema.$ref) {
returnType = responseSchema.$ref.split('/').pop() || 'unknown';
@@ -168,25 +287,31 @@ export function generateCleanArchitecture(
}
}
const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
const hasBodyParam = !!op.requestBody;
tagsMap[tag].push({
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
summary: op.summary || '',
notes: op.description || '',
httpMethod: method.toLowerCase(),
uppercaseHttpMethod: method.toUpperCase(),
path: pathKey,
allParams: allParams.map((p, i: number) => ({
...p,
'-last': i === allParams.length - 1
})),
hasQueryParams: (op.parameters || []).some((p) => p.in === 'query'),
hasQueryParams,
queryParams: (op.parameters || [])
.filter((p) => p.in === 'query')
.map((p, i: number, arr: unknown[]) => ({
paramName: p.name,
'-last': i === arr.length - 1
})),
hasBodyParam: !!op.requestBody,
hasBodyParam,
bodyParam: 'body',
hasOptions: hasQueryParams || hasBodyParam,
hasBothParamsAndBody: hasQueryParams && hasBodyParam,
returnType: returnType !== 'void' ? returnType : false,
returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
returnTypeVarName: returnType !== 'void' ? toCamelCase(returnType) : false,
@@ -198,7 +323,18 @@ export function generateCleanArchitecture(
});
});
// Generar por cada Tag
if (Object.keys(selectionFilter).length > 0) {
Object.keys(tagsMap).forEach((tag) => {
if (!selectionFilter[tag]) {
delete tagsMap[tag];
} else {
tagsMap[tag] = tagsMap[tag].filter((op) => selectionFilter[tag].includes(op.nickname));
if (tagsMap[tag].length === 0) delete tagsMap[tag];
}
});
}
// Generate per tag
Object.keys(tagsMap).forEach((tag) => {
const returnImports: { classname: string; classFilename: string; classVarName: string }[] = [];
const paramImports: { classname: string; classFilename: string; classVarName: string }[] = [];
@@ -236,7 +372,9 @@ export function generateCleanArchitecture(
// Return-type-only imports — for repo impl (Dto + Entity + Mapper)
returnImports,
// Param-only imports — for repo impl (Entity only, no Dto/Mapper)
paramImports
paramImports,
// Environment API key for the repository base URL (e.g. "aprovalmApi")
environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
}
}
]
@@ -296,15 +434,72 @@ export function generateCleanArchitecture(
generatedCount,
'providers'
);
// Mocks — repository impl, use-cases impl, repository provider, use-cases provider
renderTemplate(
templatesDir,
'api.repository.impl.mock.mustache',
apiViewData,
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.mock.ts`),
generatedCount,
'mocks'
);
renderTemplate(
templatesDir,
'api.use-cases.mock.mustache',
apiViewData,
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.mock.ts`),
generatedCount,
'mocks'
);
renderTemplate(
templatesDir,
'repository.provider.mock.mustache',
apiViewData,
path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.mock.ts`),
generatedCount,
'mocks'
);
renderTemplate(
templatesDir,
'use-cases.provider.mock.mustache',
apiViewData,
path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.mock.ts`),
generatedCount,
'mocks'
);
// Repository impl spec
renderTemplate(
templatesDir,
'api.repository.impl.spec.mustache',
apiViewData,
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.spec.ts`),
generatedCount,
'specs'
);
// Use-cases impl spec
renderTemplate(
templatesDir,
'api.use-cases.impl.spec.mustache',
apiViewData,
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.spec.ts`),
generatedCount,
'specs'
);
});
logSuccess(
`${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers generados con Mustache`
`${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers, ${generatedCount.mocks} Mocks, ${generatedCount.specs} Specs generated`
);
return generatedCount;
}
/** Renderiza un template Mustache e incrementa el contador correspondiente. */
/** Renders a Mustache template and increments the corresponding counter. */
function renderTemplate(
templatesDir: string,
templateName: string,
@@ -319,5 +514,24 @@ function renderTemplate(
const output = mustache.render(template, viewData);
fs.writeFileSync(destPath, output);
counter[key]++;
logDetail(
'generate',
`${templateName.replace('.mustache', '')}${path.relative(process.cwd(), destPath)}`
);
}
}
/** Resolves a simple test value literal for a given TypeScript type. */
function resolveTestParamValue(dataType: string): string {
switch (dataType) {
case 'string':
return "'test'";
case 'number':
return '1';
case 'boolean':
return 'true';
default:
if (dataType.endsWith('[]')) return '[]';
return '{} as any';
}
}

View File

@@ -1,11 +1,11 @@
import { execSync } from 'child_process';
import fs from 'fs-extra';
import path from 'path';
import { logStep, logSuccess, logError, logInfo } from '../utils/logger';
import { logStep, logSuccess, logError, logDetail } from '../utils/logger';
/** Invoca `openapi-generator-cli` para generar DTOs en un directorio temporal. */
/** Invokes `openapi-generator-cli` to generate DTOs into a temporary directory. */
export function generateCode(swaggerFile: string, templatesDir: string): string {
logStep('Generando código desde OpenAPI...');
logStep('Generating code from OpenAPI spec...');
const tempDir = path.join(process.cwd(), '.temp-generated');
@@ -22,12 +22,12 @@ export function generateCode(swaggerFile: string, templatesDir: string): string
-o "${tempDir}" \
--additional-properties=ngVersion=17.0.0,modelFileSuffix=.dto,modelNameSuffix=Dto`;
execSync(command, { stdio: 'inherit' });
logSuccess('Código generado correctamente');
execSync(command, { stdio: 'pipe' });
logSuccess('Code generated successfully');
return tempDir;
} catch (_error) {
logError('Error al generar código');
logError('Error generating code');
if (fs.existsSync(tempDir)) {
fs.removeSync(tempDir);
}
@@ -35,9 +35,9 @@ export function generateCode(swaggerFile: string, templatesDir: string): string
}
}
/** Copia los DTOs generados desde el directorio temporal al directorio de salida. */
/** Copies the generated DTOs from the temporary directory to the output directory. */
export function organizeFiles(tempDir: string, outputDir: string): void {
logStep('Organizando archivos DTO generados...');
logStep('Organising generated DTO files...');
const sourceDir = path.join(tempDir, 'model');
const destDir = path.join(outputDir, 'data/dtos');
@@ -51,19 +51,18 @@ export function organizeFiles(tempDir: string, outputDir: string): void {
files.forEach((file) => {
const sourcePath = path.join(sourceDir, file);
const destPath = path.join(destDir, file);
fs.copySync(sourcePath, destPath);
filesMoved++;
logInfo(` ${file}${path.relative(process.cwd(), destPath)}`);
logDetail('dto', `${file}${path.relative(process.cwd(), destPath)}`);
});
}
logSuccess(`${filesMoved} DTOs movidos correctamente`);
logSuccess(`${filesMoved} DTOs moved successfully`);
}
/** Post-procesa los DTOs generados añadiendo imports y normalizando Array<T> → T[]. */
/** Post-processes the generated DTOs: adds cross-DTO imports and normalises Array<T> → T[]. */
export function addDtoImports(outputDir: string): void {
logStep('Añadiendo imports a los DTOs generados...');
logStep('Post-processing generated DTOs...');
const dtosDir = path.join(outputDir, 'data/dtos');
@@ -119,9 +118,9 @@ export function addDtoImports(outputDir: string): void {
if (content !== originalContent) {
fs.writeFileSync(filePath, content);
filesProcessed++;
logInfo(` Procesado ${file}`);
logDetail('dto', `Post-processed ${file} (added ${imports.length} import(s))`);
}
});
logSuccess(`Imports añadidos a ${filesProcessed} DTOs`);
logSuccess(`${filesProcessed} DTOs post-processed`);
}

View File

@@ -0,0 +1,124 @@
import fs from 'fs-extra';
import path from 'path';
import { spawnSync } from 'child_process';
import { logStep, logSuccess, logWarning, logDetail } from '../utils/logger';
import type { LintResult } from '../types';
/**
* Walks up the directory tree from `startDir` to find the nearest
* directory containing a `package.json` (i.e. the project root).
* Returns `null` if none is found before reaching the filesystem root.
*/
function findProjectRoot(startDir: string): string | null {
let current = path.resolve(startDir);
while (true) {
if (fs.existsSync(path.join(current, 'package.json'))) return current;
const parent = path.dirname(current);
if (parent === current) return null;
current = parent;
}
}
/**
* Collects all `.ts` files recursively inside a directory.
*/
function collectTsFiles(dir: string): string[] {
if (!fs.existsSync(dir)) return [];
const results: string[] = [];
fs.readdirSync(dir).forEach((entry) => {
const full = path.join(dir, entry);
if (fs.statSync(full).isDirectory()) {
results.push(...collectTsFiles(full));
} else if (entry.endsWith('.ts')) {
results.push(full);
}
});
return results;
}
/**
* Runs a command synchronously. Only prints to console on fatal failure (exit >= 2).
* Exit code 1 from ESLint means "warnings remain after --fix" — not a fatal error.
* Returns captured output for logging to file.
*/
function run(cmd: string, args: string[], cwd: string): { success: boolean; output: string } {
const result = spawnSync(cmd, args, { cwd, encoding: 'utf8', shell: true });
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
const fatalError = result.status === null || result.status >= 2;
if (fatalError) {
if (result.stderr) process.stderr.write(result.stderr);
if (result.stdout) process.stdout.write(result.stdout);
}
return { success: !fatalError, output };
}
/**
* Runs Prettier and ESLint (--fix) on all generated `.ts` files inside `outputDir`.
* Both tools are looked up via `npx` in the nearest project root so that the
* target Angular project's own configuration and plugins are used.
*
* - Prettier: always attempted; logs a warning if not found.
* - ESLint: optional; silently skipped if no config is found in the project root.
*
* Returns a `LintResult` with the outcome of each tool for inclusion in the report.
*/
export function lintGeneratedFiles(outputDir: string): LintResult {
logStep('Linting generated files...');
const result: LintResult = {
prettier: { ran: false, filesFormatted: 0 },
eslint: { ran: false, filesFixed: 0 }
};
const projectRoot = findProjectRoot(outputDir);
if (!projectRoot) {
logWarning('Could not locate a project root (package.json). Skipping lint.');
return result;
}
const files = collectTsFiles(outputDir);
if (files.length === 0) {
logWarning('No TypeScript files found in output directory. Skipping lint.');
return result;
}
logDetail('lint', `Project root: ${projectRoot}`);
logDetail('lint', `Files to process: ${files.length}`);
const relativePaths = files.map((f) => path.relative(projectRoot, f));
// --- Prettier ---
const prettier = run('npx', ['prettier', '--write', ...relativePaths], projectRoot);
if (prettier.output) logDetail('prettier', prettier.output);
if (prettier.success) {
result.prettier = { ran: true, filesFormatted: files.length };
logSuccess(`Prettier formatted ${files.length} files`);
} else {
logWarning('Prettier not available or encountered errors. Skipping formatting.');
}
// --- ESLint (only if a config exists in the project root) ---
const hasEslintConfig =
fs.existsSync(path.join(projectRoot, 'eslint.config.js')) ||
fs.existsSync(path.join(projectRoot, 'eslint.config.mjs')) ||
fs.existsSync(path.join(projectRoot, '.eslintrc.js')) ||
fs.existsSync(path.join(projectRoot, '.eslintrc.json')) ||
fs.existsSync(path.join(projectRoot, '.eslintrc.yml')) ||
fs.existsSync(path.join(projectRoot, '.eslintrc.yaml'));
if (!hasEslintConfig) {
logWarning('No ESLint config found in project root. Skipping ESLint fix.');
return result;
}
const eslint = run('npx', ['eslint', '--fix', ...relativePaths], projectRoot);
if (eslint.output) logDetail('eslint', eslint.output);
if (eslint.success) {
result.eslint = { ran: true, filesFixed: files.length };
logSuccess(`ESLint fixed ${files.length} files`);
} else {
logWarning('ESLint reported errors that could not be auto-fixed. Review the output above.');
}
return result;
}

View File

@@ -1,17 +1,53 @@
import fs from 'fs-extra';
import path from 'path';
import { logStep, logSuccess } from '../utils/logger';
import type { SwaggerAnalysis, GenerationReport } from '../types';
import type { SwaggerAnalysis, GenerationReport, LintResult } from '../types';
/** Genera y persiste el reporte `generation-report.json` con las estadísticas del proceso. */
export function generateReport(outputDir: string, analysis: SwaggerAnalysis): GenerationReport {
logStep('Generando reporte de generación...');
/** Counts files ending with `.mock.ts` in a directory (returns 0 if directory does not exist). */
function countMockFiles(dir: string): number {
try {
return fs.readdirSync(dir).filter((f) => f.endsWith('.mock.ts')).length;
} catch {
return 0;
}
}
/** Counts files ending with `.spec.ts` in a directory (returns 0 if directory does not exist). */
function countSpecFiles(dir: string): number {
try {
return fs.readdirSync(dir).filter((f) => f.endsWith('.spec.ts')).length;
} catch {
return 0;
}
}
/** Generates and persists the `generation-report.json` file with process statistics. */
export function generateReport(
outputDir: string,
analysis: SwaggerAnalysis,
lintResult: LintResult
): GenerationReport {
logStep('Generating report...');
const tags = Array.isArray(analysis.tags) ? analysis.tags : [];
const tagDetails = tags.map((tag: unknown) => {
const t = tag as { name: string; description?: string };
const endpointCount = Object.values(analysis.paths).filter((pathObj) =>
Object.values(pathObj as Record<string, unknown>).some((op) => {
const operation = op as { tags?: string[] };
return operation.tags?.includes(t.name);
})
).length;
return { name: t.name, description: t.description || '', endpoints: endpointCount };
});
const report: GenerationReport = {
timestamp: new Date().toISOString(),
tags: analysis.tags.length,
endpoints: Object.keys(analysis.paths).length,
tagDetails,
outputDirectory: outputDir,
linting: lintResult,
structure: {
dtos: fs.readdirSync(path.join(outputDir, 'data/dtos')).length,
repositories: fs.readdirSync(path.join(outputDir, 'data/repositories')).length,
@@ -19,14 +55,26 @@ export function generateReport(outputDir: string, analysis: SwaggerAnalysis): Ge
useCases: fs.readdirSync(path.join(outputDir, 'domain/use-cases')).length,
providers:
fs.readdirSync(path.join(outputDir, 'di/repositories')).length +
fs.readdirSync(path.join(outputDir, 'di/use-cases')).length
fs.readdirSync(path.join(outputDir, 'di/use-cases')).length,
mocks:
countMockFiles(path.join(outputDir, 'data/dtos')) +
countMockFiles(path.join(outputDir, 'data/repositories')) +
countMockFiles(path.join(outputDir, 'di/repositories')) +
countMockFiles(path.join(outputDir, 'di/use-cases')) +
countMockFiles(path.join(outputDir, 'domain/use-cases')) +
countMockFiles(path.join(outputDir, 'entities/models')),
specs:
countSpecFiles(path.join(outputDir, 'entities/models')) +
countSpecFiles(path.join(outputDir, 'data/mappers')) +
countSpecFiles(path.join(outputDir, 'data/repositories')) +
countSpecFiles(path.join(outputDir, 'domain/use-cases'))
}
};
const reportPath = path.join(process.cwd(), 'generation-report.json');
fs.writeJsonSync(reportPath, report, { spaces: 2 });
logSuccess(`Reporte guardado en: ${reportPath}`);
logSuccess(`Report saved to: ${reportPath}`);
return report;
}

View File

@@ -1,11 +1,11 @@
import fs from 'fs-extra';
import yaml from 'js-yaml';
import { logStep, logInfo, logError } from '../utils/logger';
import { logStep, logSuccess, logError, logDetail } from '../utils/logger';
import type { SwaggerAnalysis } from '../types';
/** Parsea un archivo OpenAPI/Swagger y extrae tags, paths y el documento completo. */
/** Parses an OpenAPI/Swagger file and extracts tags, paths and the full document. */
export function analyzeSwagger(swaggerFile: string): SwaggerAnalysis {
logStep('Analizando archivo OpenAPI...');
logStep('Analysing OpenAPI file...');
try {
const fileContent = fs.readFileSync(swaggerFile, 'utf8');
@@ -14,18 +14,19 @@ export function analyzeSwagger(swaggerFile: string): SwaggerAnalysis {
const tags = Array.isArray(swagger.tags) ? swagger.tags : [];
const paths = (swagger.paths as Record<string, unknown>) || {};
logInfo(`Encontrados ${tags.length} tags en el API`);
logInfo(`Encontrados ${Object.keys(paths).length} endpoints`);
logDetail('analyze', `Input: ${swaggerFile}`);
logDetail('analyze', `Found ${tags.length} tags, ${Object.keys(paths).length} endpoints`);
tags.forEach((tag: unknown) => {
const t = tag as { name: string; description?: string };
logInfo(` - ${t.name}: ${t.description || 'Sin descripción'}`);
logDetail('analyze', ` - ${t.name}: ${t.description || 'No description'}`);
});
logSuccess(`${tags.length} tags, ${Object.keys(paths).length} endpoints found`);
return { tags, paths, swagger };
} catch (error: unknown) {
const err = error as Error;
logError(`Error al leer el archivo Swagger: ${err.message}`);
logError(`Error reading the Swagger file: ${err.message}`);
process.exit(1);
}
}

View File

@@ -1,6 +1,6 @@
/**
* Opciones recibidas desde la línea de comandos (Commander).
* Desacoplada del framework CLI para permitir su uso desde un backend u otro entrypoint.
* Options received from the command line (Commander).
* Decoupled from the CLI framework to allow use from a backend or other entry points.
*/
export interface CliOptions {
input: string;
@@ -8,4 +8,29 @@ export interface CliOptions {
templates: string;
skipInstall?: boolean;
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>;
}

View File

@@ -1,5 +1,5 @@
/**
* Contadores acumulativos de artefactos generados durante el proceso.
* Cumulative counters of artifacts generated during the process.
*/
export interface GeneratedCount {
models: number;
@@ -7,21 +7,44 @@ export interface GeneratedCount {
mappers: number;
useCases: number;
providers: number;
mocks: number;
specs: number;
}
/**
* Reporte final de generación que se persiste como `generation-report.json`.
* Result returned by the lint/format step.
*/
export interface LintResult {
prettier: { ran: boolean; filesFormatted: number };
eslint: { ran: boolean; filesFixed: number };
}
/**
* Per-tag summary included in the generation report.
*/
export interface TagDetail {
name: string;
description: string;
endpoints: number;
}
/**
* Final generation report persisted as `generation-report.json`.
*/
export interface GenerationReport {
timestamp: string;
tags: number;
endpoints: number;
tagDetails: TagDetail[];
outputDirectory: string;
linting: LintResult;
structure: {
dtos: number;
repositories: number;
mappers: number;
useCases: number;
providers: number;
mocks: number;
specs: number;
};
}

View File

@@ -1,6 +1,6 @@
/**
* @module types
* @description Barrel que re-exporta todos los tipos e interfaces compartidos del proyecto.
* @description Barrel that re-exports all shared types and interfaces for the project.
*/
export * from './cli.types';
export * from './swagger.types';

View File

@@ -1,15 +1,41 @@
/**
* Representación simplificada de un schema de componente OpenAPI.
* Se utiliza para generar modelos (entidades) y mappers.
* Summary of a single endpoint for display on the interactive selection screen.
*/
export interface OperationSummary {
nickname: string;
method: string;
path: string;
summary: string;
}
/**
* Tag with its summarised endpoints, used on the interactive selection screen.
*/
export interface TagSummary {
tag: string;
operations: OperationSummary[];
}
/**
* Selection filter map: tag → array of selected operation nicknames.
*/
export type SelectionFilter = Record<string, string[]>;
/**
* Simplified representation of an OpenAPI component schema.
* Used to generate domain models (entities) and mappers.
*/
export interface OpenApiSchema {
properties?: Record<
string,
{
type?: string;
format?: string;
description?: string;
example?: unknown;
enum?: unknown[];
$ref?: string;
items?: { $ref?: string };
items?: { $ref?: string; type?: string };
}
>;
required?: string[];
@@ -17,8 +43,8 @@ export interface OpenApiSchema {
}
/**
* Representación de una operación OpenAPI (GET, POST, etc.) dentro de un path.
* Contiene la información necesaria para generar repositorios y casos de uso.
* Representation of an OpenAPI operation (GET, POST, etc.) within a path.
* Contains the information needed to generate repositories and use cases.
*/
export interface OpenApiOperation {
tags?: string[];
@@ -62,8 +88,7 @@ export interface OpenApiOperation {
}
/**
* Operación normalizada y lista para ser consumida por los templates Mustache.
* Cada instancia representa un endpoint agrupado bajo un tag del API.
* A single parameter of a normalised API operation, ready for Mustache template consumption.
*/
export interface TagOperationParam {
paramName: string;
@@ -71,19 +96,27 @@ export interface TagOperationParam {
description: string;
required: boolean;
'-last': boolean;
testValue?: string;
}
/**
* Normalised operation ready to be consumed by Mustache templates.
* Each instance represents an endpoint grouped under an API tag.
*/
export interface TagOperation {
nickname: string;
summary: string;
notes: string;
httpMethod: string;
uppercaseHttpMethod: string;
path: string;
allParams: TagOperationParam[];
hasQueryParams: boolean;
queryParams: unknown[];
hasBodyParam: boolean;
bodyParam: string;
hasOptions: boolean;
hasBothParamsAndBody: boolean;
returnType: string | boolean;
returnBaseType: string | boolean;
returnTypeVarName: string | boolean;

View File

@@ -1,6 +1,6 @@
/**
* Resultado del análisis de un archivo OpenAPI/Swagger.
* Contiene las estructuras crudas extraídas del spec para su posterior procesamiento.
* Result of parsing an OpenAPI/Swagger file.
* Contains the raw structures extracted from the spec for further processing.
*/
export interface SwaggerAnalysis {
tags: unknown[];

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;
}

View File

@@ -0,0 +1,53 @@
import fs from 'fs-extra';
import path from 'path';
export interface ApiKeyInfo {
key: string;
url?: string;
}
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.angular', 'coverage', '.cache']);
/**
* Recursively searches for an `environment.ts` file starting from `dir`,
* up to `maxDepth` directory levels deep.
*/
export function findEnvironmentFile(dir: string, maxDepth = 8, currentDepth = 0): string | null {
if (currentDepth > maxDepth) return null;
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name)) continue;
const fullPath = path.join(dir, entry.name);
if (entry.isFile() && entry.name === 'environment.ts') {
return fullPath;
}
if (entry.isDirectory()) {
const found = findEnvironmentFile(fullPath, maxDepth, currentDepth + 1);
if (found) return found;
}
}
} catch {
//bypass errors
}
return null;
}
/**
* Parses environment.ts content and returns all top-level keys that contain "api"
* (case-insensitive), along with their `url` value if present.
*/
export function parseApiKeys(content: string): ApiKeyInfo[] {
const result: ApiKeyInfo[] = [];
const keyRegex = /^ {2}(\w*[Aa][Pp][Ii]\w*)\s*:/gm;
let match: RegExpExecArray | null;
while ((match = keyRegex.exec(content)) !== null) {
const key = match[1];
const afterKey = content.slice(match.index);
const urlMatch = afterKey.match(/url:\s*['"`]([^'"`\n]+)['"`]/);
result.push({ key, url: urlMatch?.[1] });
}
return result;
}

View File

@@ -2,7 +2,7 @@ import fs from 'fs-extra';
import path from 'path';
import { logSuccess, logInfo } from './logger';
/** Crea la estructura de directorios necesaria para Clean Architecture (idempotente). */
/** Creates the required Clean Architecture directory structure (idempotent). */
export function createDirectoryStructure(baseDir: string): void {
const dirs = [
path.join(baseDir, 'data/dtos'),
@@ -19,13 +19,13 @@ export function createDirectoryStructure(baseDir: string): void {
fs.ensureDirSync(dir);
});
logSuccess('Estructura de directorios creada');
logSuccess('Directory structure created');
}
/** Elimina un directorio temporal y todo su contenido. */
/** Removes a temporary directory and all its contents. */
export function cleanup(tempDir: string): void {
if (fs.existsSync(tempDir)) {
fs.removeSync(tempDir);
logInfo('Archivos temporales eliminados');
logInfo('Temporary files removed');
}
}

View File

@@ -1,3 +1,5 @@
import fs from 'fs-extra';
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
@@ -10,32 +12,47 @@ const colors = {
type Color = keyof typeof colors;
/** Imprime un mensaje en consola con el color ANSI indicado. */
let _logFilePath: string | null = null;
/** Initialises the generation log file, overwriting any previous run. */
export function initGenerationLog(filePath: string): void {
_logFilePath = filePath;
fs.writeFileSync(filePath, `Generation log — ${new Date().toISOString()}\n${'='.repeat(60)}\n`);
}
/** Writes a detailed entry to the generation log file (not to console). */
export function logDetail(category: string, message: string): void {
if (!_logFilePath) return;
const line = `[${new Date().toISOString()}] [${category.toUpperCase().padEnd(8)}] ${message}\n`;
fs.appendFileSync(_logFilePath, line);
}
/** Prints a console message with the given ANSI colour. */
export function log(message: string, color: Color = 'reset'): void {
console.log(`${colors[color]}${message}${colors.reset}`);
}
/** Imprime un mensaje de éxito (verde). */
/** Prints a success message (green). */
export function logSuccess(message: string): void {
log(`${message}`, 'green');
}
/** Imprime un mensaje informativo (azul). */
/** Prints an informational message (blue). */
export function logInfo(message: string): void {
log(` ${message}`, 'blue');
}
/** Imprime un mensaje de advertencia (amarillo). */
/** Prints a warning message (yellow). */
export function logWarning(message: string): void {
log(`⚠️ ${message}`, 'yellow');
}
/** Imprime un mensaje de error (rojo). */
/** Prints an error message (red). */
export function logError(message: string): void {
log(`${message}`, 'red');
}
/** Imprime un encabezado de paso/etapa (cian). */
/** Prints a step/stage header (cyan). */
export function logStep(message: string): void {
log(`\n🚀 ${message}`, 'cyan');
}

View File

@@ -0,0 +1,70 @@
/**
* Resolves a TypeScript literal string to use as a mock value for a single schema property.
*
* Priority chain:
* $ref mock call → array $ref mock call → enum[0] → example → format fallback → type default
*
* @param propName Property name (used for format heuristics such as "email").
* @param prop Raw OpenAPI property definition.
* @param context 'dto' generates `mockFooDto()`, 'model' generates `mockFooModel()`.
*/
export function resolveMockValue(
propName: string,
prop: {
type?: string;
format?: string;
example?: unknown;
enum?: unknown[];
$ref?: string;
items?: { $ref?: string; type?: string };
},
context: 'dto' | 'model' = 'dto'
): string {
const suffix = context === 'dto' ? 'Dto' : 'Model';
// 1. Direct $ref → call the referenced mock factory
if (prop.$ref) {
const refName = prop.$ref.split('/').pop()!;
return `mock${refName}${suffix}()`;
}
// 2. Array of $ref → wrap referenced mock in an array
if (prop.type === 'array' && prop.items?.$ref) {
const refName = prop.items.$ref.split('/').pop()!;
return `[mock${refName}${suffix}()]`;
}
// 3. Array of primitives
if (prop.type === 'array') return '[]';
// 4. Enum → first declared value
if (prop.enum?.length) {
const first = prop.enum[0];
return typeof first === 'string' ? `'${first}'` : String(first);
}
// 5. Example value from the swagger spec (highest fidelity)
if (prop.example !== undefined) return formatLiteral(prop.example);
// 6. Format-aware fallbacks (when no example is provided)
if (prop.format === 'date-time') return `'2024-01-01T00:00:00.000Z'`;
if (prop.format === 'date') return `'2024-01-01'`;
if (prop.format === 'uuid') return `'00000000-0000-0000-0000-000000000000'`;
if (prop.format === 'uri') return `'https://example.com'`;
if (prop.format === 'email' || propName.toLowerCase().includes('email'))
return `'user@example.com'`;
// 7. Type defaults
if (prop.type === 'string') return `'value'`;
if (prop.type === 'integer' || prop.type === 'number') return `0`;
if (prop.type === 'boolean') return `false`;
return 'undefined';
}
function formatLiteral(value: unknown): string {
if (typeof value === 'string') return `'${value}'`;
if (typeof value === 'number') return `${value}`;
if (typeof value === 'boolean') return `${value}`;
return `'${String(value)}'`;
}

View File

@@ -1,7 +1,7 @@
import { execSync } from 'child_process';
import { logStep, logSuccess, logError } from './logger';
/** Verifica si `openapi-generator-cli` está disponible en el PATH. */
/** Checks whether `openapi-generator-cli` is available on the PATH. */
export function checkOpenApiGenerator(): boolean {
try {
execSync('openapi-generator-cli version', { stdio: 'ignore' });
@@ -11,14 +11,14 @@ export function checkOpenApiGenerator(): boolean {
}
}
/** Instala `@openapitools/openapi-generator-cli` de forma global vía npm. */
/** Installs `@openapitools/openapi-generator-cli` globally via npm. */
export function installOpenApiGenerator(): void {
logStep('Instalando @openapitools/openapi-generator-cli...');
logStep('Installing @openapitools/openapi-generator-cli...');
try {
execSync('npm install -g @openapitools/openapi-generator-cli', { stdio: 'inherit' });
logSuccess('OpenAPI Generator CLI instalado correctamente');
logSuccess('OpenAPI Generator CLI installed successfully');
} catch (_error) {
logError('Error al instalar OpenAPI Generator CLI');
logError('Error installing OpenAPI Generator CLI');
process.exit(1);
}
}

183
src/utils/prompt.ts Normal file
View File

@@ -0,0 +1,183 @@
import prompts from 'prompts';
import { ApiKeyInfo } from './environment-finder';
import { colors } from './logger';
import type { TagSummary, SelectionFilter } from '../types';
function clearScreen(): void {
process.stdout.write('\x1Bc');
}
function printHeader(current?: number, total?: number): void {
const stepText =
current !== undefined && total !== undefined
? ` [${colors.cyan}${current}${colors.reset} de ${colors.cyan}${total}${colors.reset}]`
: '';
console.log(`\n ${colors.bright}🔑 Configuración de URLs base${colors.reset}${stepText}`);
console.log(` ${'─'.repeat(54)}\n`);
}
function printSummary(tags: string[], result: Record<string, string>): void {
clearScreen();
console.log(`\n ${colors.bright}✅ Configuración completada${colors.reset}`);
console.log(` ${'─'.repeat(54)}\n`);
tags.forEach((tag) => {
console.log(` ${colors.bright}${tag}${colors.reset}`);
console.log(` ${colors.cyan}environment.${result[tag]}.url${colors.reset}\n`);
});
console.log(` ${'─'.repeat(54)}\n`);
}
/**
* Interactively asks the user which tags and endpoints to generate.
* Returns a SelectionFilter map of tag → selected operation nicknames.
*/
export async function askSelectionFilter(tagSummaries: TagSummary[]): Promise<SelectionFilter> {
if (tagSummaries.length === 0) return {};
clearScreen();
console.log(`\n ${colors.bright}📋 Selección de tags y endpoints${colors.reset}`);
console.log(` ${'─'.repeat(54)}\n`);
// Step 1: select tags
const tagResponse = await prompts({
type: 'multiselect',
name: 'tags',
message: 'Tags a generar',
choices: tagSummaries.map((t) => ({
title: `${colors.bright}${t.tag}${colors.reset} ${colors.cyan}(${t.operations.length} endpoint${t.operations.length !== 1 ? 's' : ''})${colors.reset}`,
value: t.tag,
selected: true
})),
min: 1,
hint: 'Espacio para marcar/desmarcar, Enter para confirmar'
});
if (!tagResponse.tags?.length) process.exit(0);
const selectedTags: string[] = tagResponse.tags;
const filter: SelectionFilter = {};
// Step 2: for each selected tag, select endpoints
for (let i = 0; i < selectedTags.length; i++) {
const tag = selectedTags[i];
const summary = tagSummaries.find((t) => t.tag === tag)!;
clearScreen();
console.log(
`\n ${colors.bright}📋 Endpoints a generar${colors.reset} [${colors.cyan}${i + 1}${colors.reset} de ${colors.cyan}${selectedTags.length}${colors.reset}]`
);
console.log(` ${'─'.repeat(54)}\n`);
const opResponse = await prompts({
type: 'multiselect',
name: 'ops',
message: `Tag ${colors.bright}${tag}${colors.reset}`,
choices: summary.operations.map((op) => ({
title:
`${colors.bright}${op.method.padEnd(6)}${colors.reset} ${op.path}` +
(op.summary ? ` ${colors.cyan}${op.summary}${colors.reset}` : ''),
value: op.nickname,
selected: true
})),
min: 1,
hint: 'Espacio para marcar/desmarcar, Enter para confirmar'
});
if (!opResponse.ops?.length) process.exit(0);
filter[tag] = opResponse.ops;
}
return filter;
}
/**
* Interactively asks the user which environment API key to use for each tag,
* using arrow-key selection. The last option always allows typing manually.
* Returns a map of tag → environment key (e.g. { "SupplyingMaintenances": "suppliyingMaintenancesApi" }).
*/
export async function askApiKeysForTags(
tags: string[],
apiKeys: ApiKeyInfo[]
): Promise<Record<string, string>> {
if (tags.length === 0) return {};
clearScreen();
printHeader();
const modeResponse = await prompts({
type: 'select',
name: 'mode',
message: 'URL base para los repositorios',
choices: [
{ title: `${colors.bright}La misma para todos${colors.reset}`, value: 'all' },
{ title: `${colors.bright}Configurar individualmente${colors.reset}`, value: 'individual' }
],
hint: ' '
});
if (modeResponse.mode === undefined) process.exit(0);
const result: Record<string, string> = {};
if (modeResponse.mode === 'all') {
clearScreen();
printHeader();
const sharedKey = await askApiKeyForTag('todos los repositorios', apiKeys);
tags.forEach((tag) => (result[tag] = sharedKey));
} else {
for (let i = 0; i < tags.length; i++) {
clearScreen();
printHeader(i + 1, tags.length);
result[tags[i]] = await askApiKeyForTag(tags[i], apiKeys);
}
}
printSummary(tags, result);
return result;
}
async function askApiKeyForTag(tagName: string, apiKeys: ApiKeyInfo[]): Promise<string> {
const MANUAL_VALUE = '__manual__';
const choices = [
...apiKeys.map((k) => ({
title: k.url
? `${colors.bright}${k.key}${colors.reset}\n ${colors.cyan}${k.url}${colors.reset}`
: `${colors.bright}${k.key}${colors.reset}`,
value: k.key
})),
{
title: `${colors.bright}Escribir manualmente${colors.reset}`,
value: MANUAL_VALUE
}
];
const selectResponse = await prompts({
type: 'select',
name: 'key',
message: `Repositorio ${colors.bright}${tagName}${colors.reset}`,
choices,
hint: ' '
});
if (selectResponse.key === undefined) process.exit(0);
if (selectResponse.key !== MANUAL_VALUE) {
return selectResponse.key as string;
}
console.log();
const textResponse = await prompts({
type: 'text',
name: 'key',
message: `Clave de environment`,
hint: 'ej: aprovalmApi',
validate: (v: string) => v.trim().length > 0 || 'La clave no puede estar vacía'
});
if (textResponse.key === undefined) process.exit(0);
return (textResponse.key as string).trim();
}

View File

@@ -1,4 +1,4 @@
/** Traduce un tipo primitivo de OpenAPI/Swagger al equivalente TypeScript. */
/** Translates a primitive OpenAPI/Swagger type to its TypeScript equivalent. */
export function mapSwaggerTypeToTs(type?: string): string {
if (!type) return 'unknown';

View File

@@ -0,0 +1,21 @@
{{#apiInfo}}
{{#apis}}
{{#operations}}
import { MockService } from 'ng-mocks';
import { of } from 'rxjs';
import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl';
{{#returnImports}}
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
{{/returnImports}}
export const mock{{classname}}RepositoryImpl = () =>
MockService({{classname}}RepositoryImpl, {
{{#operation}}
{{nickname}}: () => of({{#isListContainer}}[mock{{returnBaseType}}Model()]{{/isListContainer}}{{^isListContainer}}{{#returnBaseType}}mock{{returnBaseType}}Model(){{/returnBaseType}}{{^returnBaseType}}undefined{{/returnBaseType}}{{/isListContainer}}),
{{/operation}}
});
{{/operations}}
{{/apis}}
{{/apiInfo}}

View File

@@ -26,32 +26,26 @@ import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
@Injectable()
export class {{classname}}RepositoryImpl extends MRepository implements {{classname}}Repository {
constructor() {
super(`${environment.modapApi.url}`);
super(`${environment.{{environmentApiKey}}.url}`);
}
{{#operation}}
{{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}{{^-last}}, {{/-last}}{{/allParams}}): Observable<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}> {
{{#isListContainer}}
return this.{{httpMethod}}<{{{returnBaseType}}}Dto>('{{path}}'{{#hasQueryParams}}, {
params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} }
}{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}})
return this.{{httpMethod}}<{{{returnBaseType}}}Dto>('{{path}}'{{#hasOptions}}, { {{#hasQueryParams}}params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} }{{/hasQueryParams}}{{#hasBothParamsAndBody}}, {{/hasBothParamsAndBody}}{{#hasBodyParam}}body{{/hasBodyParam}} }{{/hasOptions}})
.pipe(
map((response) => response.{{#vendorExtensions}}{{x-response-property}}{{/vendorExtensions}}{{^vendorExtensions}}items{{/vendorExtensions}}.map({{{returnBaseTypeVarName}}}Mapper))
);
{{/isListContainer}}
{{^isListContainer}}
{{#returnType}}
return this.{{httpMethod}}<{{{returnType}}}Dto>('{{path}}'{{#hasQueryParams}}, {
params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} }
}{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}})
return this.{{httpMethod}}<{{{returnType}}}Dto>('{{path}}'{{#hasOptions}}, { {{#hasQueryParams}}params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} }{{/hasQueryParams}}{{#hasBothParamsAndBody}}, {{/hasBothParamsAndBody}}{{#hasBodyParam}}body{{/hasBodyParam}} }{{/hasOptions}})
.pipe(
map({{{returnTypeVarName}}}Mapper)
);
{{/returnType}}
{{^returnType}}
return this.{{httpMethod}}<void>('{{path}}'{{#hasQueryParams}}, {
params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} }
}{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}});
return this.{{httpMethod}}<void>('{{path}}'{{#hasOptions}}, { {{#hasQueryParams}}params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} }{{/hasQueryParams}}{{#hasBothParamsAndBody}}, {{/hasBothParamsAndBody}}{{#hasBodyParam}}body{{/hasBodyParam}} }{{/hasOptions}});
{{/returnType}}
{{/isListContainer}}
}

View File

@@ -0,0 +1,97 @@
{{#apiInfo}}
{{#apis}}
{{#operations}}
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { {{classname}}RepositoryImpl } from './{{classFilename}}.repository.impl';
{{#returnImports}}
import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
{{/returnImports}}
describe('{{classname}}RepositoryImpl', () => {
let repository: {{classname}}RepositoryImpl;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [{{classname}}RepositoryImpl]
});
repository = TestBed.inject({{classname}}RepositoryImpl);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should be created', () => {
expect(repository).toBeTruthy();
});
{{#operation}}
describe('{{nickname}}', () => {
it('should perform a {{uppercaseHttpMethod}} request', () => {
repository.{{nickname}}({{#allParams}}{{{testValue}}}{{^-last}}, {{/-last}}{{/allParams}}).subscribe();
const req = httpMock.expectOne((r) => r.method === '{{uppercaseHttpMethod}}');
expect(req.request.method).toBe('{{uppercaseHttpMethod}}');
{{#isListContainer}}
req.flush({ items: [mock{{returnBaseType}}Dto()] });
{{/isListContainer}}
{{^isListContainer}}
{{#returnType}}
req.flush(mock{{returnBaseType}}Dto());
{{/returnType}}
{{^returnType}}
req.flush(null);
{{/returnType}}
{{/isListContainer}}
});
{{#returnType}}
it('should map the response to the domain model', () => {
const dto = mock{{returnBaseType}}Dto();
const expectedModel = mock{{returnBaseType}}Model();
{{#isListContainer}}
repository.{{nickname}}({{#allParams}}{{{testValue}}}{{^-last}}, {{/-last}}{{/allParams}}).subscribe((result) => {
expect(result).toBeTruthy();
expect(result.length).toBeGreaterThan(0);
});
httpMock.expectOne((r) => r.method === '{{uppercaseHttpMethod}}').flush({ items: [dto] });
{{/isListContainer}}
{{^isListContainer}}
repository.{{nickname}}({{#allParams}}{{{testValue}}}{{^-last}}, {{/-last}}{{/allParams}}).subscribe((result) => {
expect(result).toEqual(expectedModel);
});
httpMock.expectOne((r) => r.method === '{{uppercaseHttpMethod}}').flush(dto);
{{/isListContainer}}
});
{{/returnType}}
it('should propagate HTTP errors', (done) => {
repository.{{nickname}}({{#allParams}}{{{testValue}}}{{^-last}}, {{/-last}}{{/allParams}}).subscribe({
error: (err) => {
expect(err.status).toBe(500);
done();
}
});
httpMock
.expectOne((r) => r.method === '{{uppercaseHttpMethod}}')
.flush('Internal Server Error', { status: 500, statusText: 'Internal Server Error' });
});
});
{{/operation}}
});
{{/operations}}
{{/apis}}
{{/apiInfo}}

View File

@@ -0,0 +1,94 @@
{{#apiInfo}}
{{#apis}}
{{#operations}}
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { {{classname}}UseCasesImpl } from './{{classFilename}}.use-cases.impl';
import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
{{#returnImports}}
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
{{/returnImports}}
describe('{{classname}}UseCasesImpl', () => {
let useCase: {{classname}}UseCasesImpl;
let mockRepository: jasmine.SpyObj<{{classname}}Repository>;
beforeEach(() => {
mockRepository = jasmine.createSpyObj('{{classname}}Repository', [{{#operation}}'{{nickname}}', {{/operation}}]);
TestBed.configureTestingModule({
providers: [
{{classname}}UseCasesImpl,
{ provide: {{constantName}}_REPOSITORY, useValue: mockRepository }
]
});
useCase = TestBed.inject({{classname}}UseCasesImpl);
});
it('should be created', () => {
expect(useCase).toBeTruthy();
});
{{#operation}}
describe('{{nickname}}', () => {
it('should delegate to the repository', () => {
{{#isListContainer}}
mockRepository.{{nickname}}.and.returnValue(of([mock{{returnBaseType}}Model()]));
{{/isListContainer}}
{{^isListContainer}}
{{#returnBaseType}}
mockRepository.{{nickname}}.and.returnValue(of(mock{{returnBaseType}}Model()));
{{/returnBaseType}}
{{^returnBaseType}}
mockRepository.{{nickname}}.and.returnValue(of(undefined));
{{/returnBaseType}}
{{/isListContainer}}
useCase.{{nickname}}({{#allParams}}{{{testValue}}}{{^-last}}, {{/-last}}{{/allParams}});
expect(mockRepository.{{nickname}}).toHaveBeenCalled();
});
it('should return the observable from the repository', (done) => {
{{#isListContainer}}
const expected = [mock{{returnBaseType}}Model()];
mockRepository.{{nickname}}.and.returnValue(of(expected));
useCase.{{nickname}}({{#allParams}}{{{testValue}}}{{^-last}}, {{/-last}}{{/allParams}}).subscribe((result) => {
expect(result).toEqual(expected);
done();
});
{{/isListContainer}}
{{^isListContainer}}
{{#returnBaseType}}
const expected = mock{{returnBaseType}}Model();
mockRepository.{{nickname}}.and.returnValue(of(expected));
useCase.{{nickname}}({{#allParams}}{{{testValue}}}{{^-last}}, {{/-last}}{{/allParams}}).subscribe((result) => {
expect(result).toEqual(expected);
done();
});
{{/returnBaseType}}
{{^returnBaseType}}
mockRepository.{{nickname}}.and.returnValue(of(undefined));
useCase.{{nickname}}({{#allParams}}{{{testValue}}}{{^-last}}, {{/-last}}{{/allParams}}).subscribe({
complete: () => {
expect(mockRepository.{{nickname}}).toHaveBeenCalledOnceWith({{#allParams}}{{{testValue}}}{{^-last}}, {{/-last}}{{/allParams}});
done();
}
});
{{/returnBaseType}}
{{/isListContainer}}
});
});
{{/operation}}
});
{{/operations}}
{{/apis}}
{{/apiInfo}}

View File

@@ -0,0 +1,21 @@
{{#apiInfo}}
{{#apis}}
{{#operations}}
import { MockService } from 'ng-mocks';
import { of } from 'rxjs';
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl';
{{#returnImports}}
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
{{/returnImports}}
export const mock{{classname}}UseCasesImpl = () =>
MockService({{classname}}UseCasesImpl, {
{{#operation}}
{{nickname}}: () => of({{#isListContainer}}[mock{{returnBaseType}}Model()]{{/isListContainer}}{{^isListContainer}}{{#returnBaseType}}mock{{returnBaseType}}Model(){{/returnBaseType}}{{^returnBaseType}}undefined{{/returnBaseType}}{{/isListContainer}}),
{{/operation}}
});
{{/operations}}
{{/apis}}
{{/apiInfo}}

View File

@@ -0,0 +1,16 @@
{{#models}}
{{#model}}
{{#mockImports}}
import { mock{{classname}}Dto } from './{{classFilename}}.dto.mock';
{{/mockImports}}
import { {{classname}}Dto } from './{{classFilename}}.dto';
export const mock{{classname}}Dto = (overrides: Partial<{{classname}}Dto> = {}): {{classname}}Dto => ({
{{#vars}}
{{name}}: {{{mockValue}}},
{{/vars}}
...overrides
});
{{/model}}
{{/models}}

View File

@@ -0,0 +1,39 @@
{{#models}}
{{#model}}
import { {{classVarName}}Mapper } from './{{classFilename}}.mapper';
import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
describe('{{classVarName}}Mapper', () => {
{{#vars}}
it('should map {{name}} from DTO to model', () => {
const dto = mock{{classname}}Dto();
const result = {{classVarName}}Mapper(dto);
expect(result.{{name}}).toBe(dto.{{name}});
});
{{/vars}}
it('should return an instance of {{classname}}', () => {
const dto = mock{{classname}}Dto();
const result = {{classVarName}}Mapper(dto);
expect(result).toBeInstanceOf({{classname}});
});
it('should map all fields correctly from a complete DTO', () => {
const dto = mock{{classname}}Dto();
const result = {{classVarName}}Mapper(dto);
{{#vars}}
expect(result.{{name}}).toBe(dto.{{name}});
{{/vars}}
});
});
{{/model}}
{{/models}}

View File

@@ -16,7 +16,7 @@ export class {{classname}} {
* {{description}}
*/
{{/description}}
{{name}}{{^required}}?{{/required}}: {{{dataType}}};
{{name}}{{^required}}?{{/required}}{{#required}}!{{/required}}: {{{dataType}}};
{{/vars}}
}

View File

@@ -0,0 +1,34 @@
{{#models}}
{{#model}}
import { {{classname}} } from './{{classFilename}}.model';
import { mock{{classname}}Model } from './{{classFilename}}.model.mock';
describe('{{classname}}', () => {
it('should create an instance', () => {
const model = new {{classname}}();
expect(model).toBeInstanceOf({{classname}});
});
{{#vars}}
it('should allow setting {{name}}', () => {
const model = new {{classname}}();
const expected = mock{{classname}}Model();
model.{{name}} = expected.{{name}};
expect(model.{{name}}).toBe(expected.{{name}});
});
{{/vars}}
it('should build a valid model from mock', () => {
const model = mock{{classname}}Model();
expect(model).toBeInstanceOf({{classname}});
{{#vars}}
expect(model.{{name}}).toBeDefined();
{{/vars}}
});
});
{{/model}}
{{/models}}

View File

@@ -0,0 +1,14 @@
{{#models}}
{{#model}}
import { {{classname}} } from './{{classFilename}}.model';
import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}.mapper';
import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
export const mock{{classname}}Model = (overrides: Partial<{{classname}}> = {}): {{classname}} =>
Object.assign(new {{classname}}(), {
...{{classVarName}}Mapper(mock{{classname}}Dto()),
...overrides
});
{{/model}}
{{/models}}

View File

@@ -0,0 +1,20 @@
{{#apiInfo}}
{{#apis}}
{{#operations}}
import { Provider } from '@angular/core';
import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}.repository.contract';
import { mock{{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl.mock';
export function mock{{classname}}Repository(): Provider[] {
return [
{
provide: {{constantName}}_REPOSITORY,
useFactory: () => mock{{classname}}RepositoryImpl()
}
];
}
{{/operations}}
{{/apis}}
{{/apiInfo}}

View File

@@ -0,0 +1,20 @@
{{#apiInfo}}
{{#apis}}
{{#operations}}
import { Provider } from '@angular/core';
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}.use-cases.contract';
import { mock{{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.mock';
export function mock{{classname}}UseCases(): Provider[] {
return [
{
provide: {{constantName}}_USE_CASES,
useFactory: () => mock{{classname}}UseCasesImpl()
}
];
}
{{/operations}}
{{/apis}}
{{/apiInfo}}