@ronaldjdevfs/forge 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/package.json +1 -1
- package/skills/forge/scripts/detect.mjs +18 -6
- package/skills/forge/scripts/rename.mjs +62 -14
- package/skills/forge/templates/feature/acl-repository.ts.md +2 -2
- package/skills/forge/templates/feature/acl-translator.ts.md +1 -1
- package/skills/forge/templates/feature/entity.ts.md +1 -1
- package/skills/forge/templates/feature/mapper.ts.md +1 -1
- package/skills/forge/templates/feature/repository-impl.ts.md +2 -2
- package/skills/forge/templates/feature/repository-interface.ts.md +2 -2
- package/skills/forge/templates/feature/saga-orchestrator.ts.md +2 -2
- package/skills/forge/templates/feature/schema.ts.md +1 -1
- package/skills/forge/templates/feature/use-case.ts.md +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<img src="favicon.svg" alt="Forge Logo" width="100" height="100">
|
|
2
2
|
|
|
3
|
-
> **v1.
|
|
3
|
+
> **v1.4.0** — Domain Subdirectory Structure & Port Support
|
|
4
4
|
|
|
5
5
|
## Forge — Backend Architecture Operating System
|
|
6
6
|
|
|
@@ -58,8 +58,10 @@ Crea un nuevo feature (vertical slice) con estructura hexagonal completa. Antes
|
|
|
58
58
|
```
|
|
59
59
|
src/features/<name>/
|
|
60
60
|
├── domain/
|
|
61
|
-
│ ├── <Domain>.entity.ts
|
|
62
|
-
│
|
|
61
|
+
│ ├── entities/ (<Domain>.entity.ts)
|
|
62
|
+
│ ├── repositories/ (I<Domain>.repository.ts)
|
|
63
|
+
│ ├── errors/ (<Domain>NotFound.error.ts)
|
|
64
|
+
│ └── events/ (<Domain>Created.event.ts)
|
|
63
65
|
├── application/
|
|
64
66
|
│ ├── use-cases/ (<Action>.uc.ts)
|
|
65
67
|
│ └── mappers/ (<Domain>.mapper.ts)
|
|
@@ -262,6 +264,10 @@ src/
|
|
|
262
264
|
├── features/ ← Capacidades de negocio
|
|
263
265
|
│ └── <name>/
|
|
264
266
|
│ ├── domain/
|
|
267
|
+
│ │ ├── entities/ (<Domain>.entity.ts)
|
|
268
|
+
│ │ ├── repositories/ (I<Domain>.repository.ts)
|
|
269
|
+
│ │ ├── errors/ (<Domain>NotFound.error.ts)
|
|
270
|
+
│ │ └── events/ (<Domain>Created.event.ts)
|
|
265
271
|
│ ├── application/
|
|
266
272
|
│ └── adapters/
|
|
267
273
|
│
|
|
@@ -291,6 +297,7 @@ src/
|
|
|
291
297
|
| Directorios | `kebab-case/` | `credit-card/`, `event-bus/` |
|
|
292
298
|
| Archivos | `<PascalCase>.<artefacto>.ts` | `User.entity.ts` |
|
|
293
299
|
| Interfaces | `I<PascalCase>.<artefacto>.ts` | `IUser.repository.ts` |
|
|
300
|
+
| Puertos (domain) | `<Name>.port.ts` | `PaymentPort.port.ts` |
|
|
294
301
|
| Use cases | `<Action>.uc.ts` | `CreateUser.uc.ts` |
|
|
295
302
|
| Clases | `PascalCase` | `UserController` |
|
|
296
303
|
| Funciones | `camelCase` | `formatDate` |
|
|
@@ -378,7 +385,7 @@ Donde vive toda la inteligencia arquitectónica:
|
|
|
378
385
|
| `reference/assay.md` | Documentación del comando assay |
|
|
379
386
|
| `reference/hooks.md` | Documentación del sistema de hooks |
|
|
380
387
|
| `profiles/` | 10 perfiles tecnológicos detallados |
|
|
381
|
-
| `templates/feature/` |
|
|
388
|
+
| `templates/feature/` | 17 templates TypeScript para features |
|
|
382
389
|
| `templates/platform/` | 6 templates para componentes de platform |
|
|
383
390
|
| `templates/shared/` | 4 templates para shared (errors, contracts, types, utils) |
|
|
384
391
|
| `templates/infra/` | 4 templates para infra (prisma, mongodb, redis, mail) |
|
package/package.json
CHANGED
|
@@ -232,17 +232,29 @@ export function checkStructure(features) {
|
|
|
232
232
|
|
|
233
233
|
if (hasSubdir(fDir, "domain")) {
|
|
234
234
|
const dDir = join(fDir, "domain");
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
const entitiesDir = join(dDir, "entities");
|
|
236
|
+
const reposDir = join(dDir, "repositories");
|
|
237
|
+
|
|
238
|
+
// Check domain/entities/ subdirectory (preferred)
|
|
239
|
+
if (hasSubdir(dDir, "entities") && hasFile(entitiesDir, /\.entity\.ts$/)) {
|
|
240
|
+
checks.push({ ...severity(`${feat}: domain/entities/<Name>.entity.ts`, SEVERITY.INFO), pass: true });
|
|
237
241
|
featScore += 3;
|
|
242
|
+
} else if (hasFile(dDir, /\.entity\.ts$/)) {
|
|
243
|
+
checks.push({ ...severity(`${feat}: domain/<Name>.entity.ts (plano — migrar a domain/entities/)`, SEVERITY.WARNING), pass: false, fix: `Mover a ${feat}/domain/entities/` });
|
|
244
|
+
featScore += 1;
|
|
238
245
|
} else {
|
|
239
|
-
checks.push({ ...severity(`${feat}: falta entity en domain/`, SEVERITY.ERROR), pass: false, fix: `Crear ${feat}
|
|
246
|
+
checks.push({ ...severity(`${feat}: falta entity en domain/`, SEVERITY.ERROR), pass: false, fix: `Crear ${feat}/domain/entities/<Name>.entity.ts` });
|
|
240
247
|
}
|
|
241
|
-
|
|
242
|
-
|
|
248
|
+
|
|
249
|
+
// Check domain/repositories/ subdirectory (preferred)
|
|
250
|
+
if (hasSubdir(dDir, "repositories") && hasFile(reposDir, /\.repository\.ts$/)) {
|
|
251
|
+
checks.push({ ...severity(`${feat}: domain/repositories/I<Name>.repository.ts`, SEVERITY.INFO), pass: true });
|
|
243
252
|
featScore += 2;
|
|
253
|
+
} else if (hasFile(dDir, /^I[A-Z]/)) {
|
|
254
|
+
checks.push({ ...severity(`${feat}: domain/I<Name>Repository.ts (plano — migrar a domain/repositories/)`, SEVERITY.WARNING), pass: false, fix: `Mover a ${feat}/domain/repositories/I<Name>.repository.ts` });
|
|
255
|
+
featScore += 1;
|
|
244
256
|
} else {
|
|
245
|
-
checks.push({ ...severity(`${feat}: falta repository interface en domain/`, SEVERITY.WARNING), pass: false, fix: `Crear I${feat.charAt(0).toUpperCase() + feat.slice(1)}
|
|
257
|
+
checks.push({ ...severity(`${feat}: falta repository interface en domain/`, SEVERITY.WARNING), pass: false, fix: `Crear ${feat}/domain/repositories/I${feat.charAt(0).toUpperCase() + feat.slice(1)}.repository.ts` });
|
|
246
258
|
}
|
|
247
259
|
} else {
|
|
248
260
|
checks.push({ ...severity(`${feat}: falta domain/`, SEVERITY.ERROR), pass: false, fix: `Crear ${feat}/domain/` });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
|
|
4
4
|
import { join, relative, dirname, basename, extname, parse } from "path";
|
|
5
5
|
|
|
6
6
|
const ROOT = process.cwd();
|
|
@@ -66,7 +66,10 @@ const PLATFORM_RULES = {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
const FEATURE_SUBDIR_RULES = [
|
|
69
|
+
{ subdir: "domain/entities", pattern: "entity", case: "pascal", description: "<Name>.entity.ts" },
|
|
70
|
+
{ subdir: "domain/repositories", pattern: "repository", prefix: "I", case: "pascal", description: "I<Name>.repository.ts" },
|
|
69
71
|
{ subdir: "domain", pattern: "entity", case: "pascal", description: "<Name>.entity.ts" },
|
|
72
|
+
{ subdir: "domain", pattern: "port", case: "pascal", description: "<Name>.port.ts" },
|
|
70
73
|
{ subdir: "domain", pattern: "repository", prefix: "I", case: "pascal", description: "I<Name>.repository.ts" },
|
|
71
74
|
{ subdir: "application/use-cases", pattern: "uc", case: "pascal", description: "<Action>.uc.ts" },
|
|
72
75
|
{ subdir: "application/mappers", pattern: "mapper", case: "pascal", description: "<Name>.mapper.ts" },
|
|
@@ -178,37 +181,73 @@ export function computeExpectedName(filePath) {
|
|
|
178
181
|
if (featureSubpath === rule.subdir || featureSubpath.startsWith(rule.subdir + "/")) {
|
|
179
182
|
const stemLower = stem.toLowerCase();
|
|
180
183
|
|
|
181
|
-
// For domain/
|
|
182
|
-
|
|
183
|
-
if (rule.subdir === "domain" && rule.pattern === "entity") {
|
|
184
|
+
// For domain/entities/: <Name>.entity.ts
|
|
185
|
+
if (rule.subdir === "domain/entities") {
|
|
184
186
|
let inferredEntity;
|
|
185
187
|
if (hasSuffix(stem, "entity")) {
|
|
186
188
|
inferredEntity = stem.replace(/\.entity$/i, "");
|
|
187
189
|
} else {
|
|
188
|
-
|
|
189
|
-
inferredEntity = noEntity || featureName;
|
|
190
|
+
inferredEntity = stem;
|
|
190
191
|
}
|
|
191
192
|
const entityStem = toPascalCase(inferredEntity);
|
|
192
193
|
const expectedName = `${entityStem}.entity${ext}`;
|
|
193
194
|
if (expectedName !== filename) {
|
|
194
|
-
return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain: ${rule.description}` };
|
|
195
|
+
return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain/entities: ${rule.description}` };
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
// For domain/
|
|
199
|
-
if (rule.subdir === "domain"
|
|
199
|
+
// For domain/repositories/: I<Name>.repository.ts
|
|
200
|
+
if (rule.subdir === "domain/repositories") {
|
|
200
201
|
const expectedStem = `I${entityName}.repository`;
|
|
201
202
|
const expectedName = `${expectedStem}${ext}`;
|
|
202
203
|
const currentMatches = stem === expectedStem;
|
|
203
204
|
if (!currentMatches) {
|
|
205
|
+
return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain/repositories: ${rule.description}` };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// For flat domain/entity: migrates to domain/entities/<Name>.entity.ts
|
|
210
|
+
if (rule.subdir === "domain" && rule.pattern === "entity" && featureSubpath === "domain") {
|
|
211
|
+
let inferredEntity;
|
|
212
|
+
if (hasSuffix(stem, "entity")) {
|
|
213
|
+
inferredEntity = stem.replace(/\.entity$/i, "");
|
|
214
|
+
} else {
|
|
215
|
+
const noEntity = stem.replace(/[Ee]ntity$/, "");
|
|
216
|
+
inferredEntity = noEntity || featureName;
|
|
217
|
+
}
|
|
218
|
+
const entityStem = toPascalCase(inferredEntity);
|
|
219
|
+
const expectedName = `${entityStem}.entity${ext}`;
|
|
220
|
+
const expectedPath = join(dirname(filePath), "entities", expectedName);
|
|
221
|
+
return { current: filePath, expected: expectedPath, relPath: join(dirname(relPath), "entities", expectedName), rule: `feature/${featureName}/domain → domain/entities: ${rule.description}` };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// For domain/port: <Name>.port.ts
|
|
225
|
+
if (rule.subdir === "domain" && rule.pattern === "port") {
|
|
226
|
+
let inferredPort;
|
|
227
|
+
if (hasSuffix(stem, "port")) {
|
|
228
|
+
inferredPort = stem.replace(/\.port$/i, "");
|
|
229
|
+
} else {
|
|
230
|
+
inferredPort = stem;
|
|
231
|
+
}
|
|
232
|
+
const portStem = toPascalCase(inferredPort);
|
|
233
|
+
const expectedName = `${portStem}.port${ext}`;
|
|
234
|
+
if (expectedName !== filename) {
|
|
204
235
|
return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain: ${rule.description}` };
|
|
205
236
|
}
|
|
206
|
-
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// For flat domain/repository: migrates to domain/repositories/I<Entity>.repository.ts
|
|
240
|
+
if (rule.subdir === "domain" && rule.pattern === "repository" && featureSubpath === "domain") {
|
|
241
|
+
const expectedStem = `I${entityName}.repository`;
|
|
242
|
+
const expectedName = `${expectedStem}${ext}`;
|
|
243
|
+
const expectedPath = join(dirname(filePath), "repositories", expectedName);
|
|
244
|
+
return { current: filePath, expected: expectedPath, relPath: join(dirname(relPath), "repositories", expectedName), rule: `feature/${featureName}/domain → domain/repositories: ${rule.description}` };
|
|
207
245
|
}
|
|
208
246
|
|
|
209
247
|
// For use-cases: <Action>.uc.ts
|
|
210
248
|
if (rule.subdir === "application/use-cases") {
|
|
211
|
-
const
|
|
249
|
+
const cleanStem = hasSuffix(stem, "uc") ? stem.replace(/\.uc$/i, "") : stem;
|
|
250
|
+
const expectedName = `${toPascalCase(cleanStem)}.uc${ext}`;
|
|
212
251
|
if (expectedName !== filename) {
|
|
213
252
|
return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/application/use-cases: ${rule.description}` };
|
|
214
253
|
}
|
|
@@ -237,15 +276,21 @@ export function computeExpectedName(filePath) {
|
|
|
237
276
|
}
|
|
238
277
|
}
|
|
239
278
|
|
|
240
|
-
// Check domain/ for files
|
|
279
|
+
// Check domain/ for loose files without suffix → migrate to subdirectory
|
|
241
280
|
if (featureSubpath === "domain" && !stem.includes(".")) {
|
|
242
281
|
const lower = stem.toLowerCase();
|
|
243
282
|
if (lower.includes("repository") || stem.startsWith("I")) {
|
|
244
283
|
const expectedName = `I${entityName}.repository${ext}`;
|
|
245
|
-
|
|
284
|
+
const expectedPath = join(dirname(filePath), "repositories", expectedName);
|
|
285
|
+
return { current: filePath, expected: expectedPath, relPath: join(dirname(relPath), "repositories", expectedName), rule: `feature/${featureName}/domain → domain/repositories: I<Name>.repository.ts` };
|
|
286
|
+
}
|
|
287
|
+
if (lower.includes("port")) {
|
|
288
|
+
const expectedName = `${entityName}.port${ext}`;
|
|
289
|
+
return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain: <Name>.port.ts` };
|
|
246
290
|
}
|
|
247
291
|
const expectedName = `${entityName}.entity${ext}`;
|
|
248
|
-
|
|
292
|
+
const expectedPath = join(dirname(filePath), "entities", expectedName);
|
|
293
|
+
return { current: filePath, expected: expectedPath, relPath: join(dirname(relPath), "entities", expectedName), rule: `feature/${featureName}/domain → domain/entities: <Name>.entity.ts` };
|
|
249
294
|
}
|
|
250
295
|
|
|
251
296
|
return null;
|
|
@@ -536,6 +581,9 @@ export function renameFile(oldPath, newPath, projectRoot = ROOT) {
|
|
|
536
581
|
return { success: true, updatedImports: [], note: "El archivo ya tiene el nombre correcto" };
|
|
537
582
|
}
|
|
538
583
|
|
|
584
|
+
// Ensure target directory exists
|
|
585
|
+
mkdirSync(dirname(newPath), { recursive: true });
|
|
586
|
+
|
|
539
587
|
// Physical rename
|
|
540
588
|
try {
|
|
541
589
|
renameSync(oldPath, newPath);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/adapters/out/legacy-<system>/<Domain>ACL.ts
|
|
3
3
|
import { injectable, inject } from "tsyringe";
|
|
4
|
-
import type { I<Domain>Repository } from "../../domain/I<Domain
|
|
5
|
-
import type { <Domain>Entity } from "../../domain/<Domain
|
|
4
|
+
import type { I<Domain>Repository } from "../../domain/repositories/I<Domain>.repository.js";
|
|
5
|
+
import type { <Domain>Entity } from "../../domain/entities/<Domain>.entity.js";
|
|
6
6
|
import { <Domain>Gateway } from "./<Domain>Gateway.js";
|
|
7
7
|
import { <Domain>Translator } from "./<Domain>Translator.js";
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/adapters/out/legacy-<system>/<Domain>Translator.ts
|
|
3
|
-
import type { <Domain>Entity } from "../../../domain/<Domain
|
|
3
|
+
import type { <Domain>Entity } from "../../../domain/entities/<Domain>.entity.js";
|
|
4
4
|
import type { External<Domain>DTO } from "./External<Domain>DTO.js";
|
|
5
5
|
|
|
6
6
|
export class <Domain>Translator {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/application/mappers/<Domain>.mapper.ts
|
|
3
|
-
import type { <Domain> } from "../domain/<Domain>.entity.js";
|
|
3
|
+
import type { <Domain> } from "../domain/entities/<Domain>.entity.js";
|
|
4
4
|
|
|
5
5
|
export class <Domain>Mapper {
|
|
6
6
|
static toDomain(doc: Record<string, any>): <Domain> {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/adapters/out/persistence/<Domain>.repository.ts
|
|
3
3
|
import { injectable } from "tsyringe";
|
|
4
|
-
import type { <Domain> } from "../../../domain/<Domain>.entity.js";
|
|
5
|
-
import type { I<Domain>Repository } from "../../../domain/I<Domain>.repository.js";
|
|
4
|
+
import type { <Domain> } from "../../../domain/entities/<Domain>.entity.js";
|
|
5
|
+
import type { I<Domain>Repository } from "../../../domain/repositories/I<Domain>.repository.js";
|
|
6
6
|
import { <Domain>Mapper } from "../../../application/mappers/<Domain>.mapper.js";
|
|
7
7
|
import <Domain>Model from "./<Domain>.schema.js";
|
|
8
8
|
import { RepositoryError } from "@/shared/errors/RepositoryError.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
```typescript
|
|
2
|
-
// src/features/<domain>/domain/I<Domain>.repository.ts
|
|
3
|
-
import type { <Domain> } from "
|
|
2
|
+
// src/features/<domain>/domain/repositories/I<Domain>.repository.ts
|
|
3
|
+
import type { <Domain> } from "../entities/<Domain>.entity.js";
|
|
4
4
|
|
|
5
5
|
export interface I<Domain>Repository {
|
|
6
6
|
create(data: Partial<<Domain>>, session?: unknown): Promise<<Domain>>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/adapters/out/saga/SagaOrchestrator.ts
|
|
3
3
|
import { injectable, inject } from "tsyringe";
|
|
4
|
-
import type { SagaInstance, SagaStatus } from "../../domain/SagaInstance.entity.js";
|
|
5
|
-
import type { ISagaRepository } from "../../domain/ISagaRepository.js";
|
|
4
|
+
import type { SagaInstance, SagaStatus } from "../../domain/entities/SagaInstance.entity.js";
|
|
5
|
+
import type { ISagaRepository } from "../../domain/repositories/ISagaRepository.repository.js";
|
|
6
6
|
import type { IEventBus } from "@/platform/events/IEventBus.js";
|
|
7
7
|
import type { ILogger } from "@/platform/logger/ILogger.js";
|
|
8
8
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/adapters/out/persistence/<Domain>.schema.ts
|
|
3
3
|
import { model, Schema } from "mongoose";
|
|
4
|
-
import type { <Domain> } from "../../../domain/<Domain>.entity.js";
|
|
4
|
+
import type { <Domain> } from "../../../domain/entities/<Domain>.entity.js";
|
|
5
5
|
|
|
6
6
|
const <Domain>Schema = new Schema<<Domain>>(
|
|
7
7
|
{
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/application/use-cases/Create<Domain>.uc.ts
|
|
3
3
|
import { injectable, inject } from "tsyringe";
|
|
4
|
-
import type { <Domain> } from "../../domain/<Domain>.entity.js";
|
|
5
|
-
import type { I<Domain>Repository } from "../../domain/I<Domain>.repository.js";
|
|
4
|
+
import type { <Domain> } from "../../domain/entities/<Domain>.entity.js";
|
|
5
|
+
import type { I<Domain>Repository } from "../../domain/repositories/I<Domain>.repository.js";
|
|
6
6
|
import { UseCaseError } from "@/shared/errors/UseCaseError.js";
|
|
7
7
|
// Opcional: errores de dominio y eventos
|
|
8
8
|
// import { <Domain>NotFoundError } from "../../domain/errors/<Domain>NotFound.error.js";
|