@skapxd/eslint-opinionated 0.6.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,23 +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
 
767
- **Opciones** (desde v0.6.0) para permitir los dos patrones React idiomáticos
768
- que la versión estricta bloqueaba forzarlos a salir del componente producía
769
- workarounds peores que el problema (`.bind(null, ...)`, adapters artificiales):
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:
770
946
 
771
947
  ```js
772
948
  "skapxd/no-functions-inside-components": ["error", {
773
- allowJsxCallbacks: true, // <button onClick={() => onSelect(id)} />
774
- allowArrayMapCallbacks: true, // {entries.map((entry) => <Entry key={entry.id} />)}
949
+ allowJsxCallbacks: false, // también reporta onClick={() => ...}
950
+ allowArrayMapCallbacks: false, // también reporta items.map((i) => ...)
775
951
  }]
776
952
  ```
777
953
 
778
- Ambas exenciones aplican **solo a funciones anónimas inline en esa posición
779
- exacta**: el valor directo de una prop JSX, o el primer argumento de `.map(...)`.
780
- Un handler con nombre en el cuerpo (`const onClick = () => ...`), un callback de
781
- `useEffect` o un `.forEach` siguen reportándose — la lógica con peso sigue
782
- viviendo fuera del componente.
783
-
784
954
  ### `skapxd/no-try-catch`
785
955
 
786
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,20 +1325,74 @@ var noDeepRelativeImports = {
1337
1325
  }
1338
1326
  };
1339
1327
 
1340
- // src/utils/get-no-functions-inside-components-options.ts
1341
- function getNoFunctionsInsideComponentsOptions(options = {}) {
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 = {}) {
1342
1336
  return {
1343
- allowArrayMapCallbacks: options.allowArrayMapCallbacks ?? false,
1344
- allowJsxCallbacks: options.allowJsxCallbacks ?? false
1337
+ allowFilePatterns: [
1338
+ ...defaultExportAllowedFilePatterns,
1339
+ ...options.allowFilePatterns ?? []
1340
+ ]
1345
1341
  };
1346
1342
  }
1347
1343
 
1348
- // src/utils/is-anonymous-inline-function.ts
1349
- function isAnonymousInlineFunction(node) {
1350
- if (node.type === "ArrowFunctionExpression") {
1351
- return true;
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
+ };
1352
1387
  }
1353
- return node.type === "FunctionExpression" && !node.id;
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
+ };
1354
1396
  }
1355
1397
 
1356
1398
  // src/utils/is-array-map-callback.ts
@@ -1363,6 +1405,11 @@ function isArrayMapCallback(node) {
1363
1405
  return callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier" && callee.property.name === "map";
1364
1406
  }
1365
1407
 
1408
+ // src/utils/is-expression-arrow-function.ts
1409
+ function isExpressionArrowFunction(node) {
1410
+ return node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement";
1411
+ }
1412
+
1366
1413
  // src/utils/is-jsx-attribute-callback.ts
1367
1414
  function isJsxAttributeCallback(node) {
1368
1415
  return node.parent?.type === "JSXExpressionContainer" && node.parent.parent?.type === "JSXAttribute";
@@ -1376,7 +1423,7 @@ var noFunctionsInsideComponents = {
1376
1423
  description: "Prohibe definir funciones dentro de componentes React; se recrean en cada render."
1377
1424
  },
1378
1425
  messages: {
1379
- 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."
1380
1427
  },
1381
1428
  schema: [
1382
1429
  {
@@ -1395,7 +1442,7 @@ var noFunctionsInsideComponents = {
1395
1442
  return isFunctionNode(node) && isPascalCaseName(getFunctionName(node));
1396
1443
  }
1397
1444
  function isAllowedInlineCallback(node) {
1398
- if (!isAnonymousInlineFunction(node)) {
1445
+ if (!isExpressionArrowFunction(node)) {
1399
1446
  return false;
1400
1447
  }
1401
1448
  if (options.allowJsxCallbacks && isJsxAttributeCallback(node)) {
@@ -1599,6 +1646,7 @@ var rules = {
1599
1646
  "result-error-requires-cause": resultErrorRequiresCause,
1600
1647
  "max-hook-size": maxHookSize,
1601
1648
  "no-deep-relative-imports": noDeepRelativeImports,
1649
+ "no-default-export": noDefaultExport,
1602
1650
  "no-functions-inside-components": noFunctionsInsideComponents,
1603
1651
  "no-try-catch": noTryCatch,
1604
1652
  "prefer-ts-pattern": preferTsPattern,
@@ -1609,4 +1657,4 @@ var rules = {
1609
1657
  export {
1610
1658
  rules
1611
1659
  };
1612
- //# sourceMappingURL=chunk-5BA4KA37.mjs.map
1660
+ //# sourceMappingURL=chunk-G6TJUBFT.mjs.map