@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 +103 -0
- package/dist/astro/index.mjs +2 -2
- package/dist/{chunk-2IXJXIW5.mjs → chunk-3WLCAPUA.mjs} +4 -2
- package/dist/{chunk-2IXJXIW5.mjs.map → chunk-3WLCAPUA.mjs.map} +1 -1
- package/dist/{chunk-MSH2BTXH.mjs → chunk-7VZBQ6FD.mjs} +2 -2
- package/dist/{chunk-Z6CU2N4C.mjs → chunk-GEVX3BTI.mjs} +8 -1
- package/dist/{chunk-Z6CU2N4C.mjs.map → chunk-GEVX3BTI.mjs.map} +1 -1
- package/dist/{chunk-47EZPLJX.mjs → chunk-WO7EUISC.mjs} +276 -1
- package/dist/chunk-WO7EUISC.mjs.map +1 -0
- package/dist/index.js +285 -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 +6 -0
- package/dist/next/index.d.ts +6 -0
- package/dist/next/index.js +2 -0
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +2 -2
- package/dist/shared/index.d.mts +2 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +282 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +2 -2
- package/package.json +1 -1
- package/dist/chunk-47EZPLJX.mjs.map +0 -1
- /package/dist/{chunk-MSH2BTXH.mjs.map → chunk-7VZBQ6FD.mjs.map} +0 -0
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
|
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 = `{${[
|
|
@@ -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-
|
|
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-
|
|
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
|
|
@@ -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-
|
|
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,
|
|
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-
|
|
1997
|
+
//# sourceMappingURL=chunk-WO7EUISC.mjs.map
|