@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 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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createAstroConfigs
3
- } from "../chunk-5P6OHVFD.mjs";
4
- import "../chunk-EGNXI5HL.mjs";
3
+ } from "../chunk-7VZBQ6FD.mjs";
4
+ import "../chunk-GEVX3BTI.mjs";
5
5
  export {
6
6
  createAstroConfigs
7
7
  };
@@ -6,7 +6,7 @@ import {
6
6
  baseRules,
7
7
  createBaseLanguageOptions,
8
8
  createTypedLanguageOptions
9
- } from "./chunk-EGNXI5HL.mjs";
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-QRYL7RJO.mjs.map
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-EGNXI5HL.mjs";
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-5P6OHVFD.mjs.map
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-EGNXI5HL.mjs.map
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,QAC1B,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;;;AC/DO,IAAM,eAAe;AAAA,EAC1B,eAAe;AAAA,IACb,gBAAgB;AAAA,EAClB;AAAA,EACA,MAAM;AACR;","names":["tseslint"]}
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-O4RTJGEU.mjs.map
1997
+ //# sourceMappingURL=chunk-WO7EUISC.mjs.map