Compare commits
113 Commits
v0.0.1-alp
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 902c0fd5a2 | |||
| ef804b74c2 | |||
| 98cd7a5edb | |||
| 3ede53ae3b | |||
| aa7c6cf338 | |||
| 0075717a1f | |||
| 7c5af2f3ab | |||
| e667c7bf04 | |||
| 2e9aecdffe | |||
| 5304ed7047 | |||
| 56bd6f4e9f | |||
| cbef98a077 | |||
| 3eb604c95f | |||
| 4293857a7d | |||
| 07e4559133 | |||
| bdd0e4873e | |||
| ddca01e4e9 | |||
| 59ff941fda | |||
| 8881e9494c | |||
| 720748b73d | |||
| 7063796e28 | |||
| f349b7b2a3 | |||
| b59084dec6 | |||
| 5c83520f01 | |||
| cc0439e26e | |||
| b5b3632f5b | |||
| d78bc303fa | |||
| df9283556b | |||
| 909f709659 | |||
| 5878331abf | |||
| 7e8e6d7058 | |||
| 469697f636 | |||
| 2257e2141e | |||
| 0f64b51b63 | |||
| 9c385191e2 | |||
| d2f9eaa933 | |||
| a600a60678 | |||
| a42063c1d9 | |||
| 7f6feda81d | |||
| 12b2dd6b51 | |||
| 84486e816a | |||
| 942cf7f092 | |||
| e0fb12a6c6 | |||
| 2402b40059 | |||
| 04962e32f5 | |||
| 144629bed6 | |||
| 0c58a63d01 | |||
| 74ac1c26a1 | |||
| db70f47bb7 | |||
| 91e608415f | |||
| 058abf59c4 | |||
| 1d52da3805 | |||
| 32cb3d476f | |||
| 79ea7dfc7e | |||
| b54a94c6d3 | |||
| 77e3cbc0e9 | |||
| 16d4c8e0bb | |||
| e28443ce45 | |||
| 5707abf6bb | |||
|
|
d47afb6ff1 | ||
|
|
463626da0c | ||
| 05a58c4254 | |||
| d4d6148b25 | |||
|
|
cc650f9635 | ||
|
|
99658800ed | ||
| 0c162e30b8 | |||
|
|
9c14a070c6 | ||
| 94263d0329 | |||
| 06c4356f16 | |||
|
|
917cc3b9cf | ||
| c6fbc4c47c | |||
| 1b342b871b | |||
| a01b5a2d82 | |||
| 16fe772bd2 | |||
| 2c1163809a | |||
| e4e88d184e | |||
| f51eaf7c78 | |||
| e1ef10f317 | |||
| 77b77a17f4 | |||
| e69826b824 | |||
| 21b0333788 | |||
| 04d2299a4c | |||
| a0a8ba28f5 | |||
|
|
3bec87ba6b | ||
|
|
c86c6bece6 | ||
| e776d49a77 | |||
| f09e19265b | |||
| 7d51b6b3db | |||
| 0eb918ed71 | |||
|
|
5f34aa2f89 | ||
| dd6bb3e755 | |||
| 700597a9e8 | |||
| ad9a957be4 | |||
| f85b981ceb | |||
| 1cd25d2b90 | |||
| bada7ba0e9 | |||
| b8d2fd8582 | |||
| a90f7ba078 | |||
| 4aeb108c55 | |||
| a9bbf21317 | |||
| 324e4f458c | |||
|
|
5229a3ad45 | ||
|
|
ca63b85086 | ||
|
|
e008144813 | ||
|
|
73dcb6f701 | ||
| ac9d4ceb99 | |||
|
|
e8c919ee76 | ||
|
|
a97c3b22fa | ||
|
|
031bdb4b3a | ||
|
|
82b734bc55 | ||
|
|
f4d9340137 | ||
|
|
fc3d95052c | ||
|
|
3fe2333a03 |
41
.gitea/workflows/lint.yaml
Normal file
41
.gitea/workflows/lint.yaml
Normal 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
|
||||
|
||||
@@ -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
2
.gitignore
vendored
@@ -1,8 +1,10 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
.temp-generated/
|
||||
temp-generated/
|
||||
dist/
|
||||
dist/bin/
|
||||
|
||||
generation-report.json
|
||||
|
||||
|
||||
50
NOTICE
Normal file
50
NOTICE
Normal file
@@ -0,0 +1,50 @@
|
||||
OpenAPI Clean Architecture Generator
|
||||
Copyright (c) 2026 Blas Santomé Ocampo
|
||||
|
||||
This product includes software developed by third parties.
|
||||
All third-party packages listed below are distributed under the MIT License.
|
||||
|
||||
---
|
||||
|
||||
chalk v4.1.2
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
License: MIT
|
||||
https://github.com/chalk/chalk
|
||||
|
||||
---
|
||||
|
||||
commander v11.1.0
|
||||
Copyright (c) TJ Holowaychuk <tj@vision-media.ca>
|
||||
License: MIT
|
||||
https://github.com/tj/commander.js
|
||||
|
||||
---
|
||||
|
||||
fs-extra v11.3.4
|
||||
Copyright (c) JP Richardson <jprichardson@gmail.com>
|
||||
License: MIT
|
||||
https://github.com/jprichardson/node-fs-extra
|
||||
|
||||
---
|
||||
|
||||
js-yaml v4.1.1
|
||||
Copyright (c) Vladimir Zapparov <dervus.grim@gmail.com>
|
||||
Copyright (c) Aleksey V Zapparov <ixti@member.fsf.org>
|
||||
Copyright (c) Vitaly Puzrin <vitaly@rcdesign.ru>
|
||||
Copyright (c) Martin Grenfell <martin.grenfell@gmail.com>
|
||||
License: MIT
|
||||
https://github.com/nodeca/js-yaml
|
||||
|
||||
---
|
||||
|
||||
mustache v4.2.0
|
||||
Copyright (c) mustache.js Authors (http://github.com/janl/mustache.js)
|
||||
License: MIT
|
||||
https://github.com/janl/mustache.js
|
||||
|
||||
---
|
||||
|
||||
prompts v2.4.2
|
||||
Copyright (c) Terkel Gjervig
|
||||
License: MIT
|
||||
https://github.com/terkelg/prompts
|
||||
378
README.md
378
README.md
@@ -1,239 +1,289 @@
|
||||
# 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
|
||||
-c, --config <file> Use a JSON configuration file (skips interactive prompts)
|
||||
--init-config [file] Generate a JSON configuration file instead of generating code
|
||||
--skip-install Skip dependency installation
|
||||
--skip-lint Skip post-generation linting and formatting
|
||||
--dry-run Simulate without writing files
|
||||
-h, --help Show help
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
# Skip linting after generation
|
||||
generate-clean-arch -i swagger.yaml --skip-lint
|
||||
|
||||
# Generate a config file to reuse later
|
||||
generate-clean-arch --init-config generation-config.json
|
||||
|
||||
# Run using a config file (no interactive prompts)
|
||||
generate-clean-arch -c generation-config.json
|
||||
|
||||
# Full example with all options
|
||||
generate-clean-arch -i ./docs/api.yaml -o ./frontend/src/app -t ./custom-templates
|
||||
```
|
||||
|
||||
## 📁 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 +292,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
277
bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
@@ -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
24
generation-config.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"input": "example-swagger.yaml",
|
||||
"output": "./src/app",
|
||||
"skipLint": false,
|
||||
"skipInstall": false,
|
||||
"tags": {
|
||||
"User": {
|
||||
"baseUrl": "apiUrl",
|
||||
"endpoints": [
|
||||
"getUsers",
|
||||
"createUser",
|
||||
"getUserById",
|
||||
"deleteUser"
|
||||
]
|
||||
},
|
||||
"Product": {
|
||||
"baseUrl": "apiUrl",
|
||||
"endpoints": [
|
||||
"getProducts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"templates": "/Users/bsantome/Downloads/openapi-clean-arch-generator/templates"
|
||||
}
|
||||
185
main.ts
185
main.ts
@@ -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(`\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(`\n📁 Archivos generados en: ${colors.cyan}${options.output}${colors.reset}\n`);
|
||||
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
1776
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -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.14",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,36 +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$/, '');
|
||||
|
||||
@@ -43,7 +95,7 @@ export function generateCleanArchitecture(
|
||||
if (rawProperties[k].$ref) {
|
||||
tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
|
||||
} else if (rawProperties[k].type === 'array' && rawProperties[k].items?.$ref) {
|
||||
tsType = `Array<${rawProperties[k].items.$ref.split('/').pop()}>`;
|
||||
tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
|
||||
}
|
||||
return {
|
||||
name: k,
|
||||
@@ -53,14 +105,28 @@ export function generateCleanArchitecture(
|
||||
};
|
||||
});
|
||||
|
||||
// Collect imports for types referenced via $ref in properties
|
||||
const referencedTypes = new Set<string>();
|
||||
Object.values(rawProperties).forEach((prop) => {
|
||||
if (prop.$ref) {
|
||||
referencedTypes.add(prop.$ref.split('/').pop() || '');
|
||||
} else if (prop.type === 'array' && prop.items?.$ref) {
|
||||
referencedTypes.add(prop.items.$ref.split('/').pop() || '');
|
||||
}
|
||||
});
|
||||
const modelImports = [...referencedTypes]
|
||||
.filter(Boolean)
|
||||
.map((name) => ({ classname: name, classFilename: toCamelCase(name) }));
|
||||
|
||||
const modelViewData = {
|
||||
models: [
|
||||
{
|
||||
model: {
|
||||
classname: baseName,
|
||||
classFilename: baseName.toLowerCase(),
|
||||
classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1),
|
||||
classFilename: toCamelCase(baseName),
|
||||
classVarName: toCamelCase(baseName),
|
||||
description: schemaObj.description || '',
|
||||
imports: modelImports,
|
||||
vars: varsMap
|
||||
}
|
||||
}
|
||||
@@ -75,8 +141,8 @@ export function generateCleanArchitecture(
|
||||
{
|
||||
operations: {
|
||||
classname: baseName,
|
||||
classFilename: baseName.toLowerCase(),
|
||||
classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1)
|
||||
classFilename: toCamelCase(baseName),
|
||||
classVarName: toCamelCase(baseName)
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -88,14 +154,10 @@ export function generateCleanArchitecture(
|
||||
if (fs.existsSync(modelTemplatePath)) {
|
||||
const template = fs.readFileSync(modelTemplatePath, 'utf8');
|
||||
const output = mustache.render(template, modelViewData);
|
||||
const destPath = path.join(
|
||||
outputDir,
|
||||
'entities/models',
|
||||
`${baseName.toLowerCase()}.model.ts`
|
||||
);
|
||||
const destPath = path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.ts`);
|
||||
fs.writeFileSync(destPath, output);
|
||||
generatedCount.models++;
|
||||
logInfo(` ${baseName.toLowerCase()}.model.ts → ${path.relative(process.cwd(), destPath)}`);
|
||||
logDetail('generate', `model-entity → ${path.relative(process.cwd(), destPath)}`);
|
||||
}
|
||||
|
||||
// Mapper
|
||||
@@ -103,13 +165,75 @@ export function generateCleanArchitecture(
|
||||
if (fs.existsSync(mapperTemplatePath)) {
|
||||
const template = fs.readFileSync(mapperTemplatePath, 'utf8');
|
||||
const output = mustache.render(template, mapperViewData);
|
||||
const destPath = path.join(outputDir, 'data/mappers', `${baseName.toLowerCase()}.mapper.ts`);
|
||||
const destPath = path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.ts`);
|
||||
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) => {
|
||||
@@ -124,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) {
|
||||
@@ -138,46 +263,59 @@ 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';
|
||||
returnBaseType = returnType;
|
||||
} else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
|
||||
returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown';
|
||||
returnType = `Array<${returnBaseType}>`;
|
||||
returnType = `${returnBaseType}[]`;
|
||||
isListContainer = true;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
returnBaseTypeVarName: returnBaseType !== 'void' ? toCamelCase(returnBaseType) : false,
|
||||
isListContainer: isListContainer,
|
||||
vendorExtensions: {}
|
||||
});
|
||||
@@ -185,17 +323,38 @@ export function generateCleanArchitecture(
|
||||
});
|
||||
});
|
||||
|
||||
// Generar por cada Tag
|
||||
if (Object.keys(selectionFilter).length > 0) {
|
||||
Object.keys(tagsMap).forEach((tag) => {
|
||||
const imports: { classname: string; classFilename: string; classVarName: string }[] = [];
|
||||
Object.keys(schemas).forEach((s) => {
|
||||
if (tagsMap[tag].some((op) => op.returnType === s || op.returnType === `Array<${s}>`)) {
|
||||
imports.push({
|
||||
classname: s,
|
||||
classFilename: s.toLowerCase(),
|
||||
classVarName: s.charAt(0).toLowerCase() + s.slice(1)
|
||||
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 }[] = [];
|
||||
|
||||
Object.keys(schemas).forEach((s) => {
|
||||
const usedAsReturn = tagsMap[tag].some(
|
||||
(op) => op.returnType === s || op.returnType === `${s}[]`
|
||||
);
|
||||
const usedAsParam = tagsMap[tag].some((op) =>
|
||||
op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`)
|
||||
);
|
||||
|
||||
const entry = { classname: s, classFilename: toCamelCase(s), classVarName: toCamelCase(s) };
|
||||
|
||||
if (usedAsReturn) {
|
||||
returnImports.push(entry);
|
||||
} else if (usedAsParam) {
|
||||
// Param-only types: entity import needed for method signatures, but no Dto or Mapper
|
||||
paramImports.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
const apiViewData = {
|
||||
@@ -204,10 +363,18 @@ export function generateCleanArchitecture(
|
||||
{
|
||||
operations: {
|
||||
classname: tag,
|
||||
classFilename: tag.toLowerCase(),
|
||||
classFilename: toCamelCase(tag),
|
||||
classVarName: toCamelCase(tag),
|
||||
constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
|
||||
operation: tagsMap[tag],
|
||||
imports: imports
|
||||
// All entity imports (return + param) — for contracts and use-cases
|
||||
imports: [...returnImports, ...paramImports],
|
||||
// Return-type-only imports — for repo impl (Dto + Entity + Mapper)
|
||||
returnImports,
|
||||
// Param-only imports — for repo impl (Entity only, no Dto/Mapper)
|
||||
paramImports,
|
||||
// Environment API key for the repository base URL (e.g. "aprovalmApi")
|
||||
environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -218,7 +385,7 @@ export function generateCleanArchitecture(
|
||||
templatesDir,
|
||||
'api.use-cases.contract.mustache',
|
||||
apiViewData,
|
||||
path.join(outputDir, 'domain/use-cases', `${tag.toLowerCase()}.use-cases.contract.ts`),
|
||||
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.contract.ts`),
|
||||
generatedCount,
|
||||
'useCases'
|
||||
);
|
||||
@@ -227,7 +394,7 @@ export function generateCleanArchitecture(
|
||||
templatesDir,
|
||||
'api.use-cases.impl.mustache',
|
||||
apiViewData,
|
||||
path.join(outputDir, 'domain/use-cases', `${tag.toLowerCase()}.use-cases.impl.ts`),
|
||||
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.ts`),
|
||||
generatedCount,
|
||||
'useCases'
|
||||
);
|
||||
@@ -236,7 +403,7 @@ export function generateCleanArchitecture(
|
||||
templatesDir,
|
||||
'api.repository.contract.mustache',
|
||||
apiViewData,
|
||||
path.join(outputDir, 'domain/repositories', `${tag.toLowerCase()}.repository.contract.ts`),
|
||||
path.join(outputDir, 'domain/repositories', `${toCamelCase(tag)}.repository.contract.ts`),
|
||||
generatedCount,
|
||||
'repositories'
|
||||
);
|
||||
@@ -245,7 +412,7 @@ export function generateCleanArchitecture(
|
||||
templatesDir,
|
||||
'api.repository.impl.mustache',
|
||||
apiViewData,
|
||||
path.join(outputDir, 'data/repositories', `${tag.toLowerCase()}.repository.impl.ts`),
|
||||
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.ts`),
|
||||
generatedCount,
|
||||
'repositories'
|
||||
);
|
||||
@@ -254,7 +421,7 @@ export function generateCleanArchitecture(
|
||||
templatesDir,
|
||||
'use-cases.provider.mustache',
|
||||
apiViewData,
|
||||
path.join(outputDir, 'di/use-cases', `${tag.toLowerCase()}.use-cases.provider.ts`),
|
||||
path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.ts`),
|
||||
generatedCount,
|
||||
'providers'
|
||||
);
|
||||
@@ -263,19 +430,76 @@ export function generateCleanArchitecture(
|
||||
templatesDir,
|
||||
'repository.provider.mustache',
|
||||
apiViewData,
|
||||
path.join(outputDir, 'di/repositories', `${tag.toLowerCase()}.repository.provider.ts`),
|
||||
path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.ts`),
|
||||
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,
|
||||
@@ -290,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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 los imports de tipos referenciados. */
|
||||
/** 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');
|
||||
|
||||
@@ -85,11 +84,15 @@ export function addDtoImports(outputDir: string): void {
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(dtosDir, file);
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
const originalContent = fs.readFileSync(filePath, 'utf8');
|
||||
let content = originalContent;
|
||||
|
||||
const selfMatch = content.match(/export interface (\w+)/);
|
||||
const selfName = selfMatch ? selfMatch[1] : '';
|
||||
|
||||
// Normalize Array<T> → T[] (openapi-generator-cli always outputs Array<T>)
|
||||
content = content.replace(/Array<(\w+)>/g, '$1[]');
|
||||
|
||||
// Find all Dto type references in the file body (excluding the interface name itself)
|
||||
const references = new Set<string>();
|
||||
const typeRegex = /\b(\w+Dto)\b/g;
|
||||
@@ -100,8 +103,6 @@ export function addDtoImports(outputDir: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
if (references.size === 0) return;
|
||||
|
||||
// Build import lines for each referenced type that exists in the dtoMap
|
||||
const imports: string[] = [];
|
||||
references.forEach((ref) => {
|
||||
@@ -112,11 +113,14 @@ export function addDtoImports(outputDir: string): void {
|
||||
|
||||
if (imports.length > 0) {
|
||||
content = imports.join('\n') + '\n' + content;
|
||||
}
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(filePath, content);
|
||||
filesProcessed++;
|
||||
logInfo(` Imports añadidos a ${file}`);
|
||||
logDetail('dto', `Post-processed ${file} (added ${imports.length} import(s))`);
|
||||
}
|
||||
});
|
||||
|
||||
logSuccess(`Imports añadidos a ${filesProcessed} DTOs`);
|
||||
logSuccess(`${filesProcessed} DTOs post-processed`);
|
||||
}
|
||||
|
||||
124
src/generators/lint.generator.ts
Normal file
124
src/generators/lint.generator.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,22 +88,39 @@ 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;
|
||||
dataType: string;
|
||||
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: unknown[];
|
||||
allParams: TagOperationParam[];
|
||||
hasQueryParams: boolean;
|
||||
queryParams: unknown[];
|
||||
hasBodyParam: boolean;
|
||||
bodyParam: string;
|
||||
hasOptions: boolean;
|
||||
hasBothParamsAndBody: boolean;
|
||||
returnType: string | boolean;
|
||||
returnBaseType: string | boolean;
|
||||
returnTypeVarName: string | boolean;
|
||||
returnBaseTypeVarName: string | boolean;
|
||||
isListContainer: boolean;
|
||||
vendorExtensions: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -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
118
src/utils/config.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import fs from 'fs-extra';
|
||||
import { logInfo, logError } from './logger';
|
||||
import type { GenerationConfig, TagConfig } from '../types';
|
||||
import type { SwaggerAnalysis } from '../types';
|
||||
import type { TagSummary } from '../types';
|
||||
import type { ApiKeyInfo } from './environment-finder';
|
||||
|
||||
/**
|
||||
* Loads and validates a GenerationConfig from a JSON file.
|
||||
*/
|
||||
export function loadConfig(filePath: string): GenerationConfig {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
logError(`Configuration file not found: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
let config: GenerationConfig;
|
||||
|
||||
try {
|
||||
config = JSON.parse(raw) as GenerationConfig;
|
||||
} catch {
|
||||
logError(`Invalid JSON in configuration file: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!config.tags || typeof config.tags !== 'object') {
|
||||
logError('Configuration file must contain a "tags" object');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const [tag, tagConfig] of Object.entries(config.tags)) {
|
||||
if (!tagConfig.baseUrl || typeof tagConfig.baseUrl !== 'string') {
|
||||
logError(`Tag "${tag}" must have a "baseUrl" string`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!Array.isArray(tagConfig.endpoints) || tagConfig.endpoints.length === 0) {
|
||||
logError(`Tag "${tag}" must have a non-empty "endpoints" array`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a default GenerationConfig from a swagger analysis, including all tags
|
||||
* and all endpoints. Useful for --init-config to scaffold a config template.
|
||||
*/
|
||||
export function generateDefaultConfig(
|
||||
analysis: SwaggerAnalysis,
|
||||
tagSummaries: TagSummary[],
|
||||
cliOptions: {
|
||||
input: string;
|
||||
output: string;
|
||||
templates?: string;
|
||||
skipLint?: boolean;
|
||||
skipInstall?: boolean;
|
||||
},
|
||||
apiKeys: ApiKeyInfo[]
|
||||
): GenerationConfig {
|
||||
const tags: Record<string, TagConfig> = {};
|
||||
|
||||
for (const summary of tagSummaries) {
|
||||
const matchingKey = apiKeys.find((k) =>
|
||||
k.key.toLowerCase().includes(summary.tag.toLowerCase())
|
||||
);
|
||||
|
||||
tags[summary.tag] = {
|
||||
baseUrl: matchingKey?.key || 'apiUrl',
|
||||
endpoints: summary.operations.map((op) => op.nickname)
|
||||
};
|
||||
}
|
||||
|
||||
const config: GenerationConfig = {
|
||||
input: cliOptions.input,
|
||||
output: cliOptions.output,
|
||||
skipLint: cliOptions.skipLint ?? false,
|
||||
skipInstall: cliOptions.skipInstall ?? false,
|
||||
tags
|
||||
};
|
||||
|
||||
if (cliOptions.templates) {
|
||||
config.templates = cliOptions.templates;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a GenerationConfig to a JSON file.
|
||||
*/
|
||||
export function writeConfig(config: GenerationConfig, filePath: string): void {
|
||||
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||
logInfo(`Configuration file written to: ${filePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the selectionFilter (tag → endpoint nicknames) from a GenerationConfig.
|
||||
*/
|
||||
export function deriveSelectionFilter(config: GenerationConfig): Record<string, string[]> {
|
||||
const filter: Record<string, string[]> = {};
|
||||
for (const [tag, tagConfig] of Object.entries(config.tags)) {
|
||||
filter[tag] = [...tagConfig.endpoints];
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the tagApiKeyMap (tag → baseUrl key) from a GenerationConfig.
|
||||
*/
|
||||
export function deriveTagApiKeyMap(config: GenerationConfig): Record<string, string> {
|
||||
const map: Record<string, string> = {};
|
||||
for (const [tag, tagConfig] of Object.entries(config.tags)) {
|
||||
map[tag] = tagConfig.baseUrl;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
53
src/utils/environment-finder.ts
Normal file
53
src/utils/environment-finder.ts
Normal 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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
70
src/utils/mock-value-resolver.ts
Normal file
70
src/utils/mock-value-resolver.ts
Normal 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)}'`;
|
||||
}
|
||||
12
src/utils/name-formatter.ts
Normal file
12
src/utils/name-formatter.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Converts a PascalCase name to camelCase by lowercasing the first character.
|
||||
* Used to derive class filenames and variable names from schema/tag names.
|
||||
*
|
||||
* @example
|
||||
* toCamelCase('ProductResponse') // 'productResponse'
|
||||
* toCamelCase('UserSchema') // 'userSchema'
|
||||
*/
|
||||
export function toCamelCase(name: string): string {
|
||||
if (!name) return name;
|
||||
return name.charAt(0).toLowerCase() + name.slice(1);
|
||||
}
|
||||
@@ -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
183
src/utils/prompt.ts
Normal 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();
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -7,7 +7,7 @@ export function mapSwaggerTypeToTs(type?: string): string {
|
||||
string: 'string',
|
||||
boolean: 'boolean',
|
||||
number: 'number',
|
||||
array: 'Array<unknown>',
|
||||
array: 'unknown[]',
|
||||
object: 'unknown'
|
||||
};
|
||||
return typeMap[type] || 'unknown';
|
||||
|
||||
21
templates/api.repository.impl.mock.mustache
Normal file
21
templates/api.repository.impl.mock.mustache
Normal 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}}
|
||||
@@ -9,12 +9,15 @@ import { environment } from '@environment';
|
||||
|
||||
import { MRepository } from '@mercadona/core/utils/repository';
|
||||
|
||||
import { {{classname}}Repository } from '../../../domain/repositories/{{classFilename}}.repository.contract';
|
||||
{{#imports}}
|
||||
import { {{classname}}Dto } from '@/dtos/{{classFilename}}/{{classFilename}}.dto';
|
||||
import { {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
|
||||
{{#returnImports}}
|
||||
import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto';
|
||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
||||
import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}/{{classFilename}}.mapper';
|
||||
{{/imports}}
|
||||
import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}.mapper';
|
||||
{{/returnImports}}
|
||||
{{#paramImports}}
|
||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
||||
{{/paramImports}}
|
||||
|
||||
/**
|
||||
* {{classname}} Repository Implementation
|
||||
@@ -23,32 +26,26 @@ import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}/{{classFilen
|
||||
@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({{{returnBaseType}}}Mapper))
|
||||
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({{{returnType}}}Mapper)
|
||||
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}}
|
||||
}
|
||||
|
||||
97
templates/api.repository.impl.spec.mustache
Normal file
97
templates/api.repository.impl.spec.mustache
Normal 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}}
|
||||
94
templates/api.use-cases.impl.spec.mustache
Normal file
94
templates/api.use-cases.impl.spec.mustache
Normal 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}}
|
||||
21
templates/api.use-cases.mock.mustache
Normal file
21
templates/api.use-cases.mock.mustache
Normal 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}}
|
||||
16
templates/dto.mock.mustache
Normal file
16
templates/dto.mock.mustache
Normal 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}}
|
||||
@@ -4,7 +4,7 @@
|
||||
import { MapFromFn } from '@mercadona/common/public';
|
||||
import { Builder } from '@mercadona/common/utils';
|
||||
|
||||
import { {{classname}}Dto } from '@/dtos/{{classFilename}}/{{classFilename}}.dto';
|
||||
import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto';
|
||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
||||
|
||||
/**
|
||||
|
||||
39
templates/mapper.spec.mustache
Normal file
39
templates/mapper.spec.mustache
Normal 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}}
|
||||
@@ -16,7 +16,7 @@ export class {{classname}} {
|
||||
* {{description}}
|
||||
*/
|
||||
{{/description}}
|
||||
{{name}}{{^required}}?{{/required}}: {{{dataType}}};
|
||||
{{name}}{{^required}}?{{/required}}{{#required}}!{{/required}}: {{{dataType}}};
|
||||
{{/vars}}
|
||||
}
|
||||
|
||||
|
||||
34
templates/model-entity.spec.mustache
Normal file
34
templates/model-entity.spec.mustache
Normal 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}}
|
||||
14
templates/model.mock.mustache
Normal file
14
templates/model.mock.mustache
Normal 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}}
|
||||
20
templates/repository.provider.mock.mustache
Normal file
20
templates/repository.provider.mock.mustache
Normal 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}}
|
||||
20
templates/use-cases.provider.mock.mustache
Normal file
20
templates/use-cases.provider.mock.mustache
Normal 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}}
|
||||
@@ -3,8 +3,8 @@
|
||||
{{#operations}}
|
||||
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||
|
||||
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.contract';
|
||||
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.impl';
|
||||
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}.use-cases.contract';
|
||||
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl';
|
||||
|
||||
/**
|
||||
* {{classname}} Use Cases Provider
|
||||
|
||||
Reference in New Issue
Block a user