@skapxd/eslint-opinionated 0.1.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 +731 -0
- package/dist/astro/index.d.mts +33 -0
- package/dist/astro/index.d.ts +33 -0
- package/dist/astro/index.js +72 -0
- package/dist/astro/index.js.map +1 -0
- package/dist/astro/index.mjs +8 -0
- package/dist/astro/index.mjs.map +1 -0
- package/dist/chunk-3FB4H7N6.mjs +31 -0
- package/dist/chunk-3FB4H7N6.mjs.map +1 -0
- package/dist/chunk-BAHAXSWA.mjs +62 -0
- package/dist/chunk-BAHAXSWA.mjs.map +1 -0
- package/dist/chunk-CQKEQ32W.mjs +99 -0
- package/dist/chunk-CQKEQ32W.mjs.map +1 -0
- package/dist/chunk-RP7BOODV.mjs +1550 -0
- package/dist/chunk-RP7BOODV.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3451 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +3428 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +1781 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +44 -0
- package/dist/index.mjs.map +1 -0
- package/dist/next/index.d.mts +65 -0
- package/dist/next/index.d.ts +65 -0
- package/dist/next/index.js +103 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/index.mjs +8 -0
- package/dist/next/index.mjs.map +1 -0
- package/dist/rules-qISQhAKV.d.mts +5 -0
- package/dist/rules-qISQhAKV.d.ts +5 -0
- package/dist/shared/index.d.mts +110 -0
- package/dist/shared/index.d.ts +110 -0
- package/dist/shared/index.js +1684 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/index.mjs +15 -0
- package/dist/shared/index.mjs.map +1 -0
- package/package.json +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
# @skapxd/eslint-opinionated
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
**Reglas de ESLint para que los agentes no negocien la arquitectura de tu
|
|
6
|
+
proyecto.**
|
|
7
|
+
|
|
8
|
+
A diferencia de un prompt o una nota en el README (que el agente puede priorizar,
|
|
9
|
+
reinterpretar o ignorar), `@skapxd/eslint-opinionated` convierte tus opiniones de
|
|
10
|
+
arquitectura en guardrails que se **ejecutan** y **fallan** cuando el código no
|
|
11
|
+
respeta la forma del proyecto — lo haya escrito una persona, Claude, Codex,
|
|
12
|
+
Cursor o Copilot.
|
|
13
|
+
|
|
14
|
+
- **Una función por archivo:** un archivo con cinco helpers escondidos no pasa;
|
|
15
|
+
la regla hasta te dibuja la carpeta sugerida con formato `tree`.
|
|
16
|
+
- **Errores con `Result`:** una función `async` que puede fallar lo dice en su
|
|
17
|
+
tipo de retorno (`Promise<Result<...>>`), no en una convención oral.
|
|
18
|
+
- **Causa preservada:** al transformar un error de dominio, el `cause` original
|
|
19
|
+
no puede desaparecer — type-aware, vía el checker de TypeScript.
|
|
20
|
+
- **Hooks acotados:** un hook con demasiado estado deja de pasar como "solo un
|
|
21
|
+
hook largo" y empuja hacia `useReducer` o módulos más pequeños.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm eslint
|
|
25
|
+
pnpm eslint src/server/payment-gateway.ts
|
|
26
|
+
pnpm eslint --max-warnings=0
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
La regla no depende de la intención del autor. Se ejecuta y decide.
|
|
30
|
+
|
|
31
|
+
## 🤔 ¿Por qué existe este paquete?
|
|
32
|
+
|
|
33
|
+
Necesitaba una forma **verificable** de decirle a un agente cómo quiero que
|
|
34
|
+
escriba código en mis proyectos.
|
|
35
|
+
|
|
36
|
+
Un proyecto *es* su arquitectura. No solo lo que hace, sino su forma: archivos
|
|
37
|
+
pequeños, nombres que revelan intención, errores modelados, una causa que
|
|
38
|
+
sobrevive cuando algo falla. Si esa forma se erosiona, lo que queda es código que
|
|
39
|
+
compila, pasa los tests y aun así ya nadie puede navegar, depurar ni seguir
|
|
40
|
+
modificando — ni una persona, ni el siguiente agente.
|
|
41
|
+
|
|
42
|
+
Y la experiencia se repetía siempre igual: la regla quedaba clarísima en la
|
|
43
|
+
conversación, pero no en el resultado final. El agente entendía la intención
|
|
44
|
+
general y en el detalle dejaba pequeñas desviaciones:
|
|
45
|
+
|
|
46
|
+
- un helper que se quedaba en el mismo archivo "porque era pequeño";
|
|
47
|
+
- una función `async` que retornaba `Promise<number>` aunque podía fallar;
|
|
48
|
+
- un hook que seguía creciendo porque "todavía funcionaba";
|
|
49
|
+
- un error técnico capturado por `trySafe` que se perdía al mapearlo a un error
|
|
50
|
+
de negocio.
|
|
51
|
+
|
|
52
|
+
Nada de eso rompe la app hoy. **Ese es exactamente el problema.** Son daños
|
|
53
|
+
pequeños de arquitectura: pasan desapercibidos, se acumulan y después hacen que
|
|
54
|
+
el proyecto sea más difícil de navegar, depurar y seguir modificando con
|
|
55
|
+
agentes — justo cuando ya no recuerdas por qué cada cosa estaba donde estaba.
|
|
56
|
+
|
|
57
|
+
Un prompt ayuda, pero un prompt no es una barrera. El mismo prompt lo interpreta
|
|
58
|
+
distinto cada agente, cada modelo, e incluso el mismo modelo en momentos
|
|
59
|
+
distintos. Puede darle más peso a una instrucción que a otra, priorizar que el
|
|
60
|
+
test pase y dejar lo arquitectónico "suficientemente bien".
|
|
61
|
+
|
|
62
|
+
Por eso este paquete mueve esa presión fuera del prompt: si la arquitectura
|
|
63
|
+
importa, tiene que ser ejecutable. La idea no es pedirle mejor al agente que
|
|
64
|
+
recuerde tus reglas. La idea es que el proyecto tenga una opinión que se pueda
|
|
65
|
+
verificar después de cada cambio.
|
|
66
|
+
|
|
67
|
+
## Qué intenta proteger
|
|
68
|
+
|
|
69
|
+
El objetivo no es "código bonito". El objetivo es que un proyecto siga siendo
|
|
70
|
+
navegable, depurable y corregible por agentes.
|
|
71
|
+
|
|
72
|
+
Quiero abrir un proyecto y que `tree` cuente una historia útil: archivos
|
|
73
|
+
pequeños, nombres semánticos y carpetas que revelan intención.
|
|
74
|
+
|
|
75
|
+
Quiero que una función de dominio que puede fallar lo diga en su tipo de
|
|
76
|
+
retorno, no en una convención oral.
|
|
77
|
+
|
|
78
|
+
Quiero que si un error se transforma, la causa original siga ahí, porque
|
|
79
|
+
debuggear un mensaje genérico sin `cause` es perder el contexto justo cuando más
|
|
80
|
+
se necesita.
|
|
81
|
+
|
|
82
|
+
Quiero que un hook con demasiados estados deje de pasar silenciosamente como
|
|
83
|
+
"solo un hook largo" y empiece a empujar hacia `useReducer`, hooks más pequeños
|
|
84
|
+
o módulos de transición explícitos.
|
|
85
|
+
|
|
86
|
+
Quiero que un agente pueda generar código, pero que el proyecto le conteste:
|
|
87
|
+
|
|
88
|
+
> "Esto compila, pero no se escribe así aquí."
|
|
89
|
+
|
|
90
|
+
Eso es lo que estas reglas intentan proteger.
|
|
91
|
+
|
|
92
|
+
## Por qué las alternativas no bastan
|
|
93
|
+
|
|
94
|
+
### ESLint core
|
|
95
|
+
|
|
96
|
+
ESLint core puede limitar líneas, complejidad o cantidad de statements. Eso es
|
|
97
|
+
útil, pero demasiado genérico.
|
|
98
|
+
|
|
99
|
+
No entiende una regla como:
|
|
100
|
+
|
|
101
|
+
> "Este archivo tiene 15 funciones en la raíz. Convierte el archivo en una
|
|
102
|
+
> carpeta, deja `index.ts`, mueve cada helper a un archivo semántico y muestra la
|
|
103
|
+
> estructura sugerida con caracteres tipo `tree`."
|
|
104
|
+
|
|
105
|
+
Tampoco sabe que `src/app/api/foo/route.ts` en Next.js no puede convertirse en
|
|
106
|
+
`src/app/api/foo/route/index.ts`.
|
|
107
|
+
|
|
108
|
+
### `typescript-eslint`
|
|
109
|
+
|
|
110
|
+
`typescript-eslint` es excelente para reglas de TypeScript, y este paquete lo
|
|
111
|
+
usa como base. Pero sus reglas no imponen contratos de dominio como:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
export async function reserveAiMinutes(...): Promise<Result<Success, DomainError>>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
ni verifican que al transformar un error no se pierda la causa original:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
return Result.err({
|
|
122
|
+
cause: result.error,
|
|
123
|
+
message: "No pude completar la operación.",
|
|
124
|
+
type: "OPERATION_FAILED",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`@skapxd/eslint-opinionated` usa parser services y el TypeScript checker para aplicar
|
|
130
|
+
esas reglas sobre tipos reales, no solo sobre nombres de imports.
|
|
131
|
+
|
|
132
|
+
### Plugins de React, Next.js y Astro
|
|
133
|
+
|
|
134
|
+
Los plugins de framework protegen invariantes del framework: hooks, rendering,
|
|
135
|
+
rutas, convenciones del compilador, etc.
|
|
136
|
+
|
|
137
|
+
Eso es necesario, pero no responde preguntas de arquitectura del proyecto:
|
|
138
|
+
|
|
139
|
+
- ¿este hook ya es demasiado grande?
|
|
140
|
+
- ¿este archivo debería ser una carpeta?
|
|
141
|
+
- ¿este helper debe quedarse junto al entrypoint de Next?
|
|
142
|
+
- ¿este error de negocio preserva el error técnico que lo causó?
|
|
143
|
+
|
|
144
|
+
Este plugin los complementa. No los reemplaza.
|
|
145
|
+
|
|
146
|
+
### Reglas genéricas de complejidad
|
|
147
|
+
|
|
148
|
+
`max-lines-per-function`, `complexity` y `max-statements` son reglas útiles, pero
|
|
149
|
+
son reglas ciegas al dominio.
|
|
150
|
+
|
|
151
|
+
Un hook con 14 `useState` no solo es "largo": probablemente está modelando
|
|
152
|
+
transiciones de estado que deberían vivir en un reducer o en módulos separados.
|
|
153
|
+
Por eso `skapxd/max-hook-size` mira específicamente hooks y cantidad de estado
|
|
154
|
+
propio.
|
|
155
|
+
|
|
156
|
+
### Codemods, grep y herramientas de búsqueda
|
|
157
|
+
|
|
158
|
+
Un codemod puede mover archivos. `rg` puede encontrar patrones. Pero esas
|
|
159
|
+
herramientas no mantienen la restricción viva en el editor, CI y `lint`.
|
|
160
|
+
|
|
161
|
+
Son útiles para arreglar. No son suficientes para gobernar.
|
|
162
|
+
|
|
163
|
+
### Prompts e instrucciones para agentes
|
|
164
|
+
|
|
165
|
+
Los prompts son necesarios. Sin contexto, un agente no tiene cómo saber qué
|
|
166
|
+
quieres. Pero el prompt es una instrucción, no una garantía.
|
|
167
|
+
|
|
168
|
+
El mismo prompt puede ser interpretado distinto por cada agente, por cada modelo
|
|
169
|
+
o incluso por el mismo modelo en momentos distintos. Además, cuando una tarea
|
|
170
|
+
tiene muchas restricciones, el agente puede resolver lo funcional y fallar en lo
|
|
171
|
+
arquitectónico.
|
|
172
|
+
|
|
173
|
+
Este paquete mueve esa presión fuera del prompt: la regla se ejecuta después y
|
|
174
|
+
puede fallar con un mensaje concreto.
|
|
175
|
+
|
|
176
|
+
### Comparación rápida
|
|
177
|
+
|
|
178
|
+
| Herramienta | Estilo/sintaxis | Type-aware | Framework-aware | Arquitectura de archivos | Result/cause | Guardrail CLI/CI |
|
|
179
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
180
|
+
| ESLint core | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
181
|
+
| `typescript-eslint` | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
182
|
+
| React/Next/Astro plugins | ✅ | Parcial | ✅ | Parcial | ❌ | ✅ |
|
|
183
|
+
| `max-lines` / `complexity` | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
184
|
+
| Codemods / search | ❌ | Parcial | Parcial | ✅ | Parcial | ❌ |
|
|
185
|
+
| Prompt/instrucciones para agentes | ❌ | ❌ | Parcial | Parcial | Parcial | ❌ |
|
|
186
|
+
| **`@skapxd/eslint-opinionated`** | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
187
|
+
|
|
188
|
+
### En resumen
|
|
189
|
+
|
|
190
|
+
`@skapxd/eslint-opinionated` existe para cubrir un hueco que ninguna de las anteriores
|
|
191
|
+
cubre por sí sola: que un proyecto pueda **opinar de forma verificable** sobre
|
|
192
|
+
cómo un agente escribe código — su forma de archivos, sus contratos de error, su
|
|
193
|
+
manera de no perder la causa — y no solo sobre su estilo o su sintaxis.
|
|
194
|
+
|
|
195
|
+
No busca ser un style guide universal. Es una capa de guardrails ejecutables para
|
|
196
|
+
proyectos que prefieren muchos archivos pequeños, nombres semánticos, errores
|
|
197
|
+
modelados con `Result` y estructuras que se entienden desde el árbol del proyecto.
|
|
198
|
+
|
|
199
|
+
Las reglas no viven en el prompt, donde el agente puede ignorarlas. Viven en un
|
|
200
|
+
comando que puede fallar y decir exactamente qué se rompió.
|
|
201
|
+
|
|
202
|
+
## 🚀 Uso rápido
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pnpm add -D @skapxd/eslint-opinionated eslint typescript typescript-eslint
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
210
|
+
|
|
211
|
+
export default [
|
|
212
|
+
skapxd.configs.shared.base,
|
|
213
|
+
];
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Luego ejecútalo como cualquier regla de ESLint:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
pnpm eslint
|
|
220
|
+
pnpm eslint src
|
|
221
|
+
pnpm eslint --max-warnings=0
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Adopción incremental: lintear solo lo que cambió
|
|
225
|
+
|
|
226
|
+
En una base de código existente, activar todas las reglas de golpe genera mucho
|
|
227
|
+
ruido. El paquete incluye el comando **`skapxd-lint-changed`**, que ejecuta
|
|
228
|
+
**todas** las reglas **solo sobre los archivos que tocaste** (detectados con
|
|
229
|
+
git), no sobre todo el repo. Así el código nuevo nace limpio y el legacy se
|
|
230
|
+
arregla cuando lo editas — la "regla del boy scout".
|
|
231
|
+
|
|
232
|
+
No necesita husky ni hooks: basta con un script en tu `package.json`.
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"scripts": {
|
|
237
|
+
"lint:changed": "skapxd-lint-changed",
|
|
238
|
+
"lint:ci": "skapxd-lint-changed --base origin/main"
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- `pnpm lint:changed` → lintea lo que cambiaste en tu árbol de trabajo
|
|
244
|
+
(modificado, en staging y sin trackear) respecto al último commit.
|
|
245
|
+
- `pnpm lint:ci` (con `--base <rama>`) → lintea lo que tu branch cambió desde que
|
|
246
|
+
divergió de esa rama. Ideal para CI / pull requests.
|
|
247
|
+
|
|
248
|
+
Usa tu `eslint.config.*` y tus reglas tal cual; lo único que hace es **acotar el
|
|
249
|
+
conjunto de archivos**. Si no hay cambios, no hace nada y sale con código `0`; si
|
|
250
|
+
hay errores, sale con código `1` (apto para CI). Como acota por **archivo
|
|
251
|
+
completo**, también dispara las reglas estructurales (p. ej.
|
|
252
|
+
`one-root-function-per-file`), que un filtrado por línea se perdería.
|
|
253
|
+
|
|
254
|
+
## Estructura del paquete
|
|
255
|
+
|
|
256
|
+
```text
|
|
257
|
+
src/
|
|
258
|
+
├── shared/
|
|
259
|
+
│ ├── rules.ts
|
|
260
|
+
│ ├── configs.ts
|
|
261
|
+
│ └── index.ts
|
|
262
|
+
├── next/
|
|
263
|
+
│ ├── configs.ts
|
|
264
|
+
│ └── index.ts
|
|
265
|
+
├── astro/
|
|
266
|
+
│ ├── configs.ts
|
|
267
|
+
│ └── index.ts
|
|
268
|
+
└── index.ts
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
| Módulo | Propósito |
|
|
272
|
+
| --- | --- |
|
|
273
|
+
| `@skapxd/eslint-opinionated/shared` | Reglas y presets comunes para backend, frontend y paquetes npm. |
|
|
274
|
+
| `@skapxd/eslint-opinionated/next` | Presets específicos para Next.js. |
|
|
275
|
+
| `@skapxd/eslint-opinionated/astro` | Presets específicos para Astro. |
|
|
276
|
+
| `@skapxd/eslint-opinionated` | Entry point principal con todas las reglas y configs. |
|
|
277
|
+
|
|
278
|
+
## Presets
|
|
279
|
+
|
|
280
|
+
### Shared
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
284
|
+
|
|
285
|
+
export default [
|
|
286
|
+
skapxd.configs.shared.base,
|
|
287
|
+
skapxd.configs.shared.frontend,
|
|
288
|
+
skapxd.configs.shared.backend,
|
|
289
|
+
];
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Backend
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
296
|
+
|
|
297
|
+
export default [
|
|
298
|
+
{
|
|
299
|
+
files: ["src/server/**/*.{ts,tsx}", "src/app/api/**/*.{ts,tsx}"],
|
|
300
|
+
...skapxd.configs.shared.backend,
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Frontend
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
309
|
+
|
|
310
|
+
export default [
|
|
311
|
+
{
|
|
312
|
+
files: ["src/app/**/*.{ts,tsx}", "src/components/**/*.{ts,tsx}"],
|
|
313
|
+
...skapxd.configs.shared.frontend,
|
|
314
|
+
},
|
|
315
|
+
// Capa de servicios: todo await debe ir envuelto en trySafe.
|
|
316
|
+
skapxd.configs.shared.frontendServices,
|
|
317
|
+
];
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Por defecto `frontendServices` aplica a `**/services/**` y `**/api/**`. Si tus
|
|
321
|
+
servicios viven en otra carpeta, sobreescribe `files`:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
export default [
|
|
325
|
+
{
|
|
326
|
+
...skapxd.configs.shared.frontendServices,
|
|
327
|
+
files: ["src/data/**/*.{ts,tsx}"],
|
|
328
|
+
},
|
|
329
|
+
];
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Next.js
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
import nextPlugin from "@next/eslint-plugin-next";
|
|
336
|
+
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
|
337
|
+
import reactPlugin from "eslint-plugin-react";
|
|
338
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
339
|
+
import tseslint from "typescript-eslint";
|
|
340
|
+
|
|
341
|
+
export default [
|
|
342
|
+
...tseslint.configs.recommended,
|
|
343
|
+
{
|
|
344
|
+
plugins: {
|
|
345
|
+
"@next/next": nextPlugin,
|
|
346
|
+
react: reactPlugin,
|
|
347
|
+
"react-hooks": reactHooksPlugin,
|
|
348
|
+
},
|
|
349
|
+
rules: {
|
|
350
|
+
...nextPlugin.configs.recommended.rules,
|
|
351
|
+
...nextPlugin.configs["core-web-vitals"].rules,
|
|
352
|
+
...reactPlugin.configs.recommended.rules,
|
|
353
|
+
...reactPlugin.configs["jsx-runtime"].rules,
|
|
354
|
+
...reactHooksPlugin.configs.recommended.rules,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
...skapxd.configs.next,
|
|
358
|
+
];
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
También puedes importar solo el factory de Next.js:
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
365
|
+
import { createNextConfigs } from "@skapxd/eslint-opinionated/next";
|
|
366
|
+
|
|
367
|
+
export default [
|
|
368
|
+
...createNextConfigs(skapxd),
|
|
369
|
+
];
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Astro
|
|
373
|
+
|
|
374
|
+
```js
|
|
375
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
376
|
+
|
|
377
|
+
export default [
|
|
378
|
+
...skapxd.configs.astro,
|
|
379
|
+
];
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
También puedes importar solo el factory de Astro:
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
386
|
+
import { createAstroConfigs } from "@skapxd/eslint-opinionated/astro";
|
|
387
|
+
|
|
388
|
+
export default [
|
|
389
|
+
...createAstroConfigs(skapxd),
|
|
390
|
+
];
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Paquete npm
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
397
|
+
|
|
398
|
+
export default [
|
|
399
|
+
{
|
|
400
|
+
files: ["src/**/*.{ts,tsx}"],
|
|
401
|
+
...skapxd.configs.shared.package,
|
|
402
|
+
},
|
|
403
|
+
];
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Strict (sin escape via `eslint-disable`)
|
|
407
|
+
|
|
408
|
+
Un prompt o un agente puede saltarse cualquier regla con
|
|
409
|
+
`// eslint-disable-next-line`. El preset `strict` activa `noInlineConfig`, que
|
|
410
|
+
hace que ESLint **ignore todas las directivas inline** en los archivos que cubre:
|
|
411
|
+
ningún `eslint-disable` surte efecto, así que las reglas no se pueden bypassear.
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
415
|
+
|
|
416
|
+
export default [
|
|
417
|
+
...skapxd.configs.next,
|
|
418
|
+
// Aplícalo al final, acotado a los archivos donde quieras blindar las reglas.
|
|
419
|
+
{
|
|
420
|
+
files: ["src/**/*.{ts,tsx}"],
|
|
421
|
+
...skapxd.configs.strict,
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Si necesitas una excepción puntual (p. ej. archivos generados), añade después un
|
|
427
|
+
bloque con `linterOptions: { noInlineConfig: false }` para esos globs.
|
|
428
|
+
|
|
429
|
+
## Reglas
|
|
430
|
+
|
|
431
|
+
| Regla | Qué protege |
|
|
432
|
+
| --- | --- |
|
|
433
|
+
| `skapxd/one-root-function-per-file` | Un archivo, una función top-level semántica. |
|
|
434
|
+
| `skapxd/async-functions-return-result` | Funciones async de dominio deben retornar `Promise<Result<...>>`. |
|
|
435
|
+
| `skapxd/result-error-requires-cause` | Un `Result.err` derivado debe preservar `cause: result.error`. |
|
|
436
|
+
| `skapxd/await-requires-try-safe` | Los `await` deben estar protegidos por `trySafe`. La activa el preset `shared.frontendServices` en la capa de servicios. |
|
|
437
|
+
| `skapxd/no-ad-hoc-ok-result` | Evita contratos `{ ok: ... }` hechos a mano en async exports. |
|
|
438
|
+
| `skapxd/max-hook-size` | Marca hooks grandes o con demasiados `useState`. |
|
|
439
|
+
| `skapxd/jsx-return-name-pascal-case` | Funciones que retornan JSX deben nombrarse como componentes. |
|
|
440
|
+
| `skapxd/no-deep-relative-imports` | Limita la profundidad de los imports relativos (`../`). |
|
|
441
|
+
| `skapxd/no-functions-inside-components` | Prohíbe definir funciones dentro de componentes React. |
|
|
442
|
+
| `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
|
|
443
|
+
| `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
|
|
444
|
+
| `skapxd/prefer-ts-pattern` | Prohíbe `switch` y ternarios anidados; usa `match()` de ts-pattern. |
|
|
445
|
+
| `skapxd/no-jsx-ternary-null` | Prefiere `cond && <El />` sobre `cond ? <El /> : null` en JSX. |
|
|
446
|
+
|
|
447
|
+
### `skapxd/one-root-function-per-file`
|
|
448
|
+
|
|
449
|
+
Limita cada archivo a una sola función declarada en la raíz.
|
|
450
|
+
|
|
451
|
+
Cuando detecta varias funciones, sugiere una estructura con formato tipo
|
|
452
|
+
`tree`. Por ejemplo:
|
|
453
|
+
|
|
454
|
+
```text
|
|
455
|
+
payment-gateway.ts
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
puede convertirse en:
|
|
459
|
+
|
|
460
|
+
```text
|
|
461
|
+
payment-gateway/
|
|
462
|
+
├── index.ts
|
|
463
|
+
└── get-ai-minute-packages.ts
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
En archivos de convención de Next.js (`route.ts`, `page.tsx`, `layout.tsx`,
|
|
467
|
+
etc.) no sugiere estructuras inválidas. Mantiene el entrypoint requerido y
|
|
468
|
+
sugiere helpers al lado.
|
|
469
|
+
|
|
470
|
+
### `skapxd/async-functions-return-result`
|
|
471
|
+
|
|
472
|
+
Obliga a que funciones async en dominios configurados declaren un retorno como:
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
Promise<Result<Success, DomainError>>
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Es **type-aware** y está atada a `@skapxd/result`: usa el TypeScript checker para
|
|
479
|
+
confirmar que el `Result` viene de ese paquete, no solo que el tipo *se llame*
|
|
480
|
+
`Result`. Un `Result` de otro paquete (o un tipo homónimo hecho a mano) **no**
|
|
481
|
+
cumple la regla.
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
import { Result } from "@skapxd/result";
|
|
485
|
+
async function ok(): Promise<Result<number, Error>> {} // ✅
|
|
486
|
+
|
|
487
|
+
type Result<T, E> = ...; // ❌ Result ajeno
|
|
488
|
+
async function no(): Promise<Result<number, Error>> {} // se reporta
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
> Requiere `projectService` (los presets `backend` y `next/server` ya lo activan).
|
|
492
|
+
> Sin información de tipos cae a una comprobación por nombre (`resultTypeNames`),
|
|
493
|
+
> menos estricta.
|
|
494
|
+
|
|
495
|
+
### `skapxd/result-error-requires-cause`
|
|
496
|
+
|
|
497
|
+
Evita perder el error original al transformar un `Result` fallido:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
if (!result.ok) {
|
|
501
|
+
return Result.err({
|
|
502
|
+
cause: result.error,
|
|
503
|
+
message: "No pude completar la operación.",
|
|
504
|
+
type: "OPERATION_FAILED",
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Esta regla es type-aware. Usa TypeScript parser services para confirmar que el
|
|
510
|
+
valor del guard y `Result.err` vienen de `@skapxd/result`. Por eso funciona con
|
|
511
|
+
aliases, re-exports y tipos inferidos, sin depender solo del nombre importado en
|
|
512
|
+
el archivo.
|
|
513
|
+
|
|
514
|
+
### `skapxd/await-requires-try-safe`
|
|
515
|
+
|
|
516
|
+
> Es la regla más agresiva del paquete (marca *todos* los `await` sin proteger),
|
|
517
|
+
> así que solo la activa el preset `shared.frontendServices`, acotada a la capa
|
|
518
|
+
> de servicios (`**/services/**`, `**/api/**`). Para activarla en otros globs,
|
|
519
|
+
> añádela tú mismo:
|
|
520
|
+
>
|
|
521
|
+
> ```js
|
|
522
|
+
> rules: {
|
|
523
|
+
> "skapxd/await-requires-try-safe": ["error", {
|
|
524
|
+
> trySafeCallNames: ["trySafe"],
|
|
525
|
+
> allowFilePatterns: [],
|
|
526
|
+
> }],
|
|
527
|
+
> }
|
|
528
|
+
> ```
|
|
529
|
+
|
|
530
|
+
Obliga a proteger operaciones `await` con `trySafe`:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
const result = await trySafe(() => client.execute({...}));
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
o dentro de un callback:
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
const result = await trySafe(async () => {
|
|
540
|
+
const response = await fetch(url);
|
|
541
|
+
return response.json();
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Si lo que se awaitea ya retorna `Result`/`Promise<Result<...>>` de
|
|
546
|
+
`@skapxd/result`, la regla no exige `trySafe`: los errores ya están modelados en
|
|
547
|
+
el tipo y envolverlo sería redundante.
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
declare function getUser(): Promise<Result<User, Error>>;
|
|
551
|
+
|
|
552
|
+
const result = await getUser(); // ✅ ya es un Result, no necesita trySafe
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Esta exención es type-aware: un `Result` casero (que no venga de
|
|
556
|
+
`@skapxd/result`) no exime.
|
|
557
|
+
|
|
558
|
+
### `skapxd/max-hook-size`
|
|
559
|
+
|
|
560
|
+
Marca hooks que crecen demasiado o acumulan muchos `useState`.
|
|
561
|
+
|
|
562
|
+
La intención es empujar el diseño hacia `useReducer`, hooks más pequeños o
|
|
563
|
+
módulos de transición de estado.
|
|
564
|
+
|
|
565
|
+
### `skapxd/no-deep-relative-imports`
|
|
566
|
+
|
|
567
|
+
Limita cuántos niveles puede subir un import relativo. Por defecto **prohíbe
|
|
568
|
+
cualquier `../`**: un import que sube a una carpeta padre suele ser señal de que
|
|
569
|
+
falta un alias de ruta o de que el módulo está mal ubicado.
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
import { x } from "./sibling"; // ✅ mismo nivel
|
|
573
|
+
import { y } from "../shared/y"; // ❌ sube a una carpeta padre
|
|
574
|
+
import { z } from "#/shared/y"; // ✅ alias de ruta
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Opción `maxDepth` (por defecto `0`) para permitir hasta N niveles de `../`:
|
|
578
|
+
|
|
579
|
+
```js
|
|
580
|
+
rules: {
|
|
581
|
+
// permite ../ (un nivel) pero sigue prohibiendo ../../
|
|
582
|
+
"skapxd/no-deep-relative-imports": ["warn", { maxDepth: 1 }],
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Revisa imports estáticos (`import`), re-exports (`export ... from`) e imports
|
|
587
|
+
dinámicos (`import(...)`). El remedio habitual es un alias de ruta (`@/...`) o
|
|
588
|
+
acercar el módulo a quien lo usa.
|
|
589
|
+
|
|
590
|
+
### `skapxd/no-functions-inside-components`
|
|
591
|
+
|
|
592
|
+
Prohíbe **cualquier** función definida dentro de un componente React (una función
|
|
593
|
+
con nombre PascalCase). Cada render recrea esas funciones, lo que dispara
|
|
594
|
+
re-renders innecesarios en hijos memoizados y mezcla lógica con composición.
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
function Card() {
|
|
598
|
+
const onClick = () => save(); // ❌ se recrea en cada render
|
|
599
|
+
useEffect(() => subscribe(), []); // ❌ callback dentro del componente
|
|
600
|
+
return <ul>{items.map((i) => <Li />)}</ul>; // ❌ callback de .map en el render
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
El cuerpo del componente queda como composición declarativa; **toda** función
|
|
605
|
+
—handlers, efectos, memos, mapeos— vive fuera:
|
|
606
|
+
|
|
607
|
+
```tsx
|
|
608
|
+
const onClick = () => save(); // ✅ helper fuera del componente
|
|
609
|
+
|
|
610
|
+
function useCardItems() { // ✅ lógica en un hook
|
|
611
|
+
return useMemo(() => buildItems(), []);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function Card() {
|
|
615
|
+
const items = useCardItems();
|
|
616
|
+
return <ul>{items}</ul>;
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
"Componente" se detecta por nombre PascalCase, así que un hook (`useX`) o un
|
|
621
|
+
helper en minúscula **sí** pueden tener funciones dentro — ahí es donde se mueve
|
|
622
|
+
la lógica.
|
|
623
|
+
|
|
624
|
+
### `skapxd/no-try-catch`
|
|
625
|
+
|
|
626
|
+
Prohíbe `try/catch`. La intención es que los errores se modelen como `Result` en
|
|
627
|
+
vez de saltar como excepciones invisibles en el tipo.
|
|
628
|
+
|
|
629
|
+
```ts
|
|
630
|
+
const result = await trySafe(() => client.execute(query)); // ✅
|
|
631
|
+
if (!result.ok) return Result.err({ cause: result.error, type: "DB_FAILED" });
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Se complementa con `result-error-requires-cause` (preservar la causa) y, si la
|
|
635
|
+
activas, con `await-requires-try-safe` (que además exige envolver cada `await`).
|
|
636
|
+
|
|
637
|
+
### `skapxd/no-promise-chain`
|
|
638
|
+
|
|
639
|
+
Prohíbe encadenar `.then()`, `.catch()` y `.finally()` sobre promesas. La única
|
|
640
|
+
forma de tratar funciones asíncronas es `await` (envuelto en `trySafe`), para que
|
|
641
|
+
el control de flujo y los errores sean explícitos y secuenciales.
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
fetchData().then(handle).catch(report); // ❌
|
|
645
|
+
const result = await trySafe(() => fetchData()); // ✅
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
Es **type-aware**: solo marca el `.then/.catch/.finally` cuando el receptor es
|
|
649
|
+
una promesa real (un objeto cualquiera con un método `.catch` no se toca). Sin
|
|
650
|
+
`projectService` cae a verificación por nombre. La opción `methods` ajusta qué
|
|
651
|
+
métodos se prohíben (por defecto los tres):
|
|
652
|
+
|
|
653
|
+
```js
|
|
654
|
+
// solo prohibir .catch, permitir .then/.finally
|
|
655
|
+
"skapxd/no-promise-chain": ["error", { methods: ["catch"] }]
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### `skapxd/prefer-ts-pattern`
|
|
659
|
+
|
|
660
|
+
Prohíbe `switch/case` y ternarios anidados, empujando hacia `match()` de
|
|
661
|
+
[`ts-pattern`](https://github.com/gvergnaud/ts-pattern), que da exhaustividad
|
|
662
|
+
verificada por el compilador.
|
|
663
|
+
|
|
664
|
+
```ts
|
|
665
|
+
// ❌ switch // ❌ ternario anidado
|
|
666
|
+
switch (status) { ... } const label = a ? "x" : b ? "y" : "z";
|
|
667
|
+
|
|
668
|
+
// ✅
|
|
669
|
+
const label = match(status)
|
|
670
|
+
.with("active", () => "x")
|
|
671
|
+
.with("paused", () => "y")
|
|
672
|
+
.exhaustive();
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### `skapxd/no-jsx-ternary-null`
|
|
676
|
+
|
|
677
|
+
Cuando renderizas JSX condicional y una rama del ternario es `null`, prefiere la
|
|
678
|
+
forma con `&&`:
|
|
679
|
+
|
|
680
|
+
```tsx
|
|
681
|
+
{isLoggedIn ? <Dashboard /> : null} // ❌
|
|
682
|
+
{isLoggedIn && <Dashboard />} // ✅
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Solo aplica a JSX renderizado (hijos de un elemento/fragmento), no a atributos
|
|
686
|
+
—donde `&&` cambiaría la semántica—. Cuidado con el clásico gotcha de `&&`: un
|
|
687
|
+
valor `0` se renderiza en pantalla; con booleanos no hay problema.
|
|
688
|
+
|
|
689
|
+
## Supuestos y límites conocidos
|
|
690
|
+
|
|
691
|
+
Tres reglas se apoyan en **convenciones de React/JS** para identificar lo que
|
|
692
|
+
miran. No son fallos: son el contrato de la regla. Conviene conocerlos:
|
|
693
|
+
|
|
694
|
+
| Regla | Supuesto | Implicación |
|
|
695
|
+
| --- | --- | --- |
|
|
696
|
+
| `no-functions-inside-components` | "Componente" = función con nombre **PascalCase**. | Un componente en minúscula o anónimo no se detecta; una función PascalCase que *no* sea componente podría marcarse. |
|
|
697
|
+
| `jsx-return-name-pascal-case` | Detecta **JSX literal** en el cuerpo de la función. | Si devuelves JSX por indirección (`return render()`), no se detecta. |
|
|
698
|
+
| `max-hook-size` | "Hook" = nombre que empieza con **`use`**; el tamaño se mide en líneas. | Una función con lógica de hook pero sin prefijo `use` no se mide. |
|
|
699
|
+
|
|
700
|
+
Estos supuestos **se auto-refuerzan** con el resto del plugin: si nombras un
|
|
701
|
+
componente en minúscula, `jsx-return-name-pascal-case` te obliga a pasarlo a
|
|
702
|
+
PascalCase, y entonces `no-functions-inside-components` ya lo reconoce. Por eso no
|
|
703
|
+
perseguimos "robustez" más allá de la convención: las reglas que la imponen
|
|
704
|
+
cierran el hueco.
|
|
705
|
+
|
|
706
|
+
En cambio, las reglas atadas a `@skapxd/result`
|
|
707
|
+
(`async-functions-return-result`, `result-error-requires-cause`,
|
|
708
|
+
`await-requires-try-safe`) **no** dependen de nombres: resuelven el símbolo hasta
|
|
709
|
+
el paquete real (vía el `name` de su `package.json`), así que funcionan con
|
|
710
|
+
alias, re-exports y en monorepos.
|
|
711
|
+
|
|
712
|
+
## Notas sobre reglas type-aware
|
|
713
|
+
|
|
714
|
+
Algunas reglas necesitan información real de TypeScript. Los presets que la
|
|
715
|
+
necesitan configuran:
|
|
716
|
+
|
|
717
|
+
```js
|
|
718
|
+
languageOptions: {
|
|
719
|
+
parserOptions: {
|
|
720
|
+
projectService: true,
|
|
721
|
+
},
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
Esto hace el lint un poco más lento, pero reduce falsos positivos importantes:
|
|
726
|
+
por ejemplo, distinguir un `Result` real de `@skapxd/result` de otro objeto que
|
|
727
|
+
casualmente también tenga propiedades `ok` y `error`.
|
|
728
|
+
|
|
729
|
+
## Licencia
|
|
730
|
+
|
|
731
|
+
MIT
|