@skapxd/eslint-opinionated 0.8.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
@@ -549,6 +549,8 @@ de cada regla):
549
549
  | `no-emoji` | `allowFilePatterns` (globs) |
550
550
  | `no-functions-inside-components` | `allowJsxCallbacks`, `allowArrayMapCallbacks` (ambas `true` por defecto) |
551
551
  | `no-promise-chain` | `methods` |
552
+ | `no-tunnel-props` | `allowFilePatterns` (globs), `allowPropPatterns` (regex) |
553
+ | `prefer-abort-signal` | `allowFilePatterns` (globs), `effectNames` (default `["useEffect", "useLayoutEffect"]`) |
552
554
 
553
555
  Los `allowFilePatterns` de todas las reglas son **globs** (`*` un segmento,
554
556
  `**` cualquier profundidad, `{a,b}` alternativas; un patrón sin prefijo
@@ -569,6 +571,8 @@ matchea en cualquier carpeta). Las 7 reglas restantes no tienen opciones: su
569
571
  | `skapxd/no-deep-relative-imports` | Limita la profundidad de los imports relativos (`../`). |
570
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. |
571
573
  | `skapxd/no-emoji` | Prohíbe emojis en strings y JSX; cada sistema los renderiza distinto. Usa un icono SVG. |
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`. |
572
576
  | `skapxd/no-functions-inside-components` | Prohíbe definir funciones dentro de componentes React. |
573
577
  | `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
574
578
  | `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
@@ -912,6 +916,58 @@ eximir archivos completos (fixtures, seeds), usa `allowFilePatterns`:
912
916
  }]
913
917
  ```
914
918
 
919
+ ### `skapxd/no-tunnel-props`
920
+
921
+ **Ninguna prop viaja más de un nivel.** El contrato de saltos: quien **crea**
922
+ un valor (estado de un hook, acción de un store, dato calculado) puede pasarlo
923
+ a UN hijo; quien lo **recibe** como prop no puede reenviarlo a otro
924
+ componente. Eso prohíbe exactamente la cadena `abuelo → padre → hijo` — el
925
+ prop drilling — sin tocar el paso legítimo de un nivel.
926
+
927
+ ```tsx
928
+ // ✅ primer salto: el abuelo CREA la acción y la baja un nivel
929
+ const Abuelo = () => {
930
+ const onSelect = useTranscriptStore((s) => s.select);
931
+ return <Padre onSelect={onSelect} />;
932
+ };
933
+
934
+ // ❌ segundo salto: el padre la RECIBE y la reenvía
935
+ const Padre = ({ onSelect }) => <Hijo onSelect={onSelect} />;
936
+
937
+ // ❌ el rename no lo esconde, y usarla localmente no autoriza el reenvío
938
+ const Padre = ({ onSelect }) => <Hijo handler={onSelect} />;
939
+
940
+ // ❌ el túnel puro
941
+ const Padre = ({ ...props }) => <Hijo {...props} />;
942
+ ```
943
+
944
+ La detección es local y exacta: si el identifier que pones en una prop de otro
945
+ componente viene de tus **props destructuradas**, no lo creaste tú — es su
946
+ segundo salto.
947
+
948
+ Las salidas que sugiere el mensaje:
949
+
950
+ 1. **Store global o custom hook**: la acción/estado vive en un store (p. ej.
951
+ [zustand](https://github.com/pmndrs/zustand)) o un hook, y el componente
952
+ que la necesita la consume directo — la cadena desaparece:
953
+
954
+ ```tsx
955
+ function Hijo({ entry }: { entry: Entry }) {
956
+ const select = useTranscriptStore((s) => s.select);
957
+ return <button onClick={() => select(entry.id)}>…</button>;
958
+ }
959
+ ```
960
+
961
+ 2. **Composición**: el padre arma el JSX y el intermedio recibe `children` —
962
+ el dato viaja dentro del JSX, no por props. (`children` nunca cuenta como
963
+ túnel: es la alternativa.)
964
+
965
+ No cuenta como reenvío: usar la prop (`<h2>{title}</h2>`), derivar datos
966
+ (`title={game.title}`), o pasarla a un elemento **nativo** (`value={value}`
967
+ en un `<input>` es la frontera con el DOM). Para wrappers legítimos de un
968
+ design system, exime props por nombre (`allowPropPatterns: ["^className$"]`)
969
+ o archivos completos (`allowFilePatterns`).
970
+
915
971
  ### `skapxd/no-functions-inside-components`
916
972
 
917
973
  Prohíbe definir funciones **con peso propio** dentro de un componente React
@@ -1031,6 +1087,53 @@ const label = match(status)
1031
1087
  .exhaustive();
1032
1088
  ```
1033
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
+
1034
1137
  ### `skapxd/no-jsx-ternary-null`
1035
1138
 
1036
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-MSH2BTXH.mjs";
4
- import "../chunk-Z6CU2N4C.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-Z6CU2N4C.mjs";
9
+ } from "./chunk-GEVX3BTI.mjs";
10
10
 
11
11
  // src/next/configs.ts
12
12
  var nextDefaultExportFileGlob = `{${[
@@ -53,6 +53,8 @@ function createNextConfigs(pluginReference) {
53
53
  rules: {
54
54
  "skapxd/jsx-return-name-pascal-case": "error",
55
55
  "skapxd/no-functions-inside-components": "error",
56
+ "skapxd/no-tunnel-props": "error",
57
+ "skapxd/prefer-abort-signal": "error",
56
58
  "skapxd/no-jsx-ternary-null": "error",
57
59
  "skapxd/max-hook-size": [
58
60
  "error",
@@ -69,4 +71,4 @@ function createNextConfigs(pluginReference) {
69
71
  export {
70
72
  createNextConfigs
71
73
  };
72
- //# sourceMappingURL=chunk-2IXJXIW5.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-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,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-Z6CU2N4C.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-MSH2BTXH.mjs.map
43
+ //# sourceMappingURL=chunk-7VZBQ6FD.mjs.map
@@ -70,7 +70,14 @@ function createSharedConfigs(pluginReference) {
70
70
  // en trySafe en el sitio.
71
71
  "skapxd/await-requires-result": "error",
72
72
  "skapxd/jsx-return-name-pascal-case": "error",
73
+ // Anti prop-drilling: ninguna prop viaja más de un nivel. Quien la
74
+ // crea puede pasarla a UN hijo; quien la recibe no la reenvía —
75
+ // estado y acciones a un store global o custom hook.
73
76
  "skapxd/no-functions-inside-components": "error",
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",
74
81
  "skapxd/no-jsx-ternary-null": "error",
75
82
  "skapxd/max-hook-size": [
76
83
  "error",
@@ -107,4 +114,4 @@ export {
107
114
  createSharedConfigs,
108
115
  strictConfig
109
116
  };
110
- //# sourceMappingURL=chunk-Z6CU2N4C.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 \"skapxd/no-functions-inside-components\": \"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,QACtC,yCAAyC;AAAA,QACzC,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;;;AC3DO,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"]}
@@ -1448,6 +1448,144 @@ var noEmoji = {
1448
1448
  }
1449
1449
  };
1450
1450
 
1451
+ // src/utils/collect-identifiers-named.ts
1452
+ function collectIdentifiersNamed(node, name, results = []) {
1453
+ if (node?.type === "Identifier" && node.name === name) {
1454
+ results.push(node);
1455
+ }
1456
+ for (const child of getNodeChildren(node)) {
1457
+ collectIdentifiersNamed(child, name, results);
1458
+ }
1459
+ return results;
1460
+ }
1461
+
1462
+ // src/utils/get-no-tunnel-props-options.ts
1463
+ function getNoTunnelPropsOptions(options = {}) {
1464
+ return {
1465
+ allowFilePatterns: options.allowFilePatterns ?? [],
1466
+ // Regex de nombres de prop que sí pueden reenviarse (p. ej.
1467
+ // ["^className$", "^style$"] en wrappers de un design system).
1468
+ allowPropPatterns: options.allowPropPatterns ?? []
1469
+ };
1470
+ }
1471
+
1472
+ // src/utils/get-object-pattern-prop-names.ts
1473
+ function getObjectPatternPropNames(pattern) {
1474
+ const result = { propNames: [], restName: null };
1475
+ if (pattern?.type !== "ObjectPattern") {
1476
+ return result;
1477
+ }
1478
+ for (const property of pattern.properties) {
1479
+ if (property.type === "RestElement" && property.argument.type === "Identifier") {
1480
+ result.restName = property.argument.name;
1481
+ continue;
1482
+ }
1483
+ if (property.type === "Property" && property.key.type === "Identifier" && property.value.type === "Identifier" && property.key.name === property.value.name) {
1484
+ result.propNames.push(property.key.name);
1485
+ }
1486
+ }
1487
+ return result;
1488
+ }
1489
+
1490
+ // src/utils/is-pascal-case-jsx-element.ts
1491
+ function isPascalCaseJsxElement(openingElement) {
1492
+ return openingElement?.type === "JSXOpeningElement" && openingElement.name?.type === "JSXIdentifier" && isPascalCaseName(openingElement.name.name);
1493
+ }
1494
+
1495
+ // src/utils/is-forwarded-prop-reference.ts
1496
+ function isForwardedPropReference(identifier) {
1497
+ const container = identifier.parent;
1498
+ if (container?.type !== "JSXExpressionContainer") {
1499
+ return false;
1500
+ }
1501
+ const attribute = container.parent;
1502
+ if (attribute?.type !== "JSXAttribute") {
1503
+ return false;
1504
+ }
1505
+ return isPascalCaseJsxElement(attribute.parent);
1506
+ }
1507
+
1508
+ // src/rules/no-tunnel-props.ts
1509
+ var noTunnelProps = {
1510
+ meta: {
1511
+ type: "problem",
1512
+ docs: {
1513
+ description: "Ninguna prop viaja mas de un nivel: quien la recibe no puede reenviarla a otro componente."
1514
+ },
1515
+ messages: {
1516
+ forwardedProp: "La prop `{{prop}}` que `{{component}}` recibe se reenvia a otro componente: ya va por su segundo salto (abuelo -> padre -> hijo). Quien CREA un valor puede pasarlo UN nivel; quien lo recibe no lo reenvia. Mueve el estado/accion a un store global (p. ej. zustand) o a un custom hook y consumelo donde se usa, o deja que el padre componga el JSX (`children`).",
1517
+ spreadTunnel: "`{{component}}` reenvia TODAS sus props con `{...{{name}}}` a otro componente: es un tunel puro. Mueve el estado a un store global (p. ej. zustand) o a un custom hook, o deja que el padre componga el JSX (`children`)."
1518
+ },
1519
+ schema: [
1520
+ {
1521
+ additionalProperties: false,
1522
+ properties: {
1523
+ allowFilePatterns: {
1524
+ items: { type: "string" },
1525
+ type: "array"
1526
+ },
1527
+ allowPropPatterns: {
1528
+ items: { type: "string" },
1529
+ type: "array"
1530
+ }
1531
+ },
1532
+ type: "object"
1533
+ }
1534
+ ]
1535
+ },
1536
+ create(context) {
1537
+ const options = getNoTunnelPropsOptions(context.options[0]);
1538
+ const filename = context.filename ?? context.getFilename();
1539
+ if (matchesAnyGlob(filename, options.allowFilePatterns)) {
1540
+ return {};
1541
+ }
1542
+ function reportSpreadTunnel(node, componentName, restName) {
1543
+ const spreads = collectIdentifiersNamed(node.body, restName).filter(
1544
+ (identifier) => identifier.parent?.type === "JSXSpreadAttribute" && isPascalCaseJsxElement(identifier.parent.parent)
1545
+ );
1546
+ if (spreads.length > 0) {
1547
+ context.report({
1548
+ data: { component: componentName, name: restName },
1549
+ messageId: "spreadTunnel",
1550
+ node: spreads[0].parent
1551
+ });
1552
+ }
1553
+ }
1554
+ function reportForwardedProps(node, componentName, propNames) {
1555
+ for (const propName of propNames) {
1556
+ if (matchesAnyPattern(propName, options.allowPropPatterns)) {
1557
+ continue;
1558
+ }
1559
+ for (const usage of collectIdentifiersNamed(node.body, propName)) {
1560
+ if (isForwardedPropReference(usage)) {
1561
+ context.report({
1562
+ data: { component: componentName, prop: propName },
1563
+ messageId: "forwardedProp",
1564
+ node: usage.parent.parent
1565
+ });
1566
+ }
1567
+ }
1568
+ }
1569
+ }
1570
+ function reportIfTunnelComponent(node) {
1571
+ const componentName = getFunctionName(node);
1572
+ if (!isPascalCaseName(componentName)) {
1573
+ return;
1574
+ }
1575
+ const { propNames, restName } = getObjectPatternPropNames(node.params[0]);
1576
+ if (restName) {
1577
+ reportSpreadTunnel(node, componentName, restName);
1578
+ }
1579
+ reportForwardedProps(node, componentName, propNames);
1580
+ }
1581
+ return {
1582
+ ArrowFunctionExpression: reportIfTunnelComponent,
1583
+ FunctionDeclaration: reportIfTunnelComponent,
1584
+ FunctionExpression: reportIfTunnelComponent
1585
+ };
1586
+ }
1587
+ };
1588
+
1451
1589
  // src/utils/get-no-functions-inside-components-options.ts
1452
1590
  function getNoFunctionsInsideComponentsOptions(options = {}) {
1453
1591
  return {
@@ -1559,6 +1697,141 @@ var noTryCatch = {
1559
1697
  }
1560
1698
  };
1561
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
+
1562
1835
  // src/rules/prefer-ts-pattern.ts
1563
1836
  var preferTsPattern = {
1564
1837
  meta: {
@@ -1709,8 +1982,10 @@ var rules = {
1709
1982
  "no-deep-relative-imports": noDeepRelativeImports,
1710
1983
  "no-default-export": noDefaultExport,
1711
1984
  "no-emoji": noEmoji,
1985
+ "no-tunnel-props": noTunnelProps,
1712
1986
  "no-functions-inside-components": noFunctionsInsideComponents,
1713
1987
  "no-try-catch": noTryCatch,
1988
+ "prefer-abort-signal": preferAbortSignal,
1714
1989
  "prefer-ts-pattern": preferTsPattern,
1715
1990
  "no-jsx-ternary-null": noJsxTernaryNull,
1716
1991
  "no-promise-chain": noPromiseChain
@@ -1719,4 +1994,4 @@ var rules = {
1719
1994
  export {
1720
1995
  rules
1721
1996
  };
1722
- //# sourceMappingURL=chunk-47EZPLJX.mjs.map
1997
+ //# sourceMappingURL=chunk-WO7EUISC.mjs.map