@skapxd/eslint-opinionated 0.9.0 → 0.10.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 +49 -0
- package/dist/astro/index.mjs +2 -2
- package/dist/{chunk-QRYL7RJO.mjs → chunk-3WLCAPUA.mjs} +3 -2
- package/dist/{chunk-QRYL7RJO.mjs.map → chunk-3WLCAPUA.mjs.map} +1 -1
- package/dist/{chunk-5P6OHVFD.mjs → chunk-7VZBQ6FD.mjs} +2 -2
- package/dist/{chunk-EGNXI5HL.mjs → chunk-GEVX3BTI.mjs} +4 -1
- package/dist/{chunk-EGNXI5HL.mjs.map → chunk-GEVX3BTI.mjs.map} +1 -1
- package/dist/{chunk-O4RTJGEU.mjs → chunk-WO7EUISC.mjs} +137 -1
- package/dist/chunk-WO7EUISC.mjs.map +1 -0
- package/dist/index.js +141 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +3 -0
- package/dist/next/index.d.ts +3 -0
- package/dist/next/index.js +1 -0
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +2 -2
- package/dist/shared/index.d.mts +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +139 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +2 -2
- package/package.json +1 -1
- package/dist/chunk-O4RTJGEU.mjs.map +0 -1
- /package/dist/{chunk-5P6OHVFD.mjs.map → chunk-7VZBQ6FD.mjs.map} +0 -0
package/README.md
CHANGED
|
@@ -550,6 +550,7 @@ de cada regla):
|
|
|
550
550
|
| `no-functions-inside-components` | `allowJsxCallbacks`, `allowArrayMapCallbacks` (ambas `true` por defecto) |
|
|
551
551
|
| `no-promise-chain` | `methods` |
|
|
552
552
|
| `no-tunnel-props` | `allowFilePatterns` (globs), `allowPropPatterns` (regex) |
|
|
553
|
+
| `prefer-abort-signal` | `allowFilePatterns` (globs), `effectNames` (default `["useEffect", "useLayoutEffect"]`) |
|
|
553
554
|
|
|
554
555
|
Los `allowFilePatterns` de todas las reglas son **globs** (`*` un segmento,
|
|
555
556
|
`**` cualquier profundidad, `{a,b}` alternativas; un patrón sin prefijo
|
|
@@ -571,6 +572,7 @@ matchea en cualquier carpeta). Las 7 reglas restantes no tienen opciones: su
|
|
|
571
572
|
| `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. |
|
|
572
573
|
| `skapxd/no-emoji` | Prohíbe emojis en strings y JSX; cada sistema los renderiza distinto. Usa un icono SVG. |
|
|
573
574
|
| `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. |
|
|
575
|
+
| `skapxd/prefer-abort-signal` | Listeners en efectos se limpian con `AbortController` (`{ signal }` + `abort()`), no con `removeEventListener`. |
|
|
574
576
|
| `skapxd/no-functions-inside-components` | Prohíbe definir funciones dentro de componentes React. |
|
|
575
577
|
| `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
|
|
576
578
|
| `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
|
|
@@ -1085,6 +1087,53 @@ const label = match(status)
|
|
|
1085
1087
|
.exhaustive();
|
|
1086
1088
|
```
|
|
1087
1089
|
|
|
1090
|
+
### `skapxd/prefer-abort-signal`
|
|
1091
|
+
|
|
1092
|
+
Dentro de un `useEffect`/`useLayoutEffect`, los listeners se limpian con
|
|
1093
|
+
`AbortController`, no con `removeEventListener` manual:
|
|
1094
|
+
|
|
1095
|
+
```ts
|
|
1096
|
+
// ❌ registro y limpieza espejados a mano
|
|
1097
|
+
useEffect(() => {
|
|
1098
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
1099
|
+
media.addEventListener("change", onSystemChange);
|
|
1100
|
+
return () => media.removeEventListener("change", onSystemChange);
|
|
1101
|
+
}, [settings]);
|
|
1102
|
+
|
|
1103
|
+
// ✅ un AbortController por efecto
|
|
1104
|
+
useEffect(() => {
|
|
1105
|
+
const controller = new AbortController();
|
|
1106
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
1107
|
+
media.addEventListener("change", onSystemChange, { signal: controller.signal });
|
|
1108
|
+
return () => controller.abort();
|
|
1109
|
+
}, [settings]);
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
Por qué: un solo `abort()` limpia **todos** los listeners del efecto (no hay
|
|
1113
|
+
que espejar cada `add` con su `remove`), y elimina el bug clásico de pasar una
|
|
1114
|
+
referencia distinta a `removeEventListener` (un `.bind()` o una arrow nueva)
|
|
1115
|
+
que deja el listener vivo para siempre.
|
|
1116
|
+
|
|
1117
|
+
Reporta dos cosas dentro del callback del efecto (incluidas sus funciones
|
|
1118
|
+
anidadas y el cleanup): `addEventListener` sin `signal` en las options, y
|
|
1119
|
+
cualquier `removeEventListener`. Fuera de un efecto la regla no opina.
|
|
1120
|
+
|
|
1121
|
+
Cuando las options no son un objeto literal, la verificación resuelve en
|
|
1122
|
+
capas:
|
|
1123
|
+
|
|
1124
|
+
1. **Por scope**: `addEventListener("x", fn, opts)` sigue `opts` hasta su
|
|
1125
|
+
`const opts = {...}` y lo inspecciona — sin necesitar type-checking.
|
|
1126
|
+
2. **Por tipo** (con `projectService`): si no hay inicializador visible (un
|
|
1127
|
+
parámetro, un import), pregunta al checker si el **tipo** declara `signal`;
|
|
1128
|
+
si ni el tipo la tiene, es imposible que llegue y se reporta.
|
|
1129
|
+
3. Sin inicializador ni tipos: beneficio de la duda.
|
|
1130
|
+
|
|
1131
|
+
El boolean de capture (`addEventListener("x", fn, true)`) se reporta siempre:
|
|
1132
|
+
no puede traer `signal`.
|
|
1133
|
+
|
|
1134
|
+
`effectNames` permite cubrir wrappers propios (`["useEffect",
|
|
1135
|
+
"useLayoutEffect", "useIsomorphicEffect"]`).
|
|
1136
|
+
|
|
1088
1137
|
### `skapxd/no-jsx-ternary-null`
|
|
1089
1138
|
|
|
1090
1139
|
Cuando renderizas JSX condicional y una rama del ternario es `null`, prefiere la
|
package/dist/astro/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
baseRules,
|
|
7
7
|
createBaseLanguageOptions,
|
|
8
8
|
createTypedLanguageOptions
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-GEVX3BTI.mjs";
|
|
10
10
|
|
|
11
11
|
// src/next/configs.ts
|
|
12
12
|
var nextDefaultExportFileGlob = `{${[
|
|
@@ -54,6 +54,7 @@ function createNextConfigs(pluginReference) {
|
|
|
54
54
|
"skapxd/jsx-return-name-pascal-case": "error",
|
|
55
55
|
"skapxd/no-functions-inside-components": "error",
|
|
56
56
|
"skapxd/no-tunnel-props": "error",
|
|
57
|
+
"skapxd/prefer-abort-signal": "error",
|
|
57
58
|
"skapxd/no-jsx-ternary-null": "error",
|
|
58
59
|
"skapxd/max-hook-size": [
|
|
59
60
|
"error",
|
|
@@ -70,4 +71,4 @@ function createNextConfigs(pluginReference) {
|
|
|
70
71
|
export {
|
|
71
72
|
createNextConfigs
|
|
72
73
|
};
|
|
73
|
-
//# sourceMappingURL=chunk-
|
|
74
|
+
//# sourceMappingURL=chunk-3WLCAPUA.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/next/configs.ts"],"sourcesContent":["import { nextAppMetadataFileStems } from \"#/constants/next-app-metadata-file-stems\";\nimport { nextAppRouteSegmentFileStems } from \"#/constants/next-app-route-segment-file-stems\";\nimport {\n baseRules,\n createBaseLanguageOptions,\n createTypedLanguageOptions,\n} from \"#/shared/configs\";\n\n// Entrypoints donde Next exige `export default` (page, layout, sitemap, ...):\n// la regla no-default-export los exime automáticamente en este preset.\nconst nextDefaultExportFileGlob = `{${[\n ...nextAppRouteSegmentFileStems,\n ...nextAppMetadataFileStems,\n].join(\",\")}}.{js,jsx,ts,tsx}`;\n\nexport function createNextConfigs(pluginReference: unknown) {\n const baseLanguageOptions = createBaseLanguageOptions();\n const typedLanguageOptions = createTypedLanguageOptions();\n\n return [\n {\n languageOptions: baseLanguageOptions,\n name: \"skapxd/next/base\",\n plugins: { skapxd: pluginReference },\n rules: {\n ...baseRules,\n \"skapxd/no-default-export\": [\n \"error\",\n {\n allowFilePatterns: [nextDefaultExportFileGlob],\n },\n ],\n },\n },\n {\n files: [\"src/app/api/**/*.{ts,tsx}\", \"src/server/**/*.{ts,tsx}\"],\n languageOptions: typedLanguageOptions,\n name: \"skapxd/next/server\",\n plugins: { skapxd: pluginReference },\n rules: {\n ...baseRules,\n // Obligatoria: todo await resuelve en Result. A diferencia de\n // async-functions-return-result (apagada por defecto), no necesita\n // excepciones para los entrypoints de Next: envolver un await en\n // trySafe es compatible con cualquier firma que imponga el framework.\n \"skapxd/await-requires-result\": \"error\",\n },\n },\n {\n files: [\"**/*.tsx\"],\n languageOptions: baseLanguageOptions,\n name: \"skapxd/next/react\",\n plugins: { skapxd: pluginReference },\n rules: {\n \"skapxd/jsx-return-name-pascal-case\": \"error\",\n \"skapxd/no-functions-inside-components\": \"error\",\n \"skapxd/no-tunnel-props\": \"error\",\n \"skapxd/no-jsx-ternary-null\": \"error\",\n \"skapxd/max-hook-size\": [\n \"error\",\n {\n maxLines: 120,\n maxUseState: 1,\n },\n ],\n },\n },\n ];\n}\n"],"mappings":";;;;;;;;;;;AAUA,IAAM,4BAA4B,IAAI;AAAA,EACpC,GAAG;AAAA,EACH,GAAG;AACL,EAAE,KAAK,GAAG,CAAC;AAEJ,SAAS,kBAAkB,iBAA0B;AAC1D,QAAM,sBAAsB,0BAA0B;AACtD,QAAM,uBAAuB,2BAA2B;AAExD,SAAO;AAAA,IACL;AAAA,MACE,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,GAAG;AAAA,QACH,4BAA4B;AAAA,UAC1B;AAAA,UACA;AAAA,YACE,mBAAmB,CAAC,yBAAyB;AAAA,UAC/C;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO,CAAC,6BAA6B,0BAA0B;AAAA,MAC/D,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA,QAKH,gCAAgC;AAAA,MAClC;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO,CAAC,UAAU;AAAA,MAClB,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,sCAAsC;AAAA,QACtC,yCAAyC;AAAA,QACzC,0BAA0B;AAAA,QAC1B,8BAA8B;AAAA,QAC9B,wBAAwB;AAAA,UACtB;AAAA,UACA;AAAA,YACE,UAAU;AAAA,YACV,aAAa;AAAA,UACf;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/next/configs.ts"],"sourcesContent":["import { nextAppMetadataFileStems } from \"#/constants/next-app-metadata-file-stems\";\nimport { nextAppRouteSegmentFileStems } from \"#/constants/next-app-route-segment-file-stems\";\nimport {\n baseRules,\n createBaseLanguageOptions,\n createTypedLanguageOptions,\n} from \"#/shared/configs\";\n\n// Entrypoints donde Next exige `export default` (page, layout, sitemap, ...):\n// la regla no-default-export los exime automáticamente en este preset.\nconst nextDefaultExportFileGlob = `{${[\n ...nextAppRouteSegmentFileStems,\n ...nextAppMetadataFileStems,\n].join(\",\")}}.{js,jsx,ts,tsx}`;\n\nexport function createNextConfigs(pluginReference: unknown) {\n const baseLanguageOptions = createBaseLanguageOptions();\n const typedLanguageOptions = createTypedLanguageOptions();\n\n return [\n {\n languageOptions: baseLanguageOptions,\n name: \"skapxd/next/base\",\n plugins: { skapxd: pluginReference },\n rules: {\n ...baseRules,\n \"skapxd/no-default-export\": [\n \"error\",\n {\n allowFilePatterns: [nextDefaultExportFileGlob],\n },\n ],\n },\n },\n {\n files: [\"src/app/api/**/*.{ts,tsx}\", \"src/server/**/*.{ts,tsx}\"],\n languageOptions: typedLanguageOptions,\n name: \"skapxd/next/server\",\n plugins: { skapxd: pluginReference },\n rules: {\n ...baseRules,\n // Obligatoria: todo await resuelve en Result. A diferencia de\n // async-functions-return-result (apagada por defecto), no necesita\n // excepciones para los entrypoints de Next: envolver un await en\n // trySafe es compatible con cualquier firma que imponga el framework.\n \"skapxd/await-requires-result\": \"error\",\n },\n },\n {\n files: [\"**/*.tsx\"],\n languageOptions: baseLanguageOptions,\n name: \"skapxd/next/react\",\n plugins: { skapxd: pluginReference },\n rules: {\n \"skapxd/jsx-return-name-pascal-case\": \"error\",\n \"skapxd/no-functions-inside-components\": \"error\",\n \"skapxd/no-tunnel-props\": \"error\",\n \"skapxd/prefer-abort-signal\": \"error\",\n \"skapxd/no-jsx-ternary-null\": \"error\",\n \"skapxd/max-hook-size\": [\n \"error\",\n {\n maxLines: 120,\n maxUseState: 1,\n },\n ],\n },\n },\n ];\n}\n"],"mappings":";;;;;;;;;;;AAUA,IAAM,4BAA4B,IAAI;AAAA,EACpC,GAAG;AAAA,EACH,GAAG;AACL,EAAE,KAAK,GAAG,CAAC;AAEJ,SAAS,kBAAkB,iBAA0B;AAC1D,QAAM,sBAAsB,0BAA0B;AACtD,QAAM,uBAAuB,2BAA2B;AAExD,SAAO;AAAA,IACL;AAAA,MACE,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,GAAG;AAAA,QACH,4BAA4B;AAAA,UAC1B;AAAA,UACA;AAAA,YACE,mBAAmB,CAAC,yBAAyB;AAAA,UAC/C;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO,CAAC,6BAA6B,0BAA0B;AAAA,MAC/D,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA,QAKH,gCAAgC;AAAA,MAClC;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO,CAAC,UAAU;AAAA,MAClB,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,sCAAsC;AAAA,QACtC,yCAAyC;AAAA,QACzC,0BAA0B;AAAA,QAC1B,8BAA8B;AAAA,QAC9B,8BAA8B;AAAA,QAC9B,wBAAwB;AAAA,UACtB;AAAA,UACA;AAAA,YACE,UAAU;AAAA,YACV,aAAa;AAAA,UACf;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
baseRules,
|
|
3
3
|
createBaseLanguageOptions,
|
|
4
4
|
createTypedLanguageOptions
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-GEVX3BTI.mjs";
|
|
6
6
|
|
|
7
7
|
// src/astro/configs.ts
|
|
8
8
|
function createAstroConfigs(pluginReference) {
|
|
@@ -40,4 +40,4 @@ function createAstroConfigs(pluginReference) {
|
|
|
40
40
|
export {
|
|
41
41
|
createAstroConfigs
|
|
42
42
|
};
|
|
43
|
-
//# sourceMappingURL=chunk-
|
|
43
|
+
//# sourceMappingURL=chunk-7VZBQ6FD.mjs.map
|
|
@@ -75,6 +75,9 @@ function createSharedConfigs(pluginReference) {
|
|
|
75
75
|
// estado y acciones a un store global o custom hook.
|
|
76
76
|
"skapxd/no-functions-inside-components": "error",
|
|
77
77
|
"skapxd/no-tunnel-props": "error",
|
|
78
|
+
// Listeners en efectos: un AbortController por efecto, cleanup con
|
|
79
|
+
// un solo abort() en vez de removeEventListener por listener.
|
|
80
|
+
"skapxd/prefer-abort-signal": "error",
|
|
78
81
|
"skapxd/no-jsx-ternary-null": "error",
|
|
79
82
|
"skapxd/max-hook-size": [
|
|
80
83
|
"error",
|
|
@@ -111,4 +114,4 @@ export {
|
|
|
111
114
|
createSharedConfigs,
|
|
112
115
|
strictConfig
|
|
113
116
|
};
|
|
114
|
-
//# sourceMappingURL=chunk-
|
|
117
|
+
//# sourceMappingURL=chunk-GEVX3BTI.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/shared/configs/create-typed-language-options.ts","../src/shared/configs/base-rules.ts","../src/shared/configs/create-base-language-options.ts","../src/shared/configs/create-shared-configs.ts","../src/shared/configs/strict-config.ts"],"sourcesContent":["import tseslint from \"typescript-eslint\";\n\nexport function createTypedLanguageOptions() {\n return {\n // Sin el parser explícito, un consumidor que use solo estos presets\n // obtiene \"Parsing error\" en cada archivo TS (espree no parsea TS).\n parser: tseslint.parser,\n parserOptions: {\n projectService: true,\n },\n };\n}\n","export const baseRules = {\n \"skapxd/no-ad-hoc-ok-result\": \"error\",\n \"skapxd/no-deep-relative-imports\": \"error\",\n \"skapxd/no-default-export\": \"error\",\n \"skapxd/no-emoji\": \"error\",\n \"skapxd/no-promise-chain\": \"error\",\n \"skapxd/no-try-catch\": \"error\",\n \"skapxd/one-root-function-per-file\": \"error\",\n \"skapxd/prefer-ts-pattern\": \"error\",\n \"skapxd/result-error-requires-cause\": \"error\",\n};\n","import tseslint from \"typescript-eslint\";\n\n// Variante sin type-checking: solo el parser, para presets que aplican a TS\n// pero no necesitan projectService (base, package, next/base, next/react).\nexport function createBaseLanguageOptions() {\n return {\n parser: tseslint.parser,\n };\n}\n","import { baseRules } from \"./base-rules\";\nimport { createBaseLanguageOptions } from \"./create-base-language-options\";\nimport { createTypedLanguageOptions } from \"./create-typed-language-options\";\n\nexport function createSharedConfigs(pluginReference: unknown) {\n const baseLanguageOptions = createBaseLanguageOptions();\n const typedLanguageOptions = createTypedLanguageOptions();\n\n return {\n backend: {\n languageOptions: typedLanguageOptions,\n name: \"skapxd/shared/backend\",\n plugins: { skapxd: pluginReference },\n rules: {\n ...baseRules,\n // La regla obligatoria del sistema de errores es await-requires-result\n // (todo await resuelve en Result). async-functions-return-result queda\n // apagada por defecto: exigir la firma por decreto choca con los bordes\n // del framework y bloquea la adopción incremental; la presión sobre los\n // awaits produce el mismo estado final. Ver \"¿Por qué está apagada por\n // defecto?\" en el README.\n \"skapxd/await-requires-result\": \"error\",\n },\n },\n base: {\n languageOptions: baseLanguageOptions,\n name: \"skapxd/shared/base\",\n plugins: { skapxd: pluginReference },\n rules: baseRules,\n },\n frontend: {\n languageOptions: typedLanguageOptions,\n name: \"skapxd/shared/frontend\",\n plugins: { skapxd: pluginReference },\n rules: {\n ...baseRules,\n // En el front no se obliga a retornar Result, pero todo await debe\n // resolver en uno: o la función llamada ya retorna Promise<Result>\n // (camino preferido: errores modelados en el dominio) o se envuelve\n // en trySafe en el sitio.\n \"skapxd/await-requires-result\": \"error\",\n \"skapxd/jsx-return-name-pascal-case\": \"error\",\n // Anti prop-drilling: ninguna prop viaja más de un nivel. Quien la\n // crea puede pasarla a UN hijo; quien la recibe no la reenvía —\n // estado y acciones a un store global o custom hook.\n \"skapxd/no-functions-inside-components\": \"error\",\n \"skapxd/no-tunnel-props\": \"error\",\n \"skapxd/no-jsx-ternary-null\": \"error\",\n \"skapxd/max-hook-size\": [\n \"error\",\n {\n maxLines: 120,\n maxUseState: 1,\n },\n ],\n },\n },\n package: {\n languageOptions: baseLanguageOptions,\n name: \"skapxd/shared/package\",\n plugins: { skapxd: pluginReference },\n rules: {\n \"skapxd/one-root-function-per-file\": \"error\",\n },\n },\n };\n}\n","// Config endurecida: ignora TODOS los comentarios `eslint-disable` (y demás\n// directivas inline) en los archivos que cubre. Así ni una persona ni un agente\n// pueden saltarse una regla con `// eslint-disable-next-line`.\nexport const strictConfig = {\n linterOptions: {\n noInlineConfig: true,\n },\n name: \"skapxd/strict\",\n};\n"],"mappings":";AAAA,OAAO,cAAc;AAEd,SAAS,6BAA6B;AAC3C,SAAO;AAAA;AAAA;AAAA,IAGL,QAAQ,SAAS;AAAA,IACjB,eAAe;AAAA,MACb,gBAAgB;AAAA,IAClB;AAAA,EACF;AACF;;;ACXO,IAAM,YAAY;AAAA,EACvB,8BAA8B;AAAA,EAC9B,mCAAmC;AAAA,EACnC,4BAA4B;AAAA,EAC5B,mBAAmB;AAAA,EACnB,2BAA2B;AAAA,EAC3B,uBAAuB;AAAA,EACvB,qCAAqC;AAAA,EACrC,4BAA4B;AAAA,EAC5B,sCAAsC;AACxC;;;ACVA,OAAOA,eAAc;AAId,SAAS,4BAA4B;AAC1C,SAAO;AAAA,IACL,QAAQA,UAAS;AAAA,EACnB;AACF;;;ACJO,SAAS,oBAAoB,iBAA0B;AAC5D,QAAM,sBAAsB,0BAA0B;AACtD,QAAM,uBAAuB,2BAA2B;AAExD,SAAO;AAAA,IACL,SAAS;AAAA,MACP,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOH,gCAAgC;AAAA,MAClC;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,IACT;AAAA,IACA,UAAU;AAAA,MACR,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA,QAKH,gCAAgC;AAAA,QAChC,sCAAsC;AAAA;AAAA;AAAA;AAAA,QAItC,yCAAyC;AAAA,QACzC,0BAA0B;AAAA,
|
|
1
|
+
{"version":3,"sources":["../src/shared/configs/create-typed-language-options.ts","../src/shared/configs/base-rules.ts","../src/shared/configs/create-base-language-options.ts","../src/shared/configs/create-shared-configs.ts","../src/shared/configs/strict-config.ts"],"sourcesContent":["import tseslint from \"typescript-eslint\";\n\nexport function createTypedLanguageOptions() {\n return {\n // Sin el parser explícito, un consumidor que use solo estos presets\n // obtiene \"Parsing error\" en cada archivo TS (espree no parsea TS).\n parser: tseslint.parser,\n parserOptions: {\n projectService: true,\n },\n };\n}\n","export const baseRules = {\n \"skapxd/no-ad-hoc-ok-result\": \"error\",\n \"skapxd/no-deep-relative-imports\": \"error\",\n \"skapxd/no-default-export\": \"error\",\n \"skapxd/no-emoji\": \"error\",\n \"skapxd/no-promise-chain\": \"error\",\n \"skapxd/no-try-catch\": \"error\",\n \"skapxd/one-root-function-per-file\": \"error\",\n \"skapxd/prefer-ts-pattern\": \"error\",\n \"skapxd/result-error-requires-cause\": \"error\",\n};\n","import tseslint from \"typescript-eslint\";\n\n// Variante sin type-checking: solo el parser, para presets que aplican a TS\n// pero no necesitan projectService (base, package, next/base, next/react).\nexport function createBaseLanguageOptions() {\n return {\n parser: tseslint.parser,\n };\n}\n","import { baseRules } from \"./base-rules\";\nimport { createBaseLanguageOptions } from \"./create-base-language-options\";\nimport { createTypedLanguageOptions } from \"./create-typed-language-options\";\n\nexport function createSharedConfigs(pluginReference: unknown) {\n const baseLanguageOptions = createBaseLanguageOptions();\n const typedLanguageOptions = createTypedLanguageOptions();\n\n return {\n backend: {\n languageOptions: typedLanguageOptions,\n name: \"skapxd/shared/backend\",\n plugins: { skapxd: pluginReference },\n rules: {\n ...baseRules,\n // La regla obligatoria del sistema de errores es await-requires-result\n // (todo await resuelve en Result). async-functions-return-result queda\n // apagada por defecto: exigir la firma por decreto choca con los bordes\n // del framework y bloquea la adopción incremental; la presión sobre los\n // awaits produce el mismo estado final. Ver \"¿Por qué está apagada por\n // defecto?\" en el README.\n \"skapxd/await-requires-result\": \"error\",\n },\n },\n base: {\n languageOptions: baseLanguageOptions,\n name: \"skapxd/shared/base\",\n plugins: { skapxd: pluginReference },\n rules: baseRules,\n },\n frontend: {\n languageOptions: typedLanguageOptions,\n name: \"skapxd/shared/frontend\",\n plugins: { skapxd: pluginReference },\n rules: {\n ...baseRules,\n // En el front no se obliga a retornar Result, pero todo await debe\n // resolver en uno: o la función llamada ya retorna Promise<Result>\n // (camino preferido: errores modelados en el dominio) o se envuelve\n // en trySafe en el sitio.\n \"skapxd/await-requires-result\": \"error\",\n \"skapxd/jsx-return-name-pascal-case\": \"error\",\n // Anti prop-drilling: ninguna prop viaja más de un nivel. Quien la\n // crea puede pasarla a UN hijo; quien la recibe no la reenvía —\n // estado y acciones a un store global o custom hook.\n \"skapxd/no-functions-inside-components\": \"error\",\n \"skapxd/no-tunnel-props\": \"error\",\n // Listeners en efectos: un AbortController por efecto, cleanup con\n // un solo abort() en vez de removeEventListener por listener.\n \"skapxd/prefer-abort-signal\": \"error\",\n \"skapxd/no-jsx-ternary-null\": \"error\",\n \"skapxd/max-hook-size\": [\n \"error\",\n {\n maxLines: 120,\n maxUseState: 1,\n },\n ],\n },\n },\n package: {\n languageOptions: baseLanguageOptions,\n name: \"skapxd/shared/package\",\n plugins: { skapxd: pluginReference },\n rules: {\n \"skapxd/one-root-function-per-file\": \"error\",\n },\n },\n };\n}\n","// Config endurecida: ignora TODOS los comentarios `eslint-disable` (y demás\n// directivas inline) en los archivos que cubre. Así ni una persona ni un agente\n// pueden saltarse una regla con `// eslint-disable-next-line`.\nexport const strictConfig = {\n linterOptions: {\n noInlineConfig: true,\n },\n name: \"skapxd/strict\",\n};\n"],"mappings":";AAAA,OAAO,cAAc;AAEd,SAAS,6BAA6B;AAC3C,SAAO;AAAA;AAAA;AAAA,IAGL,QAAQ,SAAS;AAAA,IACjB,eAAe;AAAA,MACb,gBAAgB;AAAA,IAClB;AAAA,EACF;AACF;;;ACXO,IAAM,YAAY;AAAA,EACvB,8BAA8B;AAAA,EAC9B,mCAAmC;AAAA,EACnC,4BAA4B;AAAA,EAC5B,mBAAmB;AAAA,EACnB,2BAA2B;AAAA,EAC3B,uBAAuB;AAAA,EACvB,qCAAqC;AAAA,EACrC,4BAA4B;AAAA,EAC5B,sCAAsC;AACxC;;;ACVA,OAAOA,eAAc;AAId,SAAS,4BAA4B;AAC1C,SAAO;AAAA,IACL,QAAQA,UAAS;AAAA,EACnB;AACF;;;ACJO,SAAS,oBAAoB,iBAA0B;AAC5D,QAAM,sBAAsB,0BAA0B;AACtD,QAAM,uBAAuB,2BAA2B;AAExD,SAAO;AAAA,IACL,SAAS;AAAA,MACP,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOH,gCAAgC;AAAA,MAClC;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,IACT;AAAA,IACA,UAAU;AAAA,MACR,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA,QAKH,gCAAgC;AAAA,QAChC,sCAAsC;AAAA;AAAA;AAAA;AAAA,QAItC,yCAAyC;AAAA,QACzC,0BAA0B;AAAA;AAAA;AAAA,QAG1B,8BAA8B;AAAA,QAC9B,8BAA8B;AAAA,QAC9B,wBAAwB;AAAA,UACtB;AAAA,UACA;AAAA,YACE,UAAU;AAAA,YACV,aAAa;AAAA,UACf;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,SAAS;AAAA,MACP,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,qCAAqC;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AACF;;;AClEO,IAAM,eAAe;AAAA,EAC1B,eAAe;AAAA,IACb,gBAAgB;AAAA,EAClB;AAAA,EACA,MAAM;AACR;","names":["tseslint"]}
|
|
@@ -1697,6 +1697,141 @@ var noTryCatch = {
|
|
|
1697
1697
|
}
|
|
1698
1698
|
};
|
|
1699
1699
|
|
|
1700
|
+
// src/utils/get-prefer-abort-signal-options.ts
|
|
1701
|
+
function getPreferAbortSignalOptions(options = {}) {
|
|
1702
|
+
return {
|
|
1703
|
+
allowFilePatterns: options.allowFilePatterns ?? [],
|
|
1704
|
+
effectNames: options.effectNames ?? ["useEffect", "useLayoutEffect"]
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// src/utils/get-variable-initializer.ts
|
|
1709
|
+
function getVariableInitializer(identifier, scope) {
|
|
1710
|
+
let current = scope;
|
|
1711
|
+
while (current) {
|
|
1712
|
+
const variable = current.variables.find(
|
|
1713
|
+
(candidate) => candidate.name === identifier.name
|
|
1714
|
+
);
|
|
1715
|
+
if (variable) {
|
|
1716
|
+
const definition = variable.defs[0];
|
|
1717
|
+
return definition?.node?.type === "VariableDeclarator" ? definition.node.init : null;
|
|
1718
|
+
}
|
|
1719
|
+
current = current.upper;
|
|
1720
|
+
}
|
|
1721
|
+
return null;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/utils/object-expression-has-signal.ts
|
|
1725
|
+
function objectExpressionHasSignal(objectExpression) {
|
|
1726
|
+
return objectExpression.properties.some(
|
|
1727
|
+
(property) => property.type === "SpreadElement" || isPropertyKeyNamed(property, "signal")
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/utils/has-abort-signal-option.ts
|
|
1732
|
+
function hasAbortSignalOption(callExpression, sourceCode, typeContext) {
|
|
1733
|
+
const options = callExpression.arguments[2];
|
|
1734
|
+
if (!options) {
|
|
1735
|
+
return false;
|
|
1736
|
+
}
|
|
1737
|
+
if (options.type === "Literal") {
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1740
|
+
if (options.type === "ObjectExpression") {
|
|
1741
|
+
return objectExpressionHasSignal(options);
|
|
1742
|
+
}
|
|
1743
|
+
if (options.type === "Identifier") {
|
|
1744
|
+
const initializer = getVariableInitializer(
|
|
1745
|
+
options,
|
|
1746
|
+
sourceCode.getScope(options)
|
|
1747
|
+
);
|
|
1748
|
+
if (initializer?.type === "ObjectExpression") {
|
|
1749
|
+
return objectExpressionHasSignal(initializer);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (typeContext) {
|
|
1753
|
+
const type = typeContext.services.getTypeAtLocation(options);
|
|
1754
|
+
return Boolean(typeContext.checker.getPropertyOfType(type, "signal"));
|
|
1755
|
+
}
|
|
1756
|
+
return true;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/utils/is-inside-effect-callback.ts
|
|
1760
|
+
function isInsideEffectCallback(node, effectNames) {
|
|
1761
|
+
let current = node.parent;
|
|
1762
|
+
while (current) {
|
|
1763
|
+
if (isFunctionNode(current)) {
|
|
1764
|
+
const call = current.parent;
|
|
1765
|
+
if (call?.type === "CallExpression" && call.arguments[0] === current && isCalleeNamed(call.callee, effectNames)) {
|
|
1766
|
+
return true;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
current = current.parent;
|
|
1770
|
+
}
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/rules/prefer-abort-signal.ts
|
|
1775
|
+
var preferAbortSignal = {
|
|
1776
|
+
meta: {
|
|
1777
|
+
type: "suggestion",
|
|
1778
|
+
docs: {
|
|
1779
|
+
description: "En efectos de React, los listeners se limpian con AbortController, no con removeEventListener."
|
|
1780
|
+
},
|
|
1781
|
+
messages: {
|
|
1782
|
+
addWithoutSignal: "Este addEventListener vive en un efecto sin `signal`. Crea `const controller = new AbortController()`, pasa `{ signal: controller.signal }` como tercer argumento y limpia con `return () => controller.abort()`: un solo abort cubre todos los listeners del efecto.",
|
|
1783
|
+
removeInsteadOfAbort: "No quites listeners a mano en el cleanup: registra con `{ signal: controller.signal }` y reemplaza este removeEventListener por `return () => controller.abort()`. Evita el bug clasico de pasar una referencia distinta al remover."
|
|
1784
|
+
},
|
|
1785
|
+
schema: [
|
|
1786
|
+
{
|
|
1787
|
+
additionalProperties: false,
|
|
1788
|
+
properties: {
|
|
1789
|
+
allowFilePatterns: {
|
|
1790
|
+
items: { type: "string" },
|
|
1791
|
+
type: "array"
|
|
1792
|
+
},
|
|
1793
|
+
effectNames: {
|
|
1794
|
+
items: { type: "string" },
|
|
1795
|
+
type: "array"
|
|
1796
|
+
}
|
|
1797
|
+
},
|
|
1798
|
+
type: "object"
|
|
1799
|
+
}
|
|
1800
|
+
]
|
|
1801
|
+
},
|
|
1802
|
+
create(context) {
|
|
1803
|
+
const options = getPreferAbortSignalOptions(context.options[0]);
|
|
1804
|
+
const filename = context.filename ?? context.getFilename();
|
|
1805
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
1806
|
+
const typeContext = getTypeContext(context);
|
|
1807
|
+
if (matchesAnyGlob(filename, options.allowFilePatterns)) {
|
|
1808
|
+
return {};
|
|
1809
|
+
}
|
|
1810
|
+
return {
|
|
1811
|
+
CallExpression(node) {
|
|
1812
|
+
if (node.callee?.type !== "MemberExpression") {
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
const isAdd = isMemberPropertyNamed(node.callee, "addEventListener");
|
|
1816
|
+
const isRemove = isMemberPropertyNamed(node.callee, "removeEventListener");
|
|
1817
|
+
if (!isAdd && !isRemove) {
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (!isInsideEffectCallback(node, options.effectNames)) {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
if (isRemove) {
|
|
1824
|
+
context.report({ messageId: "removeInsteadOfAbort", node });
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (!hasAbortSignalOption(node, sourceCode, typeContext)) {
|
|
1828
|
+
context.report({ messageId: "addWithoutSignal", node });
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1700
1835
|
// src/rules/prefer-ts-pattern.ts
|
|
1701
1836
|
var preferTsPattern = {
|
|
1702
1837
|
meta: {
|
|
@@ -1850,6 +1985,7 @@ var rules = {
|
|
|
1850
1985
|
"no-tunnel-props": noTunnelProps,
|
|
1851
1986
|
"no-functions-inside-components": noFunctionsInsideComponents,
|
|
1852
1987
|
"no-try-catch": noTryCatch,
|
|
1988
|
+
"prefer-abort-signal": preferAbortSignal,
|
|
1853
1989
|
"prefer-ts-pattern": preferTsPattern,
|
|
1854
1990
|
"no-jsx-ternary-null": noJsxTernaryNull,
|
|
1855
1991
|
"no-promise-chain": noPromiseChain
|
|
@@ -1858,4 +1994,4 @@ var rules = {
|
|
|
1858
1994
|
export {
|
|
1859
1995
|
rules
|
|
1860
1996
|
};
|
|
1861
|
-
//# sourceMappingURL=chunk-
|
|
1997
|
+
//# sourceMappingURL=chunk-WO7EUISC.mjs.map
|