@skapxd/eslint-opinionated 0.5.0 → 0.7.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
@@ -510,6 +510,50 @@ export default [
510
510
  Si necesitas una excepción puntual (p. ej. archivos generados), añade después un
511
511
  bloque con `linterOptions: { noInlineConfig: false }` para esos globs.
512
512
 
513
+ ## Configurar y sobrescribir reglas
514
+
515
+ Los presets son flat configs normales de ESLint: **el último config que
516
+ matchea un archivo gana**. Para ajustar una regla encima de un preset, esparce
517
+ sus `rules` y sobrescribe la entrada:
518
+
519
+ ```js
520
+ export default [
521
+ {
522
+ files: ["src/**/*.{ts,tsx}"],
523
+ ...skapxd.configs.shared.frontend,
524
+ rules: {
525
+ ...skapxd.configs.shared.frontend.rules,
526
+ // mismo id, nuevas opciones: esta entrada reemplaza a la del preset
527
+ "skapxd/no-deep-relative-imports": ["error", { maxDepth: 1 }],
528
+ },
529
+ },
530
+ ];
531
+ ```
532
+
533
+ > Las opciones de una regla **se reemplazan completas**, no se mergean: si el
534
+ > preset pasaba opciones y tú la redeclaras, incluye también las que quieras
535
+ > conservar. (Excepción: los patrones *integrados* de `no-default-export` —
536
+ > configs y stories — viven dentro de la regla y nunca se pierden; tus
537
+ > `allowFilePatterns` se suman a ellos.)
538
+
539
+ Referencia rápida de qué se puede configurar (detalle y defaults en la sección
540
+ de cada regla):
541
+
542
+ | Regla | Opciones |
543
+ | --- | --- |
544
+ | `async-functions-return-result` | `allowFilePatterns` (globs), `allowNamePatterns` (regex), `checkMissingReturnType`, `checkMissingReturnTypeWhenCallNames`, `requireCallNames`, `promiseTypeNames`, `resultTypeNames` |
545
+ | `await-requires-result` | `allowFilePatterns` (globs), `trySafeCallNames` |
546
+ | `max-hook-size` | `maxLines`, `maxUseState` |
547
+ | `no-deep-relative-imports` | `maxDepth` |
548
+ | `no-default-export` | `allowFilePatterns` (globs, aditivos a los integrados) |
549
+ | `no-functions-inside-components` | `allowJsxCallbacks`, `allowArrayMapCallbacks` (ambas `true` por defecto) |
550
+ | `no-promise-chain` | `methods` |
551
+
552
+ Los `allowFilePatterns` de todas las reglas son **globs** (`*` un segmento,
553
+ `**` cualquier profundidad, `{a,b}` alternativas; un patrón sin prefijo
554
+ matchea en cualquier carpeta). Las 7 reglas restantes no tienen opciones: su
555
+ única configuración es activarlas, apagarlas o cambiar la severidad.
556
+
513
557
  ## Reglas
514
558
 
515
559
  | Regla | Qué protege |
@@ -522,6 +566,7 @@ bloque con `linterOptions: { noInlineConfig: false }` para esos globs.
522
566
  | `skapxd/max-hook-size` | Marca hooks grandes o con demasiados `useState`. |
523
567
  | `skapxd/jsx-return-name-pascal-case` | Funciones que retornan JSX deben nombrarse como componentes. |
524
568
  | `skapxd/no-deep-relative-imports` | Limita la profundidad de los imports relativos (`../`). |
569
+ | `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. |
525
570
  | `skapxd/no-functions-inside-components` | Prohíbe definir funciones dentro de componentes React. |
526
571
  | `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
527
572
  | `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
@@ -610,6 +655,20 @@ async function no(): Promise<Result<number, Error>> {} // se reporta
610
655
  > Sin información de tipos cae a una comprobación por nombre (`resultTypeNames`),
611
656
  > menos estricta.
612
657
 
658
+ Todas las opciones, con sus defaults:
659
+
660
+ ```js
661
+ "skapxd/async-functions-return-result": ["error", {
662
+ allowFilePatterns: [], // globs de archivos exentos, p. ej. ["src/legacy/**"]
663
+ allowNamePatterns: [], // regex de nombres exentos, p. ej. ["^(GET|POST)$"]
664
+ checkMissingReturnType: true, // reportar también funciones SIN anotación de retorno
665
+ checkMissingReturnTypeWhenCallNames: [], // ...o solo si el cuerpo llama a estos nombres
666
+ requireCallNames: [], // acotar la regla a funciones que llamen a estos nombres
667
+ promiseTypeNames: ["Promise"], // wrappers de promesa aceptados (fallback sin tipos)
668
+ resultTypeNames: ["Result"], // nombres de Result aceptados (fallback sin tipos)
669
+ }]
670
+ ```
671
+
613
672
  ### `skapxd/result-error-requires-cause`
614
673
 
615
674
  Evita perder el error original al transformar un `Result` fallido:
@@ -698,6 +757,30 @@ Sirve para código de pegamento, pero deja el error sin modelar (`Result<T,
698
757
  unknown>`). Cuando la misma operación se repite o el error importa, el mensaje
699
758
  de la regla empuja hacia el camino 1.
700
759
 
760
+ ### `skapxd/no-ad-hoc-ok-result`
761
+
762
+ Prohíbe que una función async **exportada** retorne objetos literales con la
763
+ forma `{ ok: ... }` armados a mano. Un contrato casero fragmenta el sistema:
764
+ cada módulo inventa su variante, la exención type-aware de
765
+ `await-requires-result` no lo reconoce, y `match()` pierde la exhaustividad.
766
+
767
+ ```ts
768
+ export async function getUser(id: string) {
769
+ return { ok: false, message: "falló" }; // ❌ contrato inventado
770
+ }
771
+
772
+ export async function getUser(id: string): Promise<Result<User, UserError>> {
773
+ return Result.err({ // ✅ el Result real
774
+ cause: error,
775
+ message: "No pude cargar el usuario.",
776
+ type: "USER_FETCH_FAILED",
777
+ });
778
+ }
779
+ ```
780
+
781
+ Solo mira funciones async exportadas: un helper interno con un objeto `ok`
782
+ cualquiera no es un contrato público y no se reporta.
783
+
701
784
  ### `skapxd/max-hook-size`
702
785
 
703
786
  Marca hooks que crecen demasiado o acumulan muchos `useState`.
@@ -705,6 +788,35 @@ Marca hooks que crecen demasiado o acumulan muchos `useState`.
705
788
  La intención es empujar el diseño hacia `useReducer`, hooks más pequeños o
706
789
  módulos de transición de estado.
707
790
 
791
+ Opciones (los presets `frontend` y `next` usan `maxLines: 120`, `maxUseState: 1`):
792
+
793
+ ```js
794
+ "skapxd/max-hook-size": ["error", {
795
+ maxLines: 120, // líneas máximas del cuerpo del hook
796
+ maxUseState: 1, // useState propios permitidos antes de exigir useReducer
797
+ }]
798
+ ```
799
+
800
+ ### `skapxd/jsx-return-name-pascal-case`
801
+
802
+ Si una función devuelve JSX, es un componente, y debe llamarse como tal:
803
+ PascalCase. El mensaje sugiere el rename concreto.
804
+
805
+ ```tsx
806
+ function renderUserCard(user: User) { // ❌ "render*" devuelve JSX → es un componente
807
+ return <article>{user.name}</article>;
808
+ }
809
+
810
+ function UserCard({ user }: { user: User }) { // ✅ nombre de componente + props
811
+ return <article>{user.name}</article>;
812
+ }
813
+ ```
814
+
815
+ Esta regla es la que mantiene honesto al resto del sistema React: las reglas
816
+ de componentes detectan "componente" por nombre PascalCase, así que una
817
+ función `renderX` que devuelve JSX escaparía de ellas. Esta la captura y
818
+ fuerza el rename — y con el nombre corregido, las demás ya la ven.
819
+
708
820
  ### `skapxd/no-deep-relative-imports`
709
821
 
710
822
  Limita cuántos niveles puede subir un import relativo. Por defecto **prohíbe
@@ -730,20 +842,73 @@ Revisa imports estáticos (`import`), re-exports (`export ... from`) e imports
730
842
  dinámicos (`import(...)`). El remedio habitual es un alias de ruta (`@/...`) o
731
843
  acercar el módulo a quien lo usa.
732
844
 
845
+ ### `skapxd/no-default-export`
846
+
847
+ Prohíbe `export default` (incluida la forma `export { x as default }`). Con
848
+ exports nombrados, el nombre del símbolo es el contrato del módulo: renombrar
849
+ con el IDE actualiza todos los usos, `grep` encuentra definición y consumo, y
850
+ los autoimports no inventan nombres distintos por archivo.
851
+
852
+ ```ts
853
+ export default function getUser() {} // ❌ cada import puede llamarlo distinto
854
+ export function getUser() {} // ✅ un solo nombre canónico
855
+ ```
856
+
857
+ **Dónde sí se permite el default.** Hay entrypoints donde el ecosistema lo
858
+ exige, y la regla los reconoce en capas:
859
+
860
+ 1. **Integrados (siempre activos):** configs de tooling (`*.config.{js,mjs,cjs,ts}`:
861
+ `next.config`, `tailwind.config`, `vitest.config`, `eslint.config`, ...) y
862
+ stories de Storybook (`*.stories.*`).
863
+ 2. **Preset `next` (automático):** los entrypoints del App Router donde Next
864
+ exige el default — `page`, `layout`, `template`, `error`, `loading`,
865
+ `not-found`, `sitemap`, `robots`, `manifest`, `icon`, `opengraph-image`,
866
+ etc. No hay que configurar nada.
867
+ 3. **`allowFilePatterns` (extensible):** si usas un framework o tool que la
868
+ regla aún no contempla, agrega su glob. Los patrones propios se **suman**
869
+ a los integrados, no los reemplazan. Son globs legibles (`*` un segmento,
870
+ `**` cualquier profundidad, `{a,b}` alternativas) y un patrón sin prefijo
871
+ matchea en cualquier carpeta:
872
+
873
+ ```js
874
+ "skapxd/no-default-export": ["error", {
875
+ // p. ej. SvelteKit exige default en +page.ts / +layout.ts
876
+ allowFilePatterns: ["+page.ts", "+layout.ts"],
877
+ }]
878
+ ```
879
+
880
+ Detalle útil con `React.lazy` (que espera `{ default }`): no hace falta volver
881
+ al default export, basta mapear el named en el import dinámico:
882
+
883
+ ```ts
884
+ const Card = lazy(() => import("./card").then((m) => ({ default: m.Card })));
885
+ ```
886
+
733
887
  ### `skapxd/no-functions-inside-components`
734
888
 
735
- Prohíbe **cualquier** función definida dentro de un componente React (una función
736
- con nombre PascalCase). Cada render recrea esas funciones, lo que dispara
737
- re-renders innecesarios en hijos memoizados y mezcla lógica con composición.
889
+ Prohíbe definir funciones **con peso propio** dentro de un componente React
890
+ (una función con nombre PascalCase): handlers con nombre, helpers, callbacks de
891
+ `useEffect`. Cada render las recrea, dispara re-renders en hijos memoizados y
892
+ mezcla lógica con composición.
738
893
 
739
894
  ```tsx
740
895
  function Card() {
741
- const onClick = () => save(); // ❌ se recrea en cada render
896
+ const onClick = () => save(); // ❌ handler con nombre en el cuerpo
742
897
  useEffect(() => subscribe(), []); // ❌ callback dentro del componente
743
- return <ul>{items.map((i) => <Li />)}</ul>; // ❌ callback de .map en el render
898
+ return (
899
+ <ul>
900
+ {items.map((i) => <Li key={i} />)} {/* ✅ React idiomático */}
901
+ <button onClick={() => save()}>Guardar</button> {/* ✅ React idiomático */}
902
+ </ul>
903
+ );
744
904
  }
745
905
  ```
746
906
 
907
+ Los dos patrones idiomáticos de React están **permitidos por defecto**: el
908
+ callback anónimo como valor directo de una prop JSX y el callback anónimo de
909
+ `.map(...)` en el render. Forzarlos a salir del componente produce workarounds
910
+ peores que el problema (`.bind(null, ...)`, adapters artificiales).
911
+
747
912
  El cuerpo del componente queda como composición declarativa; **toda** función
748
913
  —handlers, efectos, memos, mapeos— vive fuera:
749
914
 
@@ -764,6 +929,28 @@ function Card() {
764
929
  helper en minúscula **sí** pueden tener funciones dentro — ahí es donde se mueve
765
930
  la lógica.
766
931
 
932
+ **Opciones.** Las exenciones aplican solo a **flechas de expresión** (sin
933
+ cuerpo `{ }`) en esa posición exacta: el valor directo de una prop JSX, o el
934
+ primer argumento de `.map(...)`. La distinción importa: una flecha de expresión
935
+ solo puede contener una expresión — es declarativa por construcción —, mientras
936
+ que un bloque da pie a `if`s, variables y llamadas que pertenecen fuera:
937
+
938
+ ```tsx
939
+ {items.map((i) => <li key={i} />)} // ✅ flecha de expresión
940
+ {items.map((i) => { return <li key={i} />; })} // ❌ bloque: invita a meter lógica
941
+ ```
942
+
943
+ Un handler con nombre en el cuerpo (`const onClick = () => ...`), un callback
944
+ de `useEffect` o un `.forEach` siguen reportándose. Para el modo ultraestricto
945
+ (ninguna función inline, como en v0.6.0 y anteriores), apágalas explícitamente:
946
+
947
+ ```js
948
+ "skapxd/no-functions-inside-components": ["error", {
949
+ allowJsxCallbacks: false, // también reporta onClick={() => ...}
950
+ allowArrayMapCallbacks: false, // también reporta items.map((i) => ...)
951
+ }]
952
+ ```
953
+
767
954
  ### `skapxd/no-try-catch`
768
955
 
769
956
  Prohíbe `try/catch`. La intención es que los errores se modelen como `Result` en
@@ -12,6 +12,7 @@ declare function createAstroConfigs(pluginReference: unknown): ({
12
12
  rules: {
13
13
  "skapxd/no-ad-hoc-ok-result": string;
14
14
  "skapxd/no-deep-relative-imports": string;
15
+ "skapxd/no-default-export": string;
15
16
  "skapxd/no-promise-chain": string;
16
17
  "skapxd/no-try-catch": string;
17
18
  "skapxd/one-root-function-per-file": string;
@@ -27,6 +28,7 @@ declare function createAstroConfigs(pluginReference: unknown): ({
27
28
  rules: {
28
29
  "skapxd/no-ad-hoc-ok-result": string;
29
30
  "skapxd/no-deep-relative-imports": string;
31
+ "skapxd/no-default-export": string;
30
32
  "skapxd/no-promise-chain": string;
31
33
  "skapxd/no-try-catch": string;
32
34
  "skapxd/one-root-function-per-file": string;
@@ -12,6 +12,7 @@ declare function createAstroConfigs(pluginReference: unknown): ({
12
12
  rules: {
13
13
  "skapxd/no-ad-hoc-ok-result": string;
14
14
  "skapxd/no-deep-relative-imports": string;
15
+ "skapxd/no-default-export": string;
15
16
  "skapxd/no-promise-chain": string;
16
17
  "skapxd/no-try-catch": string;
17
18
  "skapxd/one-root-function-per-file": string;
@@ -27,6 +28,7 @@ declare function createAstroConfigs(pluginReference: unknown): ({
27
28
  rules: {
28
29
  "skapxd/no-ad-hoc-ok-result": string;
29
30
  "skapxd/no-deep-relative-imports": string;
31
+ "skapxd/no-default-export": string;
30
32
  "skapxd/no-promise-chain": string;
31
33
  "skapxd/no-try-catch": string;
32
34
  "skapxd/one-root-function-per-file": string;
@@ -38,6 +38,7 @@ module.exports = __toCommonJS(astro_exports);
38
38
  var baseRules = {
39
39
  "skapxd/no-ad-hoc-ok-result": "error",
40
40
  "skapxd/no-deep-relative-imports": "error",
41
+ "skapxd/no-default-export": "error",
41
42
  "skapxd/no-promise-chain": "error",
42
43
  "skapxd/no-try-catch": "error",
43
44
  "skapxd/one-root-function-per-file": "error",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/astro/index.ts","../../src/shared/configs/base-rules.ts","../../src/shared/configs/create-base-language-options.ts","../../src/shared/configs/create-typed-language-options.ts","../../src/astro/configs.ts"],"sourcesContent":["export { createAstroConfigs } from \"./configs\";\n","export const baseRules = {\n \"skapxd/no-ad-hoc-ok-result\": \"error\",\n \"skapxd/no-deep-relative-imports\": \"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 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","import {\n baseRules,\n createBaseLanguageOptions,\n createTypedLanguageOptions,\n} from \"#/shared/configs\";\n\nexport function createAstroConfigs(pluginReference: unknown) {\n const baseLanguageOptions = createBaseLanguageOptions();\n const typedLanguageOptions = createTypedLanguageOptions();\n\n return [\n {\n files: [\"src/**/*.{ts,tsx}\"],\n languageOptions: baseLanguageOptions,\n name: \"skapxd/astro/base\",\n plugins: { skapxd: pluginReference },\n rules: baseRules,\n },\n // Los .astro no llevan parser propio: lo aporta eslint-plugin-astro,\n // que el consumidor debe tener configurado.\n {\n files: [\"src/**/*.astro\"],\n name: \"skapxd/astro/astro-files\",\n plugins: { skapxd: pluginReference },\n rules: baseRules,\n },\n {\n files: [\"src/**/*.{ts,tsx}\"],\n languageOptions: typedLanguageOptions,\n name: \"skapxd/astro/typescript\",\n plugins: { skapxd: pluginReference },\n rules: {\n \"skapxd/await-requires-result\": \"error\",\n \"skapxd/result-error-requires-cause\": \"error\",\n },\n },\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAO,IAAM,YAAY;AAAA,EACvB,8BAA8B;AAAA,EAC9B,mCAAmC;AAAA,EACnC,2BAA2B;AAAA,EAC3B,uBAAuB;AAAA,EACvB,qCAAqC;AAAA,EACrC,4BAA4B;AAAA,EAC5B,sCAAsC;AACxC;;;ACRA,+BAAqB;AAId,SAAS,4BAA4B;AAC1C,SAAO;AAAA,IACL,QAAQ,yBAAAA,QAAS;AAAA,EACnB;AACF;;;ACRA,IAAAC,4BAAqB;AAEd,SAAS,6BAA6B;AAC3C,SAAO;AAAA;AAAA;AAAA,IAGL,QAAQ,0BAAAC,QAAS;AAAA,IACjB,eAAe;AAAA,MACb,gBAAgB;AAAA,IAClB;AAAA,EACF;AACF;;;ACLO,SAAS,mBAAmB,iBAA0B;AAC3D,QAAM,sBAAsB,0BAA0B;AACtD,QAAM,uBAAuB,2BAA2B;AAExD,SAAO;AAAA,IACL;AAAA,MACE,OAAO,CAAC,mBAAmB;AAAA,MAC3B,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,IACT;AAAA;AAAA;AAAA,IAGA;AAAA,MACE,OAAO,CAAC,gBAAgB;AAAA,MACxB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,IACT;AAAA,IACA;AAAA,MACE,OAAO,CAAC,mBAAmB;AAAA,MAC3B,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,gCAAgC;AAAA,QAChC,sCAAsC;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AACF;","names":["tseslint","import_typescript_eslint","tseslint"]}
1
+ {"version":3,"sources":["../../src/astro/index.ts","../../src/shared/configs/base-rules.ts","../../src/shared/configs/create-base-language-options.ts","../../src/shared/configs/create-typed-language-options.ts","../../src/astro/configs.ts"],"sourcesContent":["export { createAstroConfigs } from \"./configs\";\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-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 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","import {\n baseRules,\n createBaseLanguageOptions,\n createTypedLanguageOptions,\n} from \"#/shared/configs\";\n\nexport function createAstroConfigs(pluginReference: unknown) {\n const baseLanguageOptions = createBaseLanguageOptions();\n const typedLanguageOptions = createTypedLanguageOptions();\n\n return [\n {\n files: [\"src/**/*.{ts,tsx}\"],\n languageOptions: baseLanguageOptions,\n name: \"skapxd/astro/base\",\n plugins: { skapxd: pluginReference },\n rules: baseRules,\n },\n // Los .astro no llevan parser propio: lo aporta eslint-plugin-astro,\n // que el consumidor debe tener configurado.\n {\n files: [\"src/**/*.astro\"],\n name: \"skapxd/astro/astro-files\",\n plugins: { skapxd: pluginReference },\n rules: baseRules,\n },\n {\n files: [\"src/**/*.{ts,tsx}\"],\n languageOptions: typedLanguageOptions,\n name: \"skapxd/astro/typescript\",\n plugins: { skapxd: pluginReference },\n rules: {\n \"skapxd/await-requires-result\": \"error\",\n \"skapxd/result-error-requires-cause\": \"error\",\n },\n },\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAO,IAAM,YAAY;AAAA,EACvB,8BAA8B;AAAA,EAC9B,mCAAmC;AAAA,EACnC,4BAA4B;AAAA,EAC5B,2BAA2B;AAAA,EAC3B,uBAAuB;AAAA,EACvB,qCAAqC;AAAA,EACrC,4BAA4B;AAAA,EAC5B,sCAAsC;AACxC;;;ACTA,+BAAqB;AAId,SAAS,4BAA4B;AAC1C,SAAO;AAAA,IACL,QAAQ,yBAAAA,QAAS;AAAA,EACnB;AACF;;;ACRA,IAAAC,4BAAqB;AAEd,SAAS,6BAA6B;AAC3C,SAAO;AAAA;AAAA;AAAA,IAGL,QAAQ,0BAAAC,QAAS;AAAA,IACjB,eAAe;AAAA,MACb,gBAAgB;AAAA,IAClB;AAAA,EACF;AACF;;;ACLO,SAAS,mBAAmB,iBAA0B;AAC3D,QAAM,sBAAsB,0BAA0B;AACtD,QAAM,uBAAuB,2BAA2B;AAExD,SAAO;AAAA,IACL;AAAA,MACE,OAAO,CAAC,mBAAmB;AAAA,MAC3B,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,IACT;AAAA;AAAA;AAAA,IAGA;AAAA,MACE,OAAO,CAAC,gBAAgB;AAAA,MACxB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,IACT;AAAA,IACA;AAAA,MACE,OAAO,CAAC,mBAAmB;AAAA,MAC3B,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,gCAAgC;AAAA,QAChC,sCAAsC;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AACF;","names":["tseslint","import_typescript_eslint","tseslint"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createAstroConfigs
3
- } from "../chunk-7H7DRTP2.mjs";
4
- import "../chunk-RWQGOTVZ.mjs";
3
+ } from "../chunk-OVPAO7Q3.mjs";
4
+ import "../chunk-LDAR6XT6.mjs";
5
5
  export {
6
6
  createAstroConfigs
7
7
  };
@@ -1,10 +1,18 @@
1
+ import {
2
+ nextAppMetadataFileStems,
3
+ nextAppRouteSegmentFileStems
4
+ } from "./chunk-HB755SJQ.mjs";
1
5
  import {
2
6
  baseRules,
3
7
  createBaseLanguageOptions,
4
8
  createTypedLanguageOptions
5
- } from "./chunk-RWQGOTVZ.mjs";
9
+ } from "./chunk-LDAR6XT6.mjs";
6
10
 
7
11
  // src/next/configs.ts
12
+ var nextDefaultExportFileGlob = `{${[
13
+ ...nextAppRouteSegmentFileStems,
14
+ ...nextAppMetadataFileStems
15
+ ].join(",")}}.{js,jsx,ts,tsx}`;
8
16
  function createNextConfigs(pluginReference) {
9
17
  const baseLanguageOptions = createBaseLanguageOptions();
10
18
  const typedLanguageOptions = createTypedLanguageOptions();
@@ -13,7 +21,15 @@ function createNextConfigs(pluginReference) {
13
21
  languageOptions: baseLanguageOptions,
14
22
  name: "skapxd/next/base",
15
23
  plugins: { skapxd: pluginReference },
16
- rules: baseRules
24
+ rules: {
25
+ ...baseRules,
26
+ "skapxd/no-default-export": [
27
+ "error",
28
+ {
29
+ allowFilePatterns: [nextDefaultExportFileGlob]
30
+ }
31
+ ]
32
+ }
17
33
  },
18
34
  {
19
35
  files: ["src/app/api/**/*.{ts,tsx}", "src/server/**/*.{ts,tsx}"],
@@ -53,4 +69,4 @@ function createNextConfigs(pluginReference) {
53
69
  export {
54
70
  createNextConfigs
55
71
  };
56
- //# sourceMappingURL=chunk-QJQ2E2NG.mjs.map
72
+ //# sourceMappingURL=chunk-72XIHMY7.mjs.map
@@ -0,0 +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,3 +1,8 @@
1
+ import {
2
+ nextAppMetadataFileStems,
3
+ nextAppRouteSegmentFileStems
4
+ } from "./chunk-HB755SJQ.mjs";
5
+
1
6
  // src/utils/get-file-name.ts
2
7
  function getFileName(filename) {
3
8
  return filename.split(/[\\/]/).at(-1) ?? filename;
@@ -47,33 +52,6 @@ function isInsideAppDirectory(filename) {
47
52
  return getPathParts(filename).includes("app");
48
53
  }
49
54
 
50
- // src/constants/next-app-metadata-file-stems.ts
51
- var nextAppMetadataFileStems = [
52
- "apple-icon",
53
- "icon",
54
- "manifest",
55
- "opengraph-image",
56
- "robots",
57
- "sitemap",
58
- "twitter-image"
59
- ];
60
-
61
- // src/constants/next-app-route-segment-file-stems.ts
62
- var nextAppRouteSegmentFileStems = [
63
- "default",
64
- "error",
65
- "forbidden",
66
- "global-error",
67
- "global-not-found",
68
- "layout",
69
- "loading",
70
- "not-found",
71
- "page",
72
- "route",
73
- "template",
74
- "unauthorized"
75
- ];
76
-
77
55
  // src/constants/next-project-root-file-stems.ts
78
56
  var nextProjectRootFileStems = [
79
57
  "instrumentation",
@@ -580,6 +558,16 @@ function isSkapxdResultOrPromiseResultType(type, typeContext) {
580
558
  return Boolean(promisedType && isSkapxdResultType(promisedType, typeContext));
581
559
  }
582
560
 
561
+ // src/utils/matches-any-glob.ts
562
+ import picomatch from "picomatch";
563
+ function matchesAnyGlob(filePath, globs) {
564
+ const normalized = filePath.replaceAll("\\", "/");
565
+ return globs.some((glob) => {
566
+ const anchored = glob.startsWith("/") || glob.startsWith("**") ? glob : `**/${glob}`;
567
+ return picomatch(anchored)(normalized);
568
+ });
569
+ }
570
+
583
571
  // src/utils/matches-any-pattern.ts
584
572
  function matchesAnyPattern(value, patterns) {
585
573
  return patterns.some((pattern) => new RegExp(pattern).test(value));
@@ -642,7 +630,7 @@ var asyncFunctionsReturnResult = {
642
630
  return isPromiseOfResultType(annotation, options);
643
631
  }
644
632
  function reportIfInvalidAsyncReturn(node, name, reportNode = node) {
645
- if (!node.async || matchesAnyPattern(filename, options.allowFilePatterns)) {
633
+ if (!node.async || matchesAnyGlob(filename, options.allowFilePatterns)) {
646
634
  return;
647
635
  }
648
636
  const functionName = name ?? "anonymous";
@@ -939,7 +927,7 @@ var awaitRequiresResult = {
939
927
  }
940
928
  return {
941
929
  AwaitExpression(node) {
942
- if (matchesAnyPattern(filename, options.allowFilePatterns)) {
930
+ if (matchesAnyGlob(filename, options.allowFilePatterns)) {
943
931
  return;
944
932
  }
945
933
  if (typeContext && isSkapxdResultOrPromiseResultExpression(node.argument, typeContext)) {
@@ -1337,6 +1325,96 @@ var noDeepRelativeImports = {
1337
1325
  }
1338
1326
  };
1339
1327
 
1328
+ // src/constants/default-export-allowed-file-patterns.ts
1329
+ var defaultExportAllowedFilePatterns = [
1330
+ "*.config.*",
1331
+ "*.stories.*"
1332
+ ];
1333
+
1334
+ // src/utils/get-no-default-export-options.ts
1335
+ function getNoDefaultExportOptions(options = {}) {
1336
+ return {
1337
+ allowFilePatterns: [
1338
+ ...defaultExportAllowedFilePatterns,
1339
+ ...options.allowFilePatterns ?? []
1340
+ ]
1341
+ };
1342
+ }
1343
+
1344
+ // src/rules/no-default-export.ts
1345
+ var noDefaultExport = {
1346
+ meta: {
1347
+ type: "suggestion",
1348
+ docs: {
1349
+ description: "Prohibe export default; un export nombrado hace del nombre el contrato del modulo."
1350
+ },
1351
+ messages: {
1352
+ noDefaultExport: 'No uses `export default`: un export nombrado hace el simbolo renombrable con el IDE, grepeable y estable en autoimports. Si este archivo es un entrypoint donde un framework o tool exige el default, agrega su glob en `allowFilePatterns` de skapxd/no-default-export (p. ej. "+page.ts").'
1353
+ },
1354
+ schema: [
1355
+ {
1356
+ additionalProperties: false,
1357
+ properties: {
1358
+ allowFilePatterns: {
1359
+ items: { type: "string" },
1360
+ type: "array"
1361
+ }
1362
+ },
1363
+ type: "object"
1364
+ }
1365
+ ]
1366
+ },
1367
+ create(context) {
1368
+ const options = getNoDefaultExportOptions(context.options[0]);
1369
+ const filename = context.filename ?? context.getFilename();
1370
+ if (matchesAnyGlob(filename, options.allowFilePatterns)) {
1371
+ return {};
1372
+ }
1373
+ return {
1374
+ ExportDefaultDeclaration(node) {
1375
+ context.report({ messageId: "noDefaultExport", node });
1376
+ },
1377
+ // Cubre la forma indirecta: `export { algo as default }`.
1378
+ ExportNamedDeclaration(node) {
1379
+ for (const specifier of node.specifiers ?? []) {
1380
+ const exportedName = specifier.exported?.name ?? specifier.exported?.value;
1381
+ if (exportedName === "default") {
1382
+ context.report({ messageId: "noDefaultExport", node: specifier });
1383
+ }
1384
+ }
1385
+ }
1386
+ };
1387
+ }
1388
+ };
1389
+
1390
+ // src/utils/get-no-functions-inside-components-options.ts
1391
+ function getNoFunctionsInsideComponentsOptions(options = {}) {
1392
+ return {
1393
+ allowArrayMapCallbacks: options.allowArrayMapCallbacks ?? true,
1394
+ allowJsxCallbacks: options.allowJsxCallbacks ?? true
1395
+ };
1396
+ }
1397
+
1398
+ // src/utils/is-array-map-callback.ts
1399
+ function isArrayMapCallback(node) {
1400
+ const parent = node.parent;
1401
+ if (parent?.type !== "CallExpression" || parent.arguments[0] !== node) {
1402
+ return false;
1403
+ }
1404
+ const callee = parent.callee;
1405
+ return callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier" && callee.property.name === "map";
1406
+ }
1407
+
1408
+ // src/utils/is-expression-arrow-function.ts
1409
+ function isExpressionArrowFunction(node) {
1410
+ return node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement";
1411
+ }
1412
+
1413
+ // src/utils/is-jsx-attribute-callback.ts
1414
+ function isJsxAttributeCallback(node) {
1415
+ return node.parent?.type === "JSXExpressionContainer" && node.parent.parent?.type === "JSXAttribute";
1416
+ }
1417
+
1340
1418
  // src/rules/no-functions-inside-components.ts
1341
1419
  var noFunctionsInsideComponents = {
1342
1420
  meta: {
@@ -1345,19 +1423,41 @@ var noFunctionsInsideComponents = {
1345
1423
  description: "Prohibe definir funciones dentro de componentes React; se recrean en cada render."
1346
1424
  },
1347
1425
  messages: {
1348
- functionInsideComponent: "No definas funciones dentro del componente `{{component}}`: se recrean en cada render. Muevela a un hook (`useX`) o a un helper fuera del componente."
1426
+ functionInsideComponent: "No definas funciones dentro del componente `{{component}}`: se recrean en cada render. Muevela a un hook (`useX`) o a un helper fuera del componente. Los callbacks inline de JSX y .map solo se permiten como flecha de expresion (sin cuerpo `{ }`): un bloque ya da pie a ifs y logica que pertenece fuera."
1349
1427
  },
1350
- schema: []
1428
+ schema: [
1429
+ {
1430
+ additionalProperties: false,
1431
+ properties: {
1432
+ allowArrayMapCallbacks: { type: "boolean" },
1433
+ allowJsxCallbacks: { type: "boolean" }
1434
+ },
1435
+ type: "object"
1436
+ }
1437
+ ]
1351
1438
  },
1352
1439
  create(context) {
1440
+ const options = getNoFunctionsInsideComponentsOptions(context.options[0]);
1353
1441
  function isComponentFunction(node) {
1354
1442
  return isFunctionNode(node) && isPascalCaseName(getFunctionName(node));
1355
1443
  }
1444
+ function isAllowedInlineCallback(node) {
1445
+ if (!isExpressionArrowFunction(node)) {
1446
+ return false;
1447
+ }
1448
+ if (options.allowJsxCallbacks && isJsxAttributeCallback(node)) {
1449
+ return true;
1450
+ }
1451
+ return options.allowArrayMapCallbacks && isArrayMapCallback(node);
1452
+ }
1356
1453
  function reportIfInsideComponent(node) {
1357
1454
  const enclosingFunction = getContainingFunction(node);
1358
1455
  if (!enclosingFunction || !isComponentFunction(enclosingFunction)) {
1359
1456
  return;
1360
1457
  }
1458
+ if (isAllowedInlineCallback(node)) {
1459
+ return;
1460
+ }
1361
1461
  context.report({
1362
1462
  data: {
1363
1463
  component: getFunctionName(enclosingFunction)
@@ -1546,6 +1646,7 @@ var rules = {
1546
1646
  "result-error-requires-cause": resultErrorRequiresCause,
1547
1647
  "max-hook-size": maxHookSize,
1548
1648
  "no-deep-relative-imports": noDeepRelativeImports,
1649
+ "no-default-export": noDefaultExport,
1549
1650
  "no-functions-inside-components": noFunctionsInsideComponents,
1550
1651
  "no-try-catch": noTryCatch,
1551
1652
  "prefer-ts-pattern": preferTsPattern,
@@ -1556,4 +1657,4 @@ var rules = {
1556
1657
  export {
1557
1658
  rules
1558
1659
  };
1559
- //# sourceMappingURL=chunk-QNSHGNVQ.mjs.map
1660
+ //# sourceMappingURL=chunk-G6TJUBFT.mjs.map