@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  <img src="favicon.svg" alt="Forge Logo" width="100" height="100">
2
2
 
3
- > **v1.3.1** — Bugfixes, New Templates & Interactive Flags
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
- └── I<Domain>.repository.ts
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/` | 11 templates TypeScript para features |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronaldjdevfs/forge",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Forge — Architecture Operating System for backend systems. Arquitectura Hexagonal, DDD pragmático y vertical slices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -232,17 +232,29 @@ export function checkStructure(features) {
232
232
 
233
233
  if (hasSubdir(fDir, "domain")) {
234
234
  const dDir = join(fDir, "domain");
235
- if (hasFile(dDir, /\.entity\.ts$/)) {
236
- checks.push({ ...severity(`${feat}: domain/<Name>.entity.ts`, SEVERITY.INFO), pass: true });
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}.entity.ts en ${feat}/domain/` });
246
+ checks.push({ ...severity(`${feat}: falta entity en domain/`, SEVERITY.ERROR), pass: false, fix: `Crear ${feat}/domain/entities/<Name>.entity.ts` });
240
247
  }
241
- if (hasFile(dDir, /^I[A-Z]/)) {
242
- checks.push({ ...severity(`${feat}: domain/I<Name>Repository.ts`, SEVERITY.INFO), pass: true });
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)}Repository.ts` });
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/entity: <Entity>.entity.ts
182
- // Entity name derived from feature dir or current filename
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
- const noEntity = stem.replace(/[Ee]ntity$/, "");
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/repository interface: I<Entity>.repository.ts
199
- if (rule.subdir === "domain" && rule.pattern === "repository") {
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
- return null;
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 expectedName = `${toPascalCase(stem)}.uc${ext}`;
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 that look like entity/repository but don't match (loose 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
- return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain: I<Name>.repository.ts` };
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
- return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain: <Name>.entity.ts` };
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>Repository.js";
5
- import type { <Domain>Entity } from "../../domain/<Domain>Entity.js";
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>Entity.js";
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,5 +1,5 @@
1
1
  ```typescript
2
- // src/features/<domain>/domain/<Domain>.entity.ts
2
+ // src/features/<domain>/domain/entities/<Domain>.entity.ts
3
3
  import type { SomeGlobalType } from "@/shared/types/types.js";
4
4
 
5
5
  export interface <Domain> {
@@ -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 "./<Domain>.entity.js";
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";