@skapxd/eslint-opinionated 0.13.0 → 0.14.0
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 +681 -0
- package/dist/astro/index.d.mts +14 -0
- package/dist/astro/index.d.ts +14 -0
- package/dist/astro/index.js +10 -0
- package/dist/astro/index.js.map +1 -1
- package/dist/astro/index.mjs +2 -2
- package/dist/{chunk-JA2ZO3KQ.mjs → chunk-C5JP4FQN.mjs} +2 -2
- package/dist/chunk-NBEAEKXG.mjs +140 -0
- package/dist/chunk-NBEAEKXG.mjs.map +1 -0
- package/dist/{chunk-QADXO5IL.mjs → chunk-QR35MHTW.mjs} +59 -3
- package/dist/chunk-QR35MHTW.mjs.map +1 -0
- package/dist/{chunk-DJXM5PMG.mjs → chunk-TZ5XPZGL.mjs} +2 -2
- package/dist/{chunk-UXF7WZ5B.mjs → chunk-YUKVSZZ4.mjs} +1646 -85
- package/dist/chunk-YUKVSZZ4.mjs.map +1 -0
- package/dist/index.js +1838 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -5
- package/dist/index.mjs.map +1 -1
- package/dist/nest/index.d.mts +112 -0
- package/dist/nest/index.d.ts +112 -0
- package/dist/nest/index.js +248 -0
- package/dist/nest/index.js.map +1 -0
- package/dist/nest/index.mjs +8 -0
- package/dist/nest/index.mjs.map +1 -0
- package/dist/next/index.d.mts +14 -0
- package/dist/next/index.d.ts +14 -0
- package/dist/next/index.js +10 -0
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +2 -2
- package/dist/shared/index.d.mts +43 -0
- package/dist/shared/index.d.ts +43 -0
- package/dist/shared/index.js +1727 -111
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +2 -2
- package/package.json +6 -1
- package/dist/chunk-QADXO5IL.mjs.map +0 -1
- package/dist/chunk-UXF7WZ5B.mjs.map +0 -1
- /package/dist/{chunk-JA2ZO3KQ.mjs.map → chunk-C5JP4FQN.mjs.map} +0 -0
- /package/dist/{chunk-DJXM5PMG.mjs.map → chunk-TZ5XPZGL.mjs.map} +0 -0
package/README.md
CHANGED
|
@@ -90,6 +90,28 @@ Quiero que un agente pueda generar código, pero que el proyecto le conteste:
|
|
|
90
90
|
|
|
91
91
|
Eso es lo que estas reglas intentan proteger.
|
|
92
92
|
|
|
93
|
+
## Los axiomas
|
|
94
|
+
|
|
95
|
+
Las reglas no son una colección de gustos: se derivan de ocho axiomas. Si una
|
|
96
|
+
regla nueva no es consecuencia de alguno, no entra. Si dos reglas chocan, gana
|
|
97
|
+
la que defiende el axioma más fundamental (el orden es jerárquico).
|
|
98
|
+
|
|
99
|
+
| # | Axioma | Reglas que lo ejecutan |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| A1 | **Los estados imposibles son irrepresentables.** El tipo modela exactamente los estados válidos; lo inválido no compila. | `prefer-tagged-union-state`, `no-runtime-state-guard`, `requires-strict-tsconfig`, `@typescript-eslint/no-unnecessary-condition`, `no-explicit-any`, `consistent-type-definitions` |
|
|
102
|
+
| A2 | **Ningún efecto es invisible al tipo.** Si una operación puede fallar, su firma lo confiesa — no una convención oral ni un `throw` sorpresa. | `await-requires-result`, `no-try-catch`, `no-promise-chain`, `no-ad-hoc-ok-result`, `@typescript-eslint/no-floating-promises` |
|
|
103
|
+
| A3 | **La información no se destruye.** Un error que se transforma conserva su `cause`; uno que se detecta llega a alguien. Nadie decide "esto no importa" en silencio. | `result-error-requires-cause`, `result-error-requires-handling` |
|
|
104
|
+
| A4 | **Una unidad, una responsabilidad, un nombre semántico.** El árbol de archivos cuenta una historia; una clase expone una intención. | `one-root-function-per-file`, `max-public-methods`, `no-default-export`, `jsx-return-name-pascal-case`, `max-hook-size` |
|
|
105
|
+
| A5 | **Las decisiones se declaran, no se interpretan.** Cada rama es explícita y exhaustiva; un caso ignorado es una decisión visible, no un hueco. | `no-else`, `no-nested-if`, `prefer-ts-pattern`, `@typescript-eslint/ban-ts-comment`, el `void promesa()` de `no-floating-promises` |
|
|
106
|
+
| A6 | **Evidencia sobre convención.** Una regla decide por lo que el type-checker o los imports demuestran, no por cómo se llama un archivo o un campo. | la implementación type-aware de las reglas de Result, `nest-no-direct-instantiation` (@Injectable resuelto por símbolo), la exención ORM por decorador de `class-properties-require-readonly` |
|
|
107
|
+
| A7 | **Las fronteras son explícitas y únicas.** Lo que cruza una capa lo hace por un contrato, una sola vez, sin túneles. | `no-deep-relative-imports`, `no-tunnel-props`, `nest-no-result-response`, `nest-no-swagger-in-controllers`, `nest-no-inline-query-params` |
|
|
108
|
+
| A8 | **Inmutable por defecto.** La mutación es la excepción que se pide con evidencia, no el estado natural de las cosas. | `class-properties-require-readonly`, `no-accessors` |
|
|
109
|
+
|
|
110
|
+
A6 es distinto a los demás: no produce reglas, produce **cómo se implementan**
|
|
111
|
+
todas. Por eso las reglas de este paquete prefieren parser services y
|
|
112
|
+
provenance de imports antes que globs de nombres — el nombre es la evidencia
|
|
113
|
+
más débil que aceptamos, y solo como último recurso.
|
|
114
|
+
|
|
93
115
|
## Por qué las alternativas no bastan
|
|
94
116
|
|
|
95
117
|
### ESLint core
|
|
@@ -353,6 +375,9 @@ src/
|
|
|
353
375
|
│ ├── rules.ts
|
|
354
376
|
│ ├── configs/
|
|
355
377
|
│ └── index.ts
|
|
378
|
+
├── nest/
|
|
379
|
+
│ ├── configs.ts
|
|
380
|
+
│ └── index.ts
|
|
356
381
|
├── next/
|
|
357
382
|
│ ├── configs.ts
|
|
358
383
|
│ └── index.ts
|
|
@@ -365,6 +390,7 @@ src/
|
|
|
365
390
|
| Módulo | Propósito |
|
|
366
391
|
| --- | --- |
|
|
367
392
|
| `@skapxd/eslint-opinionated/shared` | Reglas y presets comunes para backend, frontend y paquetes npm. |
|
|
393
|
+
| `@skapxd/eslint-opinionated/nest` | Presets específicos para NestJS. |
|
|
368
394
|
| `@skapxd/eslint-opinionated/next` | Presets específicos para Next.js. |
|
|
369
395
|
| `@skapxd/eslint-opinionated/astro` | Presets específicos para Astro. |
|
|
370
396
|
| `@skapxd/eslint-opinionated` | Entry point principal con todas las reglas y configs. |
|
|
@@ -478,6 +504,68 @@ export default [
|
|
|
478
504
|
];
|
|
479
505
|
```
|
|
480
506
|
|
|
507
|
+
### NestJS
|
|
508
|
+
|
|
509
|
+
```js
|
|
510
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
511
|
+
|
|
512
|
+
export default [
|
|
513
|
+
...skapxd.configs.nest,
|
|
514
|
+
];
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Nest trae un modelo de errores por excepciones (`HttpException` + exception
|
|
518
|
+
filters). El preset no pelea contra eso: asigna a cada capa su rol en el
|
|
519
|
+
pipeline de Result:
|
|
520
|
+
|
|
521
|
+
| Capa Nest | Rol | Contrato |
|
|
522
|
+
| --- | --- | --- |
|
|
523
|
+
| Services / use-cases | El dominio puro | Todo retorna `Promise<Result<T, DomainError>>`; `trySafe` en la frontera con Mongoose/Prisma/HTTP |
|
|
524
|
+
| Controllers | La frontera | Consumen el Result con `match()`: rama ok → DTO, rama err → `throw new HttpException(...)`. El `throw` aquí es el idioma del framework, no una fuga |
|
|
525
|
+
| Exception filter global | **El suelo del sistema** | Recibe todo lo que escapó, con el `cause` completo → telemetría/log (ver "El suelo del sistema") |
|
|
526
|
+
|
|
527
|
+
Detalles del preset:
|
|
528
|
+
|
|
529
|
+
- Aplica a `src/**/*.ts` — `dev/`, `scripts/`, `e2e/` e `integration-test/`
|
|
530
|
+
quedan fuera a propósito: no son la app.
|
|
531
|
+
- Los entrypoints (`main.ts`, `instrumentation.ts`, `app-cluster.ts`) están
|
|
532
|
+
exentos de `await-requires-result`: el bootstrap debe crashear ruidoso.
|
|
533
|
+
Con `no-floating-promises` activa, el clásico `bootstrap();` del `main.ts`
|
|
534
|
+
se escribe `void bootstrap();` — fire-and-forget declarado.
|
|
535
|
+
- Los specs colocados (`*.spec.ts`, `*.e2e-spec.ts`) relajan
|
|
536
|
+
`await-requires-result`, `no-try-catch`, `result-error-requires-handling` y
|
|
537
|
+
`@typescript-eslint/no-non-null-assertion` (el `!` sobre un fixture es el
|
|
538
|
+
arrange del test): un test awaitea helpers libremente y descartar un Result
|
|
539
|
+
en una aserción no es perder un trace. `no-floating-promises` sigue activa
|
|
540
|
+
en specs: un `await` olvidado es un falso verde.
|
|
541
|
+
- Activa `skapxd/nest-no-result-response` (ver su sección): un controller
|
|
542
|
+
jamás retorna el Result crudo.
|
|
543
|
+
- **El contrato Swagger vive en los DTOs, no en el controller.** El preset
|
|
544
|
+
asume el plugin `@nestjs/swagger` activo en `nest-cli.json` (introspecciona
|
|
545
|
+
query/params/body y tipo de retorno solo): `nest-dto-requires-api-property`
|
|
546
|
+
exige `@ApiProperty` en toda propiedad pública de un `*.dto.ts`, y
|
|
547
|
+
`nest-no-swagger-in-controllers` prohíbe los decoradores redundantes
|
|
548
|
+
(`@ApiOperation`, `@ApiResponse`, `@ApiParam`, ...) en los controllers —
|
|
549
|
+
solo se permiten los que el plugin no puede inferir: `ApiExcludeEndpoint`,
|
|
550
|
+
`ApiTags`, `ApiBearerAuth`, `ApiConsumes`/`ApiBody` (uploads multipart).
|
|
551
|
+
- **Los DTOs de input validan en runtime**: `nest-dto-requires-validation`
|
|
552
|
+
exige class-validator en cada propiedad, coherencia `?` ↔ `@IsOptional`, y
|
|
553
|
+
`@Type` de class-transformer junto a `@ValidateNested`. Los DTOs de
|
|
554
|
+
respuesta (`out-*`, `*-response`, ...) quedan exentos.
|
|
555
|
+
- **Una clase = una responsabilidad**: `max-public-methods` (de las reglas
|
|
556
|
+
base) corre con los hooks de Nest inyectados vía `ignore`, y se apaga en
|
|
557
|
+
`*.controller.ts`/`*.gateway.ts` donde el framework dicta la forma.
|
|
558
|
+
`nest-no-direct-instantiation` (dependencias por constructor, no `new`) en
|
|
559
|
+
`*.service.ts`; `nest-no-inline-query-params` en `*.controller.ts` (2+
|
|
560
|
+
query params → DTO consolidado).
|
|
561
|
+
- **La configuración del proyecto también se lintea**: las premisas de las
|
|
562
|
+
que dependen las demás reglas se verifican, no se asumen.
|
|
563
|
+
`nest-requires-swagger-plugin` lee el `nest-cli.json` real (subiendo desde
|
|
564
|
+
`src/main.ts`) y exige el plugin `@nestjs/swagger`;
|
|
565
|
+
`nest-validation-pipe-config` exige `transform: true` (sin él, los `@Type`
|
|
566
|
+
de los DTOs no hacen nada) y `whitelist: true` (sin él, las props sin
|
|
567
|
+
decorador pasan crudas) en todo `new ValidationPipe`.
|
|
568
|
+
|
|
481
569
|
### Astro
|
|
482
570
|
|
|
483
571
|
```js
|
|
@@ -573,14 +661,29 @@ de cada regla):
|
|
|
573
661
|
| `async-functions-return-result` | `allowFilePatterns` (globs), `allowNamePatterns` (regex), `checkMissingReturnType`, `checkMissingReturnTypeWhenCallNames`, `requireCallNames`, `promiseTypeNames`, `resultTypeNames` |
|
|
574
662
|
| `await-requires-result` | `allowFilePatterns` (globs), `trySafeCallNames` |
|
|
575
663
|
| `max-hook-size` | `maxLines`, `maxUseState` |
|
|
664
|
+
| `class-properties-require-readonly` | `allowFilePatterns` (globs), `allowPropertyPatterns` (regex), `ormModuleSources` (default `["@nestjs/mongoose", "typeorm"]`) |
|
|
665
|
+
| `max-public-methods` | `allowFilePatterns` (globs), `max` (default `1`), `ignore` (aditivo a los hooks de Nest) |
|
|
666
|
+
| `no-accessors` | `allowFilePatterns` (globs) |
|
|
667
|
+
| `nest-dto-requires-api-property` | `allowFilePatterns` (globs), `dtoFilePatterns` (default `["*.dto.ts"]`), `apiPropertyDecoratorNames` |
|
|
668
|
+
| `nest-dto-requires-validation` | `allowFilePatterns` (globs), `dtoFilePatterns`, `outputDtoFilePatterns`, `outputDtoClassPatterns` (regex), `optionalDecoratorNames` |
|
|
669
|
+
| `nest-no-direct-instantiation` | `allowFilePatterns` (globs), `internalPatterns` (regex), `allowedPatterns` (regex), `allowedClassPatterns` (regex, default `(Error|Exception|Event)$`) |
|
|
670
|
+
| `nest-no-inline-query-params` | `allowFilePatterns` (globs), `max` (default `1`) |
|
|
671
|
+
| `nest-no-result-response` | `allowFilePatterns` (globs), `controllerDecoratorNames` (default `["Controller"]`) |
|
|
672
|
+
| `nest-no-swagger-in-controllers` | `allowFilePatterns` (globs), `allowedDecoratorNames`, `controllerDecoratorNames` |
|
|
673
|
+
| `nest-requires-swagger-plugin` | `allowFilePatterns` (globs), `mainFilePatterns` (default `["src/main.ts"]`) |
|
|
674
|
+
| `nest-validation-pipe-config` | `allowFilePatterns` (globs), `requiredPipeOptions` (default `["transform", "whitelist"]`) |
|
|
576
675
|
| `no-deep-relative-imports` | `maxDepth` |
|
|
577
676
|
| `no-default-export` | `allowFilePatterns` (globs, aditivos a los integrados) |
|
|
677
|
+
| `no-else` | `allowFilePatterns` (globs) |
|
|
578
678
|
| `no-emoji` | `allowFilePatterns` (globs) |
|
|
579
679
|
| `no-functions-inside-components` | `allowJsxCallbacks`, `allowArrayMapCallbacks` (ambas `true` por defecto) |
|
|
580
680
|
| `no-nested-if` | `allowFilePatterns` (globs) |
|
|
581
681
|
| `no-promise-chain` | `methods` |
|
|
682
|
+
| `no-runtime-state-guard` | `allowFilePatterns` (globs) |
|
|
582
683
|
| `no-tunnel-props` | `allowFilePatterns` (globs), `allowPropPatterns` (regex) |
|
|
583
684
|
| `prefer-abort-signal` | `allowFilePatterns` (globs), `effectNames` (default `["useEffect", "useLayoutEffect"]`) |
|
|
685
|
+
| `prefer-tagged-union-state` | `allowFilePatterns` (globs), `loadingPatterns` (regex, en minúsculas), `errorPatterns` (regex, en minúsculas) |
|
|
686
|
+
| `requires-strict-tsconfig` | `allowFilePatterns` (globs), `anchorFilePatterns` (globs), `requiredCompilerOptions` |
|
|
584
687
|
| `result-error-requires-handling` | `allowFilePatterns` (globs) |
|
|
585
688
|
|
|
586
689
|
Los `allowFilePatterns` de todas las reglas son **globs** (`*` un segmento,
|
|
@@ -594,18 +697,33 @@ matchea en cualquier carpeta). Las 7 reglas restantes no tienen opciones: su
|
|
|
594
697
|
| --- | --- |
|
|
595
698
|
| `skapxd/one-root-function-per-file` | Un archivo, una función top-level semántica. |
|
|
596
699
|
| `skapxd/async-functions-return-result` | Funciones async de dominio deben retornar `Promise<Result<...>>`. **Apagada por defecto; opt-in** (ver motivos en su sección). |
|
|
700
|
+
| `skapxd/requires-strict-tsconfig` | El `tsconfig` debe ser implacable (`strict`, `noImplicitReturns`, `noUncheckedIndexedAccess`): sin ellos, el compilador no puede hacer irrepresentable lo inválido. |
|
|
597
701
|
| `skapxd/result-error-requires-cause` | Un `Result.err` derivado debe preservar `cause: result.error`. |
|
|
598
702
|
| `skapxd/result-error-requires-handling` | Prohíbe descartar en silencio un Result fallido: el error se transforma o se entrega, nunca se ignora. |
|
|
599
703
|
| `skapxd/await-requires-result` | Todo `await` debe resolver en un `Result`: o la función llamada retorna `Promise<Result<...>>` (preferido) o se envuelve en `trySafe`. **Obligatoria en todos los presets tipados.** |
|
|
600
704
|
| `skapxd/no-ad-hoc-ok-result` | Evita contratos `{ ok: ... }` hechos a mano en async exports. |
|
|
601
705
|
| `skapxd/max-hook-size` | Marca hooks grandes o con demasiados `useState`. |
|
|
706
|
+
| `skapxd/class-properties-require-readonly` | Toda propiedad de clase es `readonly`: el cambio se modela con instancias nuevas, no con mutación. |
|
|
707
|
+
| `skapxd/max-public-methods` | Una clase, una responsabilidad: máximo N métodos públicos (default 1). Agnóstica al framework, en las reglas base; el preset `nest` le inyecta sus hooks. |
|
|
708
|
+
| `skapxd/no-accessors` | Prohíbe `get`/`set`: un método explícito dice la verdad; el accessor esconde computación (y métodos disfrazados). |
|
|
602
709
|
| `skapxd/jsx-return-name-pascal-case` | Funciones que retornan JSX deben nombrarse como componentes. |
|
|
710
|
+
| `skapxd/nest-dto-requires-api-property` | Toda propiedad pública de un `*.dto.ts` lleva `@ApiProperty`: el contrato HTTP se documenta en el DTO. Preset `nest`. |
|
|
711
|
+
| `skapxd/nest-dto-requires-validation` | Los DTOs de input validan en runtime: class-validator en cada propiedad, `@IsOptional` si hay `?`, `@Type` junto a `@ValidateNested`. Preset `nest`. |
|
|
712
|
+
| `skapxd/nest-no-direct-instantiation` | Prohíbe `new` sobre imports internos en services: las dependencias entran por el constructor (DI). Preset `nest`. |
|
|
713
|
+
| `skapxd/nest-no-inline-query-params` | Dos o más `@Query('x')`/`@ApiQuery` individuales son un DTO disfrazado: consolida en `@Query() filters: Dto`. Preset `nest`. |
|
|
714
|
+
| `skapxd/nest-no-result-response` | Los métodos de un `@Controller` no retornan `Result`: el envelope se serializaría al cliente. La activa el preset `nest`. |
|
|
715
|
+
| `skapxd/nest-no-swagger-in-controllers` | Los controllers no se llenan de decoradores de swagger; el plugin introspecciona los DTOs. Preset `nest`. |
|
|
716
|
+
| `skapxd/nest-requires-swagger-plugin` | `nest-cli.json` debe tener el plugin `@nestjs/swagger`: la premisa de las reglas de swagger, verificada. Preset `nest`. |
|
|
717
|
+
| `skapxd/nest-validation-pipe-config` | Todo `new ValidationPipe` configura `transform` y `whitelist`: la premisa de las reglas de DTOs. Preset `nest`. |
|
|
603
718
|
| `skapxd/no-deep-relative-imports` | Limita la profundidad de los imports relativos (`../`). |
|
|
604
719
|
| `skapxd/no-default-export` | Prohíbe `export default`; el nombre del símbolo es el contrato. Exime configs/stories y, en el preset `next`, los entrypoints del App Router. |
|
|
720
|
+
| `skapxd/no-else` | Prohíbe `else`/`else if`: el else es el estado sin nombre. Retorno anticipado, ternario simple o `match()`. |
|
|
605
721
|
| `skapxd/no-emoji` | Prohíbe emojis en strings y JSX; cada sistema los renderiza distinto. Usa un icono SVG. |
|
|
606
722
|
| `skapxd/no-nested-if` | Prohíbe `if` anidados: retorno anticipado o `match()`. Menos carga cognitiva y sin puntos ciegos para las demás reglas. |
|
|
723
|
+
| `skapxd/no-runtime-state-guard` | Prohíbe `if (this.x) throw` en métodos: el estado inválido se hace irrepresentable en el tipo, no se vigila en runtime. |
|
|
607
724
|
| `skapxd/no-tunnel-props` | Ninguna prop viaja más de un nivel: quien la recibe no puede reenviarla a otro componente. Mata el prop drilling. |
|
|
608
725
|
| `skapxd/prefer-abort-signal` | Listeners en efectos se limpian con `AbortController` (`{ signal }` + `abort()`), no con `removeEventListener`. |
|
|
726
|
+
| `skapxd/prefer-tagged-union-state` | Prohíbe estados inconsistentes representables: flag de loading + campo de error independientes → unión etiquetada. |
|
|
609
727
|
| `skapxd/no-functions-inside-components` | Prohíbe definir funciones dentro de componentes React. |
|
|
610
728
|
| `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
|
|
611
729
|
| `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
|
|
@@ -708,6 +826,78 @@ Todas las opciones, con sus defaults:
|
|
|
708
826
|
}]
|
|
709
827
|
```
|
|
710
828
|
|
|
829
|
+
### `skapxd/requires-strict-tsconfig`
|
|
830
|
+
|
|
831
|
+
Todo el sistema descansa en que el compilador pueda hacer irrepresentables
|
|
832
|
+
los estados inválidos — y eso exige un `tsconfig` implacable. Esta regla lee
|
|
833
|
+
el `tsconfig.json` **real** del proyecto (con la API de TypeScript: soporta
|
|
834
|
+
JSONC y resuelve la cadena de `extends`) y exige los flags, anclada a un
|
|
835
|
+
entrypoint para reportar una vez por proyecto:
|
|
836
|
+
|
|
837
|
+
- `strict` — sin él, el sistema de tipos está apagado a medias.
|
|
838
|
+
- `noImplicitReturns` — una rama que sale sin valor deja de ser silenciosa
|
|
839
|
+
(la pareja en compilación de `no-else`).
|
|
840
|
+
- `noUncheckedIndexedAccess` — `array[i]` y los accesos dinámicos confiesan
|
|
841
|
+
su `undefined` en vez de fingir.
|
|
842
|
+
|
|
843
|
+
`strict: true` **no implica** los otros dos: hay que pedirlos explícitos.
|
|
844
|
+
Fuera del default, a propósito: `exactOptionalPropertyTypes` y
|
|
845
|
+
`strictPropertyInitialization` chocan con los DTOs de class-transformer y
|
|
846
|
+
con muchas librerías — se agregan vía `requiredCompilerOptions` si el
|
|
847
|
+
proyecto los soporta.
|
|
848
|
+
|
|
849
|
+
Además, los **presets tipados activan reglas curadas de typescript-eslint**
|
|
850
|
+
(que ya es peer dependency — no se reimplementan):
|
|
851
|
+
|
|
852
|
+
- `@typescript-eslint/no-explicit-any` — `any` apaga el sistema de tipos:
|
|
853
|
+
todo el esfuerzo muere donde aparece uno.
|
|
854
|
+
- `@typescript-eslint/consistent-type-definitions` con `type` — las uniones
|
|
855
|
+
discriminadas son types.
|
|
856
|
+
- `@typescript-eslint/no-floating-promises` — cierra el hueco que
|
|
857
|
+
`await-requires-result` no ve: una llamada async **sin** `await` no produce
|
|
858
|
+
`AwaitExpression`, así que el rechazo muere sin pasar por `trySafe`
|
|
859
|
+
(medido: 12 promesas flotantes vivas en un backend Nest real). La única
|
|
860
|
+
salida es `void promesa()`: fire-and-forget declarado y greppeable.
|
|
861
|
+
- `@typescript-eslint/no-non-null-assertion` — `!` es "cállate, yo sé más
|
|
862
|
+
que tú" dicho al compilador. Si no puede ser nulo, que lo diga el tipo.
|
|
863
|
+
(En `nest/tests` queda apagada: el `!` sobre un fixture cuya existencia el
|
|
864
|
+
propio test garantiza es el arrange, no una mentira.)
|
|
865
|
+
- `@typescript-eslint/no-unnecessary-condition` — la generalización
|
|
866
|
+
type-aware de `no-runtime-state-guard`: si el tipo dice que un estado es
|
|
867
|
+
imposible, el guard defensivo sobra; si el guard hace falta, lo que está
|
|
868
|
+
mal es el tipo. Por eso esta regla y `requires-strict-tsconfig` van
|
|
869
|
+
juntas: sin `noUncheckedIndexedAccess`, `array[i]` miente y la regla
|
|
870
|
+
acusaría guards necesarios.
|
|
871
|
+
- `@typescript-eslint/ban-ts-comment` — un error de tipos se arregla
|
|
872
|
+
modelando mejor, no silenciando la alarma: `@ts-ignore` y `@ts-nocheck`
|
|
873
|
+
prohibidos. `@ts-expect-error` **con descripción** queda permitido: es la
|
|
874
|
+
forma legítima de testear que un estado inválido de verdad no compila.
|
|
875
|
+
|
|
876
|
+
Ausencias deliberadas, no olvidos:
|
|
877
|
+
|
|
878
|
+
| Regla ausente | Por qué |
|
|
879
|
+
| --- | --- |
|
|
880
|
+
| `switch-exhaustiveness-check` | `prefer-ts-pattern` prohíbe el `switch` entero; `match().exhaustive()` da la misma garantía sin él. |
|
|
881
|
+
| `prefer-readonly` | Superada por `class-properties-require-readonly`: exige `readonly` en la declaración, no solo en privados nunca reasignados. |
|
|
882
|
+
| `strict-boolean-expressions` | Castiga narrowing legítimo por cientos (560 hallazgos en un backend real) sin hacer irrepresentable ningún estado nuevo. Ruido, no señal. |
|
|
883
|
+
| `explicit-module-boundary-types` | Los contratos que importan ya están gobernados (`await-requires-result`, `nest-no-result-response`); anotar el resto es ceremonia (198 hallazgos) que la inferencia resuelve sin perder garantías. |
|
|
884
|
+
| `prefer-readonly-parameter-types` | Impracticable con cualquier parámetro que venga de una librería externa. |
|
|
885
|
+
|
|
886
|
+
Complemento recomendado (fuera del alcance de un linter): **tests a nivel de
|
|
887
|
+
tipos**. Si el dominio vive en los tipos, los tipos también se testean — con
|
|
888
|
+
[`expectTypeOf` de vitest](https://vitest.dev/guide/testing-types) (ya está en
|
|
889
|
+
tu stack, sin instalar `tsd`) o con `@ts-expect-error` descrito, que es
|
|
890
|
+
exactamente el caso que `ban-ts-comment` deja abierto:
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
import { expectTypeOf } from "vitest";
|
|
894
|
+
|
|
895
|
+
test("un pedido cancelado no puede tener trackingId", () => {
|
|
896
|
+
expectTypeOf<Extract<Order, { status: "cancelled" }>>()
|
|
897
|
+
.not.toHaveProperty("trackingId");
|
|
898
|
+
});
|
|
899
|
+
```
|
|
900
|
+
|
|
711
901
|
### `skapxd/result-error-requires-cause`
|
|
712
902
|
|
|
713
903
|
Evita perder el error original al transformar un `Result` fallido:
|
|
@@ -885,6 +1075,77 @@ Sirve para código de pegamento, pero deja el error sin modelar (`Result<T,
|
|
|
885
1075
|
unknown>`). Cuando la misma operación se repite o el error importa, el mensaje
|
|
886
1076
|
de la regla empuja hacia el camino 1.
|
|
887
1077
|
|
|
1078
|
+
### `skapxd/nest-no-swagger-in-controllers`
|
|
1079
|
+
|
|
1080
|
+
La contracara de la anterior: con el plugin de `@nestjs/swagger` activo en
|
|
1081
|
+
`nest-cli.json`, los decoradores de documentación en el controller son ruido
|
|
1082
|
+
redundante — el plugin ya introspecciona los DTOs de input y el tipo de
|
|
1083
|
+
retorno. Un controller lleno de `@ApiOperation`/`@ApiResponse`/`@ApiParam`
|
|
1084
|
+
entierra la lógica de la frontera bajo metadatos que viven mejor en el DTO:
|
|
1085
|
+
|
|
1086
|
+
```ts
|
|
1087
|
+
@Controller("users")
|
|
1088
|
+
export class UsersController {
|
|
1089
|
+
@ApiOperation({ summary: "Busca un usuario" }) // ❌ redundante con el plugin
|
|
1090
|
+
@ApiResponse({ status: 200, type: UserDto }) // ❌ el tipo de retorno ya lo dice
|
|
1091
|
+
@ApiParam({ name: "id" }) // ❌ el DTO de params ya lo dice
|
|
1092
|
+
@Get(":id")
|
|
1093
|
+
findOne(@Param() params: FindUserParamsDto): Promise<UserDto> { ... }
|
|
1094
|
+
}
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
Solo se permiten los decoradores que el plugin **no puede inferir**
|
|
1098
|
+
(`allowedDecoratorNames`, configurable): `ApiExcludeEndpoint` (ocultar rutas
|
|
1099
|
+
internas), `ApiTags` (agrupación), `ApiBearerAuth` (auth), y
|
|
1100
|
+
`ApiConsumes`/`ApiBody` (uploads multipart, que la introspección no ve).
|
|
1101
|
+
|
|
1102
|
+
La detección compara contra los **imports reales de `@nestjs/swagger`** del
|
|
1103
|
+
archivo: un decorador propio que se llame `ApiOperation` no se toca. Solo
|
|
1104
|
+
aplica dentro de clases `@Controller`.
|
|
1105
|
+
|
|
1106
|
+
### `skapxd/nest-requires-swagger-plugin`
|
|
1107
|
+
|
|
1108
|
+
Las reglas de swagger del preset (`nest-no-swagger-in-controllers`,
|
|
1109
|
+
`nest-dto-requires-api-property`) descansan sobre una premisa: el plugin
|
|
1110
|
+
`@nestjs/swagger` activo en `nest-cli.json`, que introspecciona DTOs y tipos
|
|
1111
|
+
de retorno. Esta regla **verifica la premisa en vez de asumirla**: anclada al
|
|
1112
|
+
entrypoint (`mainFilePatterns`, default `src/main.ts`, un reporte por
|
|
1113
|
+
proyecto), sube por las carpetas hasta el `nest-cli.json` real y exige:
|
|
1114
|
+
|
|
1115
|
+
```jsonc
|
|
1116
|
+
// nest-cli.json
|
|
1117
|
+
{
|
|
1118
|
+
"compilerOptions": {
|
|
1119
|
+
"plugins": ["@nestjs/swagger"] // ✅ (también acepta { "name": "..." })
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
Sin el plugin, el swagger queda vacío — y como el preset prohíbe documentarlo
|
|
1125
|
+
a mano en los controllers, el error te lo dice en el primer lint, no en el
|
|
1126
|
+
primer deploy.
|
|
1127
|
+
|
|
1128
|
+
### `skapxd/nest-validation-pipe-config`
|
|
1129
|
+
|
|
1130
|
+
La otra premisa verificada: todo `new ValidationPipe(...)` (el real, importado
|
|
1131
|
+
de `@nestjs/common`) debe configurar las dos opciones que hacen reales los
|
|
1132
|
+
contratos de los DTOs:
|
|
1133
|
+
|
|
1134
|
+
```ts
|
|
1135
|
+
app.useGlobalPipes(
|
|
1136
|
+
new ValidationPipe({
|
|
1137
|
+
transform: true, // sin él, class-transformer no corre: los @Type no hacen NADA
|
|
1138
|
+
whitelist: true, // sin él, las props sin decorador pasan crudas al dominio
|
|
1139
|
+
// ...el resto (exceptionFactory, transformOptions) es tuyo
|
|
1140
|
+
}),
|
|
1141
|
+
);
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
`new ValidationPipe()` sin opciones, con una faltante o con `transform: false`
|
|
1145
|
+
se reporta. Si las opciones llegan como variable, se resuelve por scope; un
|
|
1146
|
+
identifier irresoluble o un spread reciben el beneficio de la duda.
|
|
1147
|
+
`requiredPipeOptions` es configurable (p. ej. añadir `forbidNonWhitelisted`).
|
|
1148
|
+
|
|
888
1149
|
### `skapxd/no-ad-hoc-ok-result`
|
|
889
1150
|
|
|
890
1151
|
Prohíbe que una función async **exportada** retorne objetos literales con la
|
|
@@ -945,6 +1206,287 @@ de componentes detectan "componente" por nombre PascalCase, así que una
|
|
|
945
1206
|
función `renderX` que devuelve JSX escaparía de ellas. Esta la captura y
|
|
946
1207
|
fuerza el rename — y con el nombre corregido, las demás ya la ven.
|
|
947
1208
|
|
|
1209
|
+
### `skapxd/no-accessors`
|
|
1210
|
+
|
|
1211
|
+
Prohíbe `get`/`set` en clases y objetos literales. Un accessor es un método
|
|
1212
|
+
con sintaxis de propiedad: esconde computación tras un acceso que parece
|
|
1213
|
+
inocente (`config.token` que en realidad ejecuta código), y abre la puerta al
|
|
1214
|
+
**método disfrazado** — un `get sendMessage() { return (...) => ... }` que
|
|
1215
|
+
escapaba de `max-public-methods`:
|
|
1216
|
+
|
|
1217
|
+
```ts
|
|
1218
|
+
class Connection {
|
|
1219
|
+
get socket() { return this.current; } // ❌ computación disfrazada de propiedad
|
|
1220
|
+
socket() { return this.current; } // ✅ el call site dice la verdad: socket()
|
|
1221
|
+
}
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
Si algo es un dato, es una propiedad `readonly`; si algo es comportamiento,
|
|
1225
|
+
es un método explícito que cuenta en la superficie pública. No hay tercera
|
|
1226
|
+
categoría.
|
|
1227
|
+
|
|
1228
|
+
### `skapxd/class-properties-require-readonly`
|
|
1229
|
+
|
|
1230
|
+
Toda propiedad de clase (incluidas las parameter properties del constructor)
|
|
1231
|
+
lleva `readonly`. El estado mutable es la raíz de los **estados
|
|
1232
|
+
inconsistentes** — la misma enfermedad del `useState` con `isLoading`,
|
|
1233
|
+
`error` y `value` llenos a la vez que motivó este paquete: si los campos
|
|
1234
|
+
pueden mutar por separado, las combinaciones imposibles se vuelven posibles.
|
|
1235
|
+
El cambio se modela creando instancias nuevas:
|
|
1236
|
+
|
|
1237
|
+
```ts
|
|
1238
|
+
class Loan {
|
|
1239
|
+
constructor(
|
|
1240
|
+
readonly amount: number, // ✅
|
|
1241
|
+
private readonly term: number, // ✅ parameter property también
|
|
1242
|
+
) {}
|
|
1243
|
+
|
|
1244
|
+
withAmount(amount: number): Loan {
|
|
1245
|
+
return new Loan(amount, this.term); // el "cambio": una instancia nueva
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
class Cache {
|
|
1250
|
+
private entries: string[] = []; // ❌ private no exime: mutable es mutable
|
|
1251
|
+
}
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
La mutación inherente (la conexión de un socket que se reemplaza al
|
|
1255
|
+
reconectar) **se declara visible** en `allowPropertyPatterns: ["^currentSocket$"]`
|
|
1256
|
+
— una decisión en la config, greppeable, no un default silencioso.
|
|
1257
|
+
|
|
1258
|
+
**Compatibilidad con NestJS, investigada y verificada:**
|
|
1259
|
+
|
|
1260
|
+
- **DTOs ✅ sin fricción** (verificado empíricamente con class-transformer +
|
|
1261
|
+
class-validator reales): `readonly` es chequeo de compilación que se borra
|
|
1262
|
+
en runtime — `plainToInstance` asigna, `@Type` convierte, los anidados se
|
|
1263
|
+
instancian y la validación corre igual. El issue conocido de
|
|
1264
|
+
class-transformer ([typestack/class-transformer#250](https://github.com/typestack/class-transformer/issues/250))
|
|
1265
|
+
es sobre `private readonly` detrás de *getters* (accessors) — patrón que
|
|
1266
|
+
`no-accessors` ya prohíbe.
|
|
1267
|
+
- **Capa de persistencia ⚠️ exención POR PROPIEDAD, no por archivo**: una
|
|
1268
|
+
propiedad decorada por el ORM (`@Prop` de `@nestjs/mongoose`, `@Column` y
|
|
1269
|
+
compañía de `typeorm` — verificados contra los imports reales,
|
|
1270
|
+
`ormModuleSources` configurable) le pertenece al ORM y a su modelo de
|
|
1271
|
+
mutación (`doc.campo = x; await doc.save()` no compila contra readonly).
|
|
1272
|
+
La precisión importa: una propiedad **sin** `@Prop` dentro de un
|
|
1273
|
+
`*.schema.ts` es estado de clase normal (campos virtuales, caches) y sí
|
|
1274
|
+
exige `readonly` — la exención por nombre de archivo la habría silenciado.
|
|
1275
|
+
- **Cuidado con los TIPOS array readonly** (`tags: readonly string[]`,
|
|
1276
|
+
`ReadonlyArray<T>`): el plugin de `@nestjs/swagger` degrada su inferencia
|
|
1277
|
+
con ellos ([nestjs/swagger#2413](https://github.com/nestjs/swagger/issues/2413)).
|
|
1278
|
+
Esta regla exige el modificador en la *propiedad* (`readonly tags: string[]`),
|
|
1279
|
+
que es inocuo para el plugin — no uses los tipos array readonly en DTOs.
|
|
1280
|
+
|
|
1281
|
+
### `skapxd/max-public-methods`
|
|
1282
|
+
|
|
1283
|
+
El `one-root-function-per-file` del mundo de clases: **una clase, una
|
|
1284
|
+
responsabilidad** — máximo `max` métodos públicos (default `1`). Es la regla
|
|
1285
|
+
que convierte un `loans.service.ts` de 1965 líneas en una carpeta de casos de
|
|
1286
|
+
uso (`find-apc-score.service.ts`, `create-signature.service.ts`, ...).
|
|
1287
|
+
|
|
1288
|
+
Es **agnóstica al framework** y vive en las reglas base: una clase en Nest,
|
|
1289
|
+
Astro, Next o un proyecto Vite responde al mismo contrato. El conocimiento
|
|
1290
|
+
del framework lo inyecta cada preset vía `ignore` — la regla en sí no sabe
|
|
1291
|
+
qué es NestJS.
|
|
1292
|
+
|
|
1293
|
+
```ts
|
|
1294
|
+
// ❌ dos casos de uso conviviendo
|
|
1295
|
+
export class ApcService {
|
|
1296
|
+
async getScore(id: string) { ... }
|
|
1297
|
+
async refreshScore(id: string) { ... }
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// ✅ un caso de uso con su séquito privado
|
|
1301
|
+
export class FindApcScoreService {
|
|
1302
|
+
constructor(private readonly repository: ApcRepository) {}
|
|
1303
|
+
async execute(id: string) { return this.normalize(...); }
|
|
1304
|
+
private normalize(raw: unknown) { ... }
|
|
1305
|
+
}
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
No cuentan: constructor, getters/setters, `private`/`protected`, `#privados`
|
|
1309
|
+
y el prefijo `_`. `ignore` exime nombres por opción — así el **preset `nest`**
|
|
1310
|
+
inyecta sus hooks (`onModuleInit`, `onApplicationBootstrap`, `canActivate`,
|
|
1311
|
+
`intercept`, `transform`, `catch`, `use`, ...): callbacks que el framework
|
|
1312
|
+
llama, no superficie pública. Fuera de Nest esos nombres no significan nada y
|
|
1313
|
+
cuentan como cualquier método.
|
|
1314
|
+
|
|
1315
|
+
El preset `nest` además la **apaga en `*.controller.ts` y `*.gateway.ts`**:
|
|
1316
|
+
ahí la forma la dicta el framework (un método por ruta/evento) y el límite no
|
|
1317
|
+
aporta semántica. El mensaje de error es un playbook de refactor completo
|
|
1318
|
+
(nombres semánticos, extracción de estado compartido, actualización del
|
|
1319
|
+
módulo y los imports) pensado para que un agente lo ejecute solo.
|
|
1320
|
+
|
|
1321
|
+
### `skapxd/nest-dto-requires-api-property`
|
|
1322
|
+
|
|
1323
|
+
El contrato HTTP — query, params, body y respuesta — se documenta en el DTO,
|
|
1324
|
+
no en el controller. Toda propiedad **pública de instancia** de una clase en
|
|
1325
|
+
un `*.dto.ts` debe llevar `@ApiProperty` o `@ApiPropertyOptional`:
|
|
1326
|
+
|
|
1327
|
+
```ts
|
|
1328
|
+
// create-user.dto.ts
|
|
1329
|
+
export class CreateUserDto {
|
|
1330
|
+
@ApiProperty({ description: "Nombre legal completo", example: "Ana Pérez" })
|
|
1331
|
+
name: string; // ✅
|
|
1332
|
+
|
|
1333
|
+
email: string; // ❌ sin documentar
|
|
1334
|
+
|
|
1335
|
+
@IsString()
|
|
1336
|
+
phone: string; // ❌ class-validator no documenta
|
|
1337
|
+
}
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
El plugin de `@nestjs/swagger` infiere el **tipo**, pero la `description` y el
|
|
1341
|
+
`example` son intención tuya — y son lo que convierte el swagger en un
|
|
1342
|
+
contrato legible (y en un buen cliente generado). Las propiedades `private`,
|
|
1343
|
+
`protected`, `#privadas` y `static` no se exigen: swagger no las serializa.
|
|
1344
|
+
`dtoFilePatterns` ajusta la convención de archivos si no usas `*.dto.ts`.
|
|
1345
|
+
|
|
1346
|
+
### `skapxd/nest-dto-requires-validation`
|
|
1347
|
+
|
|
1348
|
+
El tipo de TypeScript desaparece en runtime: un DTO de input sin
|
|
1349
|
+
class-validator es un contrato de mentira — el `ValidationPipe` deja pasar
|
|
1350
|
+
cualquier cosa (o la descarta en silencio con `whitelist`). Tres contratos en
|
|
1351
|
+
una regla:
|
|
1352
|
+
|
|
1353
|
+
```ts
|
|
1354
|
+
export class CreateLoanDto {
|
|
1355
|
+
@ApiProperty()
|
|
1356
|
+
@IsNumber() // 1. ✅ toda propiedad valida en runtime
|
|
1357
|
+
@IsNotEmpty()
|
|
1358
|
+
amount: number;
|
|
1359
|
+
|
|
1360
|
+
@ApiPropertyOptional()
|
|
1361
|
+
@IsOptional() // 2. ✅ el `?` del tipo y el runtime coinciden
|
|
1362
|
+
@IsNumber()
|
|
1363
|
+
termMonths?: number;
|
|
1364
|
+
|
|
1365
|
+
@ApiProperty()
|
|
1366
|
+
@ValidateNested()
|
|
1367
|
+
@Type(() => AddressDto) // 3. ✅ sin @Type, la validación anidada NO corre
|
|
1368
|
+
address: AddressDto;
|
|
1369
|
+
}
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
1. **Toda propiedad pública** lleva al menos un decorador de class-validator.
|
|
1373
|
+
2. **`?` exige `@IsOptional`** (o `@ValidateIf`): si el tipo dice opcional y el
|
|
1374
|
+
runtime la exige, el contrato miente.
|
|
1375
|
+
3. **`@ValidateNested` exige `@Type(() => Clase)`** de class-transformer: sin
|
|
1376
|
+
él, el objeto anidado llega como plain object y la validación anidada no
|
|
1377
|
+
corre — el bug silencioso clásico (esta regla lo encontró en producción).
|
|
1378
|
+
|
|
1379
|
+
Los **DTOs de respuesta quedan exentos** por dos vías: nombre de archivo
|
|
1380
|
+
(`outputDtoFilePatterns`: `out-*`, `output-*`, `*-response`, `*-result`,
|
|
1381
|
+
`*-output`) y **nombre de clase** (`outputDtoClassPatterns`, regex, default
|
|
1382
|
+
`(Response|Result|Output)(Dto)?$`) — porque un `UploadDocumentResponseDto`
|
|
1383
|
+
puede vivir en un archivo de nombre neutro (`upload-document.dto.ts`) o
|
|
1384
|
+
compartir archivo con DTOs de input, y la exención de la clase no contagia a
|
|
1385
|
+
sus vecinas. El server los produce, no los recibe. La detección compara
|
|
1386
|
+
contra los imports reales de `class-validator`/`class-transformer`, así que
|
|
1387
|
+
un decorador casero homónimo no engaña a la regla.
|
|
1388
|
+
|
|
1389
|
+
**El caso Multer** queda cubierto por el conjunto: el archivo llega como
|
|
1390
|
+
parámetro (`@UploadedFiles() files: Express.Multer.File[]`), nunca en un DTO
|
|
1391
|
+
validable; el schema multipart se documenta inline en el controller con
|
|
1392
|
+
`@ApiConsumes` + `@ApiBody` (permitidos por `nest-no-swagger-in-controllers`:
|
|
1393
|
+
la introspección no ve multipart); y el DTO de respuesta del upload queda
|
|
1394
|
+
exento por nombre de clase. La validación del archivo en sí (tamaño, mimetype)
|
|
1395
|
+
va donde Nest la diseñó: `ParseFilePipe` en el parámetro, no class-validator.
|
|
1396
|
+
|
|
1397
|
+
### `skapxd/nest-no-direct-instantiation`
|
|
1398
|
+
|
|
1399
|
+
En un service, `new FooService()` sobre un import **interno del proyecto**
|
|
1400
|
+
esquiva el contenedor de DI: NestJS no resuelve sus dependencias, no
|
|
1401
|
+
participa del lifecycle, y la clase deja de ser testeable con mocks. Las
|
|
1402
|
+
dependencias entran por el constructor:
|
|
1403
|
+
|
|
1404
|
+
```ts
|
|
1405
|
+
import { FooService } from "#/modules/foo/foo.service";
|
|
1406
|
+
|
|
1407
|
+
const foo = new FooService(); // ❌ esquiva la DI
|
|
1408
|
+
|
|
1409
|
+
constructor(private readonly fooService: FooService) {} // ✅ NestJS resuelve
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
La robustez viene en capas:
|
|
1413
|
+
|
|
1414
|
+
1. **Los globals del runtime nunca se marcan** (`new Date()`, `new Map()`,
|
|
1415
|
+
`new AbortController()`): la regla parte de los **imports internos**
|
|
1416
|
+
(`internalPatterns`: alias `#/`, `@/` y relativos), y un global no se
|
|
1417
|
+
importa. Las librerías externas (`new Logger(...)`) también libres, y los
|
|
1418
|
+
`import type` no cuentan.
|
|
1419
|
+
2. **Exención por nombre de clase** (`allowedClassPatterns`, default
|
|
1420
|
+
`(Error|Exception|Event)$`): errores, excepciones y eventos de dominio se
|
|
1421
|
+
construyen, no se inyectan — vivan en el archivo que vivan.
|
|
1422
|
+
3. **La capa type-aware** (con `projectService`, que el preset trae): la
|
|
1423
|
+
regla resuelve el símbolo de la clase importada y pregunta por el
|
|
1424
|
+
decorador `@Injectable`. Sin el decorador es una clase de valor (un DTO,
|
|
1425
|
+
un mapper puro) y el `new` es legítimo; con él, pertenece al contenedor y
|
|
1426
|
+
se reporta. Irresoluble → conservador, se reporta. En un proyecto real
|
|
1427
|
+
esta capa eliminó el 100% de los falsos positivos restantes.
|
|
1428
|
+
|
|
1429
|
+
`allowedPatterns` (regex de sources) sigue disponible para convenciones
|
|
1430
|
+
propias. El preset la activa en `*.service.ts`.
|
|
1431
|
+
|
|
1432
|
+
### `skapxd/nest-no-inline-query-params`
|
|
1433
|
+
|
|
1434
|
+
Dos o más `@Query('x')` individuales (o `@ApiQuery` sueltos) en un handler
|
|
1435
|
+
son un DTO disfrazado — sin validación automática, sin tipos de verdad y con
|
|
1436
|
+
el controller enterrado en decoradores:
|
|
1437
|
+
|
|
1438
|
+
```ts
|
|
1439
|
+
// ❌ cada query a mano
|
|
1440
|
+
findAll(@Query("status") status?: string, @Query("clientName") name?: string) {}
|
|
1441
|
+
|
|
1442
|
+
// ✅ el DTO consolidado: ValidationPipe valida, swagger documenta, el tipo es real
|
|
1443
|
+
findAll(@Query() filters: ListLoansDto) {}
|
|
1444
|
+
```
|
|
1445
|
+
|
|
1446
|
+
`@Query()` sin argumento (el DTO completo) y un único `@Query('id')` son
|
|
1447
|
+
legítimos (`max` configurable). El mensaje trae el playbook de migración:
|
|
1448
|
+
propiedades `?` + `@IsOptional` + validador + `@ApiPropertyOptional`, y
|
|
1449
|
+
`@Transform`/`@Type` para convertir los strings del query al tipo real.
|
|
1450
|
+
Conecta con `nest-dto-requires-validation`: el DTO que crees ya queda
|
|
1451
|
+
vigilado. Solo el `Query`/`ApiQuery` importados de Nest cuentan.
|
|
1452
|
+
|
|
1453
|
+
### `skapxd/nest-no-result-response`
|
|
1454
|
+
|
|
1455
|
+
El footgun silencioso de mezclar Result con Nest: si un método de un
|
|
1456
|
+
`@Controller` retorna el `Result` crudo, Nest lo serializa tal cual y el
|
|
1457
|
+
cliente recibe `{ ok: false, error: {...} }` con tus internals — tipos de
|
|
1458
|
+
error de dominio, causas, stack traces. Esta regla lo hace imposible:
|
|
1459
|
+
|
|
1460
|
+
```ts
|
|
1461
|
+
@Controller("users")
|
|
1462
|
+
export class UsersController {
|
|
1463
|
+
// ❌ el envelope completo viaja al cliente
|
|
1464
|
+
@Get(":id")
|
|
1465
|
+
async findOne(@Param("id") id: string): Promise<Result<User, UserError>> {
|
|
1466
|
+
return this.usersService.findOne(id);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// ✅ el controller es la frontera: match() traduce
|
|
1470
|
+
@Get(":id")
|
|
1471
|
+
async findOne(@Param("id") id: string): Promise<UserDto> {
|
|
1472
|
+
const user = await this.usersService.findOne(id);
|
|
1473
|
+
|
|
1474
|
+
return match(user)
|
|
1475
|
+
.with({ ok: true }, ({ value }) => toUserDto(value))
|
|
1476
|
+
.with({ ok: false, error: { type: "NOT_FOUND" } }, () => {
|
|
1477
|
+
throw new NotFoundException();
|
|
1478
|
+
})
|
|
1479
|
+
.exhaustive();
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
```
|
|
1483
|
+
|
|
1484
|
+
Es **type-aware**: resuelve el tipo de retorno real del método (anotado o
|
|
1485
|
+
inferido) hasta el `Result` de `@skapxd/result`, así que devolver el Result
|
|
1486
|
+
por indirección tampoco escapa. Solo aplica a clases con `@Controller`
|
|
1487
|
+
(configurable con `controllerDecoratorNames` para decoradores propios); los
|
|
1488
|
+
services retornan Result con orgullo — ese es el dominio.
|
|
1489
|
+
|
|
948
1490
|
### `skapxd/no-deep-relative-imports`
|
|
949
1491
|
|
|
950
1492
|
Limita cuántos niveles puede subir un import relativo. Por defecto **prohíbe
|
|
@@ -1041,6 +1583,37 @@ al default export, basta mapear el named en el import dinámico:
|
|
|
1041
1583
|
const Card = lazy(() => import("./card").then((m) => ({ default: m.Card })));
|
|
1042
1584
|
```
|
|
1043
1585
|
|
|
1586
|
+
### `skapxd/no-else`
|
|
1587
|
+
|
|
1588
|
+
El `if` maneja una condición *nombrada*; el `else` maneja "todo lo demás" —
|
|
1589
|
+
un complemento anónimo cuyo significado el lector deduce negando la
|
|
1590
|
+
condición. Es el último rincón donde un camino vive sin etiqueta, y donde la
|
|
1591
|
+
no-exhaustividad se esconde: una cadena `if/else if/else` sobre flags maneja
|
|
1592
|
+
2 de 4 combinaciones y deja el resto cayendo en un cajón que nadie auditó.
|
|
1593
|
+
|
|
1594
|
+
```ts
|
|
1595
|
+
// ❌ ¿qué ES el else? el lector lo deduce; el compilador no audita nada
|
|
1596
|
+
if (s === "a") { runA(); } else if (s === "b") { runB(); } else { runC(); }
|
|
1597
|
+
|
|
1598
|
+
// ✅ guards: cada salida declara su condición y termina
|
|
1599
|
+
if (!user) return Result.err({ ... });
|
|
1600
|
+
return Result.ok(buildProfile(user));
|
|
1601
|
+
|
|
1602
|
+
// ✅ match: cada variante nombrada y exhaustividad verificada
|
|
1603
|
+
match(state)
|
|
1604
|
+
.with({ status: "a" }, runA)
|
|
1605
|
+
.with({ status: "b" }, runB)
|
|
1606
|
+
.exhaustive();
|
|
1607
|
+
```
|
|
1608
|
+
|
|
1609
|
+
Las salidas: **retorno anticipado** para flujo, **ternario simple** para
|
|
1610
|
+
decisiones de valor (los anidados ya los prohíbe `prefer-ts-pattern`), y
|
|
1611
|
+
**`match().exhaustive()`** para variantes. La única fricción real — dos
|
|
1612
|
+
ramas de efectos en medio de una función — se resuelve extrayendo la función
|
|
1613
|
+
que `one-root-function-per-file` ya pedía. Complementa a `no-nested-if`
|
|
1614
|
+
(profundidad) y a `prefer-tagged-union-state` (este ataca la *declaración*
|
|
1615
|
+
del estado sin nombre; `no-else` ataca su *consumo*).
|
|
1616
|
+
|
|
1044
1617
|
### `skapxd/no-emoji`
|
|
1045
1618
|
|
|
1046
1619
|
Prohíbe emojis en strings, template literals y texto JSX. El problema no es
|
|
@@ -1067,6 +1640,40 @@ eximir archivos completos (fixtures, seeds), usa `allowFilePatterns`:
|
|
|
1067
1640
|
}]
|
|
1068
1641
|
```
|
|
1069
1642
|
|
|
1643
|
+
### `skapxd/no-runtime-state-guard`
|
|
1644
|
+
|
|
1645
|
+
El compañero de `prefer-tagged-union-state` para el comportamiento: cuando un
|
|
1646
|
+
método protege su estado con una comprobación en runtime, la máquina de
|
|
1647
|
+
estados vive en `if` + `throw` — requiere tests para cada ruta inválida y el
|
|
1648
|
+
compilador no puede ayudar (*make invalid states unrepresentable*):
|
|
1649
|
+
|
|
1650
|
+
```ts
|
|
1651
|
+
// ❌ el guard en runtime: probable con tests, invisible para el compilador
|
|
1652
|
+
class Socket {
|
|
1653
|
+
private isConnected = false;
|
|
1654
|
+
emit(event: string) {
|
|
1655
|
+
if (!this.isConnected) throw new Error("Cannot emit: not connected");
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// ✅ cada estado es un tipo: emit NO EXISTE en el socket desconectado
|
|
1660
|
+
class DisconnectedSocket {
|
|
1661
|
+
connect(): ConnectedSocket { ... } // la transición retorna el estado nuevo
|
|
1662
|
+
}
|
|
1663
|
+
class ConnectedSocket {
|
|
1664
|
+
emit(event: string): void { ... } // sin guard: el compilador lo garantiza
|
|
1665
|
+
disconnect(): DisconnectedSocket { ... }
|
|
1666
|
+
}
|
|
1667
|
+
```
|
|
1668
|
+
|
|
1669
|
+
(La variante funcional: la unión discriminada de `prefer-tagged-union-state`,
|
|
1670
|
+
consumida con `match()`.) Solo aplica al **estado propio** (`this.<prop>`) en
|
|
1671
|
+
métodos de clase — validar argumentos o inputs externos es otro territorio
|
|
1672
|
+
(DTOs, `Result`). Un `if` sobre `this` que retorna temprano sin lanzar
|
|
1673
|
+
tampoco se toca. Nota la sinergia con `class-properties-require-readonly`:
|
|
1674
|
+
el flag mutable que este guard necesita ya era ilegal — las dos reglas
|
|
1675
|
+
empujan juntas hacia las transiciones que retornan instancias nuevas.
|
|
1676
|
+
|
|
1070
1677
|
### `skapxd/no-tunnel-props`
|
|
1071
1678
|
|
|
1072
1679
|
**Ninguna prop viaja más de un nivel.** El contrato de saltos: quien **crea**
|
|
@@ -1221,6 +1828,80 @@ métodos se prohíben (por defecto los tres):
|
|
|
1221
1828
|
"skapxd/no-promise-chain": ["error", { methods: ["catch"] }]
|
|
1222
1829
|
```
|
|
1223
1830
|
|
|
1831
|
+
### `skapxd/prefer-tagged-union-state`
|
|
1832
|
+
|
|
1833
|
+
La regla temática del paquete: el estado inconsistente que motivó todo esto,
|
|
1834
|
+
ahora prohibido en su origen. Detecta las dos formas de la enfermedad:
|
|
1835
|
+
|
|
1836
|
+
**Forma A — el tipo enfermo**: un flag boolean de "en proceso" conviviendo
|
|
1837
|
+
con un campo de error como propiedades independientes. Las combinaciones
|
|
1838
|
+
imposibles (cargando Y con error, error Y con valor) son *representables*:
|
|
1839
|
+
|
|
1840
|
+
```ts
|
|
1841
|
+
// ❌ 2³ combinaciones; solo 3 tienen sentido
|
|
1842
|
+
type RequestState = { isLoading: boolean; error?: Error; value?: Data };
|
|
1843
|
+
|
|
1844
|
+
// ✅ los estados imposibles no se pueden NI ESCRIBIR
|
|
1845
|
+
type RequestState =
|
|
1846
|
+
| { status: "idle" }
|
|
1847
|
+
| { status: "loading" }
|
|
1848
|
+
| { status: "error"; error: Error }
|
|
1849
|
+
| { status: "ok"; value: Data };
|
|
1850
|
+
```
|
|
1851
|
+
|
|
1852
|
+
La forma A aplica **igual en el back**: la clase de un job con
|
|
1853
|
+
`private isProcessing = false; private lastError?: Error` es la versión OOP
|
|
1854
|
+
de la máquina repartida, y un schema de Mongoose con `@Prop() isSyncing` +
|
|
1855
|
+
`@Prop() syncError` es la versión más grave — **la inconsistencia se
|
|
1856
|
+
persiste en la base de datos**. La regla revisa tipos, interfaces y cuerpos
|
|
1857
|
+
de clase por igual, con verbos de ambos mundos (`loading`, `submitting`,
|
|
1858
|
+
`deploying`, `migrating`, `retrying`, ...).
|
|
1859
|
+
|
|
1860
|
+
**Forma B — la máquina repartida** (front): varios `useState` que en realidad
|
|
1861
|
+
son una sola máquina de estados. Cada transición toca varios setters y los
|
|
1862
|
+
renders intermedios ven combinaciones imposibles:
|
|
1863
|
+
|
|
1864
|
+
```ts
|
|
1865
|
+
// ❌ tres setters para una transición: el render del medio ve mentiras
|
|
1866
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1867
|
+
const [error, setError] = useState<Error | null>(null);
|
|
1868
|
+
const [user, setUser] = useState<User | null>(null);
|
|
1869
|
+
|
|
1870
|
+
// ✅ UN estado, transición atómica, match() exhaustivo
|
|
1871
|
+
const [state, setState] = useState<RequestState>({ status: "idle" });
|
|
1872
|
+
```
|
|
1873
|
+
|
|
1874
|
+
**Forma C — la transición repartida (evidencia ESTRUCTURAL, sin depender de
|
|
1875
|
+
nombres)**: los setters de `useState` se identifican por *posición en el
|
|
1876
|
+
destructuring* (`const [x, setX] = useState()` — el segundo elemento, se
|
|
1877
|
+
llame como se llame). Si una misma función llama a dos setters distintos,
|
|
1878
|
+
eso **prueba** que esos estados son una sola máquina — entre setter y setter,
|
|
1879
|
+
los renders intermedios ven mentiras:
|
|
1880
|
+
|
|
1881
|
+
```ts
|
|
1882
|
+
const cargar = (respuesta, fallo) => {
|
|
1883
|
+
setDatos(respuesta); // ❌ dos setters en una transición: una máquina
|
|
1884
|
+
setError(fallo); // repartida, aunque `datos` no se llame "loading"
|
|
1885
|
+
};
|
|
1886
|
+
```
|
|
1887
|
+
|
|
1888
|
+
Este detector caza lo que los nombres no ven (estados con nombres exóticos o
|
|
1889
|
+
en español ya cubiertos: `cargando`, `procesando`, `fallo`, ...). El filtro
|
|
1890
|
+
de precisión: al menos uno de los estados co-actualizados debe ser
|
|
1891
|
+
loading/error-ish — resetear dos campos independientes de un formulario no
|
|
1892
|
+
es una máquina.
|
|
1893
|
+
|
|
1894
|
+
Sobre la detección por nombres (formas A y B): es deliberadamente el
|
|
1895
|
+
escalón más bajo de evidencia del paquete — para un tipo *declarado* no hay
|
|
1896
|
+
comportamiento que observar y el nombre es la única señal disponible. El
|
|
1897
|
+
**tipo del campo de error no importa** (`Error`, `string`, código numérico,
|
|
1898
|
+
otro boolean — `isSyncing` + `hasError` es la peor forma): la enfermedad es
|
|
1899
|
+
la coexistencia. Los **callbacks** quedan excluidos (`onError?: (e) => void`,
|
|
1900
|
+
miembros de tipo función): un handler no es estado.
|
|
1901
|
+
`loadingPatterns`/`errorPatterns` ajustan las convenciones. Cierra el círculo con el resto del paquete: la unión etiquetada
|
|
1902
|
+
es a los estados lo que `Result` es a los errores, y `prefer-ts-pattern` te
|
|
1903
|
+
espera con el `match().exhaustive()` al otro lado.
|
|
1904
|
+
|
|
1224
1905
|
### `skapxd/prefer-ts-pattern`
|
|
1225
1906
|
|
|
1226
1907
|
Prohíbe `switch/case` y ternarios anidados, empujando hacia `match()` de
|