@skapxd/eslint-opinionated 0.16.0 → 0.17.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
@@ -748,6 +748,19 @@ export default [
748
748
  ];
749
749
  ```
750
750
 
751
+ Para librerías npm escritas en TypeScript (tsup o equivalente). Trae las
752
+ bases completas + el set type-driven (tipado, con `projectService`) +
753
+ `await-requires-result` + el contrato de empaquetado:
754
+
755
+ - `skapxd/package-requires-typed-exports` — los `exports` del package.json
756
+ cablean los tipos **por condición** (`import` → `.d.mts`, `require` →
757
+ `.d.ts`); el `types` único por subpath es el bug "FalseCJS".
758
+ - `skapxd/untrusted-module-requires-adapter` — inerte hasta que declares tu
759
+ inventario de paquetes con tipos mentirosos (ver su sección).
760
+
761
+ **Este mismo repo se lintea con este preset** — dogfood: la regla de exports
762
+ nos obligó a corregir nuestro propio package.json al nacer.
763
+
751
764
  ### Strict (sin escape via `eslint-disable`)
752
765
 
753
766
  Un prompt o un agente puede saltarse cualquier regla con
@@ -831,7 +844,9 @@ de cada regla):
831
844
  | `no-runtime-state-guard` | `allowFilePatterns` (globs) |
832
845
  | `no-tunnel-props` | `allowFilePatterns` (globs), `allowPropPatterns` (regex) |
833
846
  | `prefer-abort-signal` | `allowFilePatterns` (globs), `effectNames` (default `["useEffect", "useLayoutEffect"]`) |
847
+ | `package-requires-typed-exports` | `allowFilePatterns` (globs), `anchorFilePatterns` (default `src/index.ts(x)`, `src/main.ts`) |
834
848
  | `prefer-tagged-union-state` | `allowFilePatterns` (globs), `loadingPatterns` (regex, en minúsculas), `errorPatterns` (regex, en minúsculas) |
849
+ | `untrusted-module-requires-adapter` | `modules` (default `[]` — inerte), `adapterFilePatterns` (globs), `allowFilePatterns` (globs) |
835
850
  | `requires-strict-tsconfig` | `allowFilePatterns` (globs), `anchorFilePatterns` (globs), `requiredCompilerOptions` |
836
851
  | `result-error-requires-handling` | `allowFilePatterns` (globs) |
837
852
 
@@ -883,6 +898,8 @@ matchea en cualquier carpeta). Las 7 reglas restantes no tienen opciones: su
883
898
  | `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
884
899
  | `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
885
900
  | `skapxd/prefer-ts-pattern` | Prohíbe `switch` y ternarios anidados; usa `match()` de ts-pattern. |
901
+ | `skapxd/package-requires-typed-exports` | Los `exports` del package.json declaran `types` por condición (`import` → `.d.mts`, `require` → `.d.ts`): mata el bug FalseCJS. Preset `package`. |
902
+ | `skapxd/untrusted-module-requires-adapter` | Los paquetes con tipos mentirosos (@types desfasados) solo se importan desde su adaptador: la mentira vive en UN archivo. Preset `package`. |
886
903
  | `skapxd/no-jsx-ternary-null` | Prefiere `cond && <El />` sobre `cond ? <El /> : null` en JSX. |
887
904
 
888
905
  ### `skapxd/one-root-function-per-file`
@@ -2229,6 +2246,68 @@ no puede traer `signal`.
2229
2246
  `effectNames` permite cubrir wrappers propios (`["useEffect",
2230
2247
  "useLayoutEffect", "useIsomorphicEffect"]`).
2231
2248
 
2249
+ ### `skapxd/package-requires-typed-exports`
2250
+
2251
+ El contrato de empaquetado de una librería TypeScript dual (ESM + CJS): cada
2252
+ condición del mapa `exports` declara **sus propios tipos**, del sabor
2253
+ correcto.
2254
+
2255
+ ```jsonc
2256
+ "exports": {
2257
+ ".": {
2258
+ "import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" },
2259
+ "require": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
2260
+ }
2261
+ }
2262
+ ```
2263
+
2264
+ El antipatrón que mata es el **"FalseCJS"** (el hallazgo #1 de
2265
+ [arethetypeswrong](https://arethetypeswrong.github.io)): un `types` único por
2266
+ subpath apuntando al `.d.ts` — los consumidores ESM con
2267
+ `moduleResolution: node16` reciben tipos CJS y el contrato miente en la
2268
+ frontera más pública que tiene una librería. tsup con `dts: true` ya genera
2269
+ los dos sabores (`.d.mts` y `.d.ts`); esta regla verifica que el package.json
2270
+ de verdad los cablee y que los archivos existan en disco. Anclada al
2271
+ entrypoint (`src/index.ts` por defecto): un reporte por paquete.
2272
+
2273
+ Dogfood: esta regla nació reportando a este mismo repo — nuestros `exports`
2274
+ tenían el bug y el lint no volvió a verde hasta corregirlos.
2275
+
2276
+ ### `skapxd/untrusted-module-requires-adapter`
2277
+
2278
+ ¿Qué pasa cuando los tipos de un paquete de terceros **mienten**? El clásico:
2279
+ un paquete escrito en JS cuyos tipos viven aparte (`@types/...`) y van
2280
+ desfasados del runtime real, o índices que juran nunca devolver `undefined`.
2281
+ Todo el sistema de este paquete descansa en que el tipo dice la verdad
2282
+ (`no-impossible-branch` le cree ciegamente) — un tipo mentiroso envenena cada
2283
+ regla type-aware que lo toque.
2284
+
2285
+ El playbook, en orden:
2286
+
2287
+ 1. **Armadura de tsconfig primero**: `noUncheckedIndexedAccess` corrige de
2288
+ raíz la clase más común de mentira (index signatures optimistas) sin
2289
+ tocar al tercero — `requires-strict-tsconfig` ya lo exige.
2290
+ 2. **Frontera anticorrupción** (lo que esta regla impone): declara el módulo
2291
+ como no confiable y enciérralo tras UN adaptador. El adaptador importa el
2292
+ paquete, re-declara los tipos honestos (lo que el runtime de verdad
2293
+ devuelve) y exporta esa versión. El resto del código importa el adaptador
2294
+ y razona con tipos veraces — la mentira queda en un archivo auditable.
2295
+ 3. **`@ts-expect-error` con descripción** dentro del adaptador si hace falta
2296
+ forzar la corrección — es la puerta que `no-silenced-compiler` deja
2297
+ abierta, declarada y con porqué.
2298
+ 4. **Arregla el upstream**: PR a DefinitelyTyped. Mientras llega, los pasos
2299
+ 1-3 te protegen.
2300
+
2301
+ ```js
2302
+ "skapxd/untrusted-module-requires-adapter": ["error", {
2303
+ adapterFilePatterns: ["src/lib/xlsx-adapter.ts"],
2304
+ modules: ["xlsx"],
2305
+ }]
2306
+ ```
2307
+
2308
+ Sin `modules` declarados la regla es inerte: el inventario de sospechosos es
2309
+ una decisión del proyecto, no una adivinanza del linter (axioma A5).
2310
+
2232
2311
  ### `skapxd/no-jsx-ternary-null`
2233
2312
 
2234
2313
  Cuando renderizas JSX condicional y una rama del ternario es `null`, prefiere la
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createAstroConfigs
3
- } from "../chunk-7OIMY5TI.mjs";
4
- import "../chunk-4FQ7SFU4.mjs";
3
+ } from "../chunk-GSQWHSHV.mjs";
4
+ import "../chunk-QDQUU6QK.mjs";
5
5
  export {
6
6
  createAstroConfigs
7
7
  };
@@ -2,7 +2,7 @@ import {
2
2
  baseRules,
3
3
  createTypedLanguageOptions,
4
4
  typeDrivenRules
5
- } from "./chunk-4FQ7SFU4.mjs";
5
+ } from "./chunk-QDQUU6QK.mjs";
6
6
 
7
7
  // src/constants/nest-entrypoint-file-patterns.ts
8
8
  var nestEntrypointFilePatterns = [
@@ -130,4 +130,4 @@ function createNestConfigs(pluginReference) {
130
130
  export {
131
131
  createNestConfigs
132
132
  };
133
- //# sourceMappingURL=chunk-LSLLVT64.mjs.map
133
+ //# sourceMappingURL=chunk-3GEFHNU7.mjs.map
@@ -6,7 +6,7 @@ import {
6
6
  baseRules,
7
7
  createBaseLanguageOptions,
8
8
  createTypedLanguageOptions
9
- } from "./chunk-4FQ7SFU4.mjs";
9
+ } from "./chunk-QDQUU6QK.mjs";
10
10
 
11
11
  // src/next/configs.ts
12
12
  var nextDefaultExportFileGlob = `{${[
@@ -71,4 +71,4 @@ function createNextConfigs(pluginReference) {
71
71
  export {
72
72
  createNextConfigs
73
73
  };
74
- //# sourceMappingURL=chunk-YRWX3POD.mjs.map
74
+ //# sourceMappingURL=chunk-54EOEKYS.mjs.map
@@ -2,7 +2,7 @@ import {
2
2
  baseRules,
3
3
  createBaseLanguageOptions,
4
4
  createTypedLanguageOptions
5
- } from "./chunk-4FQ7SFU4.mjs";
5
+ } from "./chunk-QDQUU6QK.mjs";
6
6
 
7
7
  // src/astro/configs.ts
8
8
  function createAstroConfigs(pluginReference) {
@@ -41,4 +41,4 @@ function createAstroConfigs(pluginReference) {
41
41
  export {
42
42
  createAstroConfigs
43
43
  };
44
- //# sourceMappingURL=chunk-7OIMY5TI.mjs.map
44
+ //# sourceMappingURL=chunk-GSQWHSHV.mjs.map
@@ -2761,8 +2761,14 @@ var noEmoji = {
2761
2761
 
2762
2762
  // src/utils/wrap-tseslint-rule.ts
2763
2763
  import tseslint from "typescript-eslint";
2764
+ var upstreamRules = tseslint.plugin.rules;
2764
2765
  function wrapTseslintRule(upstreamRuleName, { description, messages }) {
2765
- const original = tseslint.plugin.rules[upstreamRuleName];
2766
+ const original = upstreamRules[upstreamRuleName];
2767
+ if (!original) {
2768
+ throw new Error(
2769
+ `wrapTseslintRule: la regla "${upstreamRuleName}" no existe en typescript-eslint.`
2770
+ );
2771
+ }
2766
2772
  return {
2767
2773
  ...original,
2768
2774
  meta: {
@@ -2856,7 +2862,10 @@ function getNoTunnelPropsOptions(options = {}) {
2856
2862
 
2857
2863
  // src/utils/get-object-pattern-prop-names.ts
2858
2864
  function getObjectPatternPropNames(pattern) {
2859
- const result = { propNames: [], restName: null };
2865
+ const result = {
2866
+ propNames: [],
2867
+ restName: null
2868
+ };
2860
2869
  if (pattern?.type !== "ObjectPattern") {
2861
2870
  return result;
2862
2871
  }
@@ -3494,7 +3503,7 @@ function readResolvedTsconfig(tsconfigPath) {
3494
3503
  if (!parsed.ok || !parsed.value) {
3495
3504
  return null;
3496
3505
  }
3497
- return parsed.value.options ?? null;
3506
+ return parsed.value.options;
3498
3507
  }
3499
3508
 
3500
3509
  // src/rules/requires-strict-tsconfig.ts
@@ -3841,6 +3850,212 @@ var noRuntimeStateGuard = {
3841
3850
  }
3842
3851
  };
3843
3852
 
3853
+ // src/rules/package-requires-typed-exports.ts
3854
+ import { readFileSync as readFileSync2 } from "fs";
3855
+ import { dirname as dirname6, resolve as resolve3 } from "path";
3856
+ import { trySafe as trySafe4 } from "@skapxd/result";
3857
+
3858
+ // src/utils/get-typed-exports-options.ts
3859
+ function getTypedExportsOptions(options = {}) {
3860
+ return {
3861
+ allowFilePatterns: options.allowFilePatterns ?? [],
3862
+ // Anclada al entrypoint de la libreria: un reporte por paquete.
3863
+ anchorFilePatterns: options.anchorFilePatterns ?? [
3864
+ "src/index.ts",
3865
+ "src/index.tsx",
3866
+ "src/main.ts"
3867
+ ]
3868
+ };
3869
+ }
3870
+
3871
+ // src/utils/get-untyped-export-conditions.ts
3872
+ import { existsSync as existsSync3 } from "fs";
3873
+ import { join as join3 } from "path";
3874
+ function getUntypedExportConditions(exportsField, packageDir) {
3875
+ const violations = [];
3876
+ const entries = "import" in exportsField || "require" in exportsField ? [[".", exportsField]] : Object.entries(exportsField);
3877
+ for (const [subpath, value] of entries) {
3878
+ if (subpath === "./package.json") {
3879
+ continue;
3880
+ }
3881
+ if (typeof value === "string") {
3882
+ violations.push({ kind: "untyped", condition: "todas", subpath });
3883
+ continue;
3884
+ }
3885
+ for (const condition of ["import", "require"]) {
3886
+ if (!(condition in value)) {
3887
+ continue;
3888
+ }
3889
+ const target = value[condition];
3890
+ if (typeof target !== "object" || typeof target.types !== "string") {
3891
+ violations.push({ kind: "untyped", condition, subpath });
3892
+ continue;
3893
+ }
3894
+ const expectsEsmTypes = condition === "import";
3895
+ const flavorOk = expectsEsmTypes ? target.types.endsWith(".d.mts") : target.types.endsWith(".d.ts") || target.types.endsWith(".d.cts");
3896
+ if (!flavorOk) {
3897
+ violations.push({ kind: "wrong-flavor", condition, subpath });
3898
+ continue;
3899
+ }
3900
+ if (!existsSync3(join3(packageDir, target.types))) {
3901
+ violations.push({ kind: "missing-file", condition, subpath });
3902
+ }
3903
+ }
3904
+ }
3905
+ return violations;
3906
+ }
3907
+
3908
+ // src/rules/package-requires-typed-exports.ts
3909
+ var kindMessages = {
3910
+ "missing-file": "missingTypesFile",
3911
+ untyped: "untypedCondition",
3912
+ "wrong-flavor": "wrongTypesFlavor"
3913
+ };
3914
+ var packageRequiresTypedExports = {
3915
+ meta: {
3916
+ type: "problem",
3917
+ docs: {
3918
+ description: "El package.json de una libreria debe cablear los tipos POR CONDICION en exports: import \u2192 .d.mts, require \u2192 .d.ts. Un types unico es el bug FalseCJS."
3919
+ },
3920
+ messages: {
3921
+ missingExports: "El package.json no tiene campo `exports`. Sin el, Node y TypeScript resuelven por heuristicas viejas y los consumidores ESM/CJS pueden recibir artefactos cruzados. Declara el mapa de exports con `types` por condicion.",
3922
+ missingTypesFile: 'En exports["{{subpath}}"].{{condition}}, el archivo de `types` declarado no existe en disco. El contrato apunta a un fantasma: o falta el build (dts) o la ruta esta mal escrita.',
3923
+ unreadablePackageJson: "No encontre un package.json legible subiendo desde este archivo. Esta regla valida el contrato de tipos de la libreria y necesita leerlo.",
3924
+ untypedCondition: 'exports["{{subpath}}"] \u2192 `{{condition}}` no declara su propio `types`. Un `types` unico a nivel del subpath es el bug "FalseCJS": el consumidor ESM con moduleResolution node16 recibe los tipos CJS. Cada condicion declara los suyos \u2014 `import` como objeto con `types: ./dist/x.d.mts` y `default: ./dist/x.mjs`; `require` como objeto con `types: ./dist/x.d.ts` y `default: ./dist/x.js`.',
3925
+ wrongTypesFlavor: 'exports["{{subpath}}"].{{condition}} apunta a tipos del formato equivocado: `import` exige `.d.mts` (tipos ESM) y `require` exige `.d.ts`/`.d.cts` (tipos CJS). tsup con dts: true ya genera ambos sabores.'
3926
+ },
3927
+ schema: [
3928
+ {
3929
+ additionalProperties: false,
3930
+ properties: {
3931
+ allowFilePatterns: {
3932
+ items: { type: "string" },
3933
+ type: "array"
3934
+ },
3935
+ anchorFilePatterns: {
3936
+ items: { type: "string" },
3937
+ type: "array"
3938
+ }
3939
+ },
3940
+ type: "object"
3941
+ }
3942
+ ]
3943
+ },
3944
+ create(context) {
3945
+ const options = getTypedExportsOptions(context.options[0]);
3946
+ const filename = context.filename ?? context.getFilename();
3947
+ if (matchesAnyGlob(filename, options.allowFilePatterns) || !matchesAnyGlob(filename, options.anchorFilePatterns)) {
3948
+ return {};
3949
+ }
3950
+ return {
3951
+ Program(node) {
3952
+ const absoluteFilename = resolve3(context.cwd ?? process.cwd(), filename);
3953
+ const packageJsonPath = findProjectFile(
3954
+ dirname6(absoluteFilename),
3955
+ "package.json"
3956
+ );
3957
+ const parsed = packageJsonPath ? trySafe4(() => JSON.parse(readFileSync2(packageJsonPath, "utf8"))) : null;
3958
+ if (!parsed || !parsed.ok) {
3959
+ context.report({ messageId: "unreadablePackageJson", node });
3960
+ return;
3961
+ }
3962
+ const exportsField = parsed.value.exports;
3963
+ if (!exportsField || typeof exportsField !== "object") {
3964
+ context.report({ messageId: "missingExports", node });
3965
+ return;
3966
+ }
3967
+ const violations = getUntypedExportConditions(
3968
+ exportsField,
3969
+ dirname6(packageJsonPath)
3970
+ );
3971
+ for (const violation of violations) {
3972
+ context.report({
3973
+ data: {
3974
+ condition: violation.condition,
3975
+ subpath: violation.subpath
3976
+ },
3977
+ messageId: kindMessages[violation.kind],
3978
+ node
3979
+ });
3980
+ }
3981
+ }
3982
+ };
3983
+ }
3984
+ };
3985
+
3986
+ // src/utils/get-untrusted-module-options.ts
3987
+ function getUntrustedModuleOptions(options = {}) {
3988
+ return {
3989
+ // Globs de los archivos adaptador: el UNICO lugar desde donde se permite
3990
+ // importar los modulos declarados como no confiables.
3991
+ adapterFilePatterns: options.adapterFilePatterns ?? [],
3992
+ allowFilePatterns: options.allowFilePatterns ?? [],
3993
+ // Inventario de paquetes cuyos tipos mienten (p. ej. @types desfasados).
3994
+ // Vacio por defecto: la regla es inerte hasta que el proyecto declara
3995
+ // sus sospechosos.
3996
+ modules: options.modules ?? []
3997
+ };
3998
+ }
3999
+
4000
+ // src/rules/untrusted-module-requires-adapter.ts
4001
+ var untrustedModuleRequiresAdapter = {
4002
+ meta: {
4003
+ type: "problem",
4004
+ docs: {
4005
+ description: "Los modulos declarados como no confiables (tipos que mienten) solo se importan desde su adaptador: la mentira vive en un archivo, no en todos."
4006
+ },
4007
+ messages: {
4008
+ untrustedImport: "`{{moduleSource}}` esta declarado como modulo de tipos NO confiables: importalo solo desde su adaptador ({{adapters}}). El adaptador es la frontera anticorrupcion: re-declara ahi los tipos honestos (lo que el runtime de verdad devuelve) y exporta esa version; el resto del codigo importa el adaptador y razona con tipos veraces. Asi la mentira de terceros vive en UN archivo auditable, no regada por el proyecto."
4009
+ },
4010
+ schema: [
4011
+ {
4012
+ additionalProperties: false,
4013
+ properties: {
4014
+ adapterFilePatterns: {
4015
+ items: { type: "string" },
4016
+ type: "array"
4017
+ },
4018
+ allowFilePatterns: {
4019
+ items: { type: "string" },
4020
+ type: "array"
4021
+ },
4022
+ modules: {
4023
+ items: { type: "string" },
4024
+ type: "array"
4025
+ }
4026
+ },
4027
+ type: "object"
4028
+ }
4029
+ ]
4030
+ },
4031
+ create(context) {
4032
+ const options = getUntrustedModuleOptions(context.options[0]);
4033
+ const filename = context.filename ?? context.getFilename();
4034
+ if (options.modules.length === 0 || matchesAnyGlob(filename, options.allowFilePatterns) || matchesAnyGlob(filename, options.adapterFilePatterns)) {
4035
+ return {};
4036
+ }
4037
+ return {
4038
+ ImportDeclaration(node) {
4039
+ const source = node.source.value;
4040
+ const isUntrusted = options.modules.some(
4041
+ (moduleName) => source === moduleName || source.startsWith(`${moduleName}/`)
4042
+ );
4043
+ if (!isUntrusted) {
4044
+ return;
4045
+ }
4046
+ context.report({
4047
+ data: {
4048
+ adapters: options.adapterFilePatterns.join(", ") || "sin definir",
4049
+ moduleSource: source
4050
+ },
4051
+ messageId: "untrustedImport",
4052
+ node
4053
+ });
4054
+ }
4055
+ };
4056
+ }
4057
+ };
4058
+
3844
4059
  // src/shared/rules.ts
3845
4060
  var rules = {
3846
4061
  "class-properties-require-readonly": classPropertiesRequireReadonly,
@@ -3893,10 +4108,12 @@ var rules = {
3893
4108
  "no-jsx-ternary-null": noJsxTernaryNull,
3894
4109
  "no-nested-if": noNestedIf,
3895
4110
  "no-promise-chain": noPromiseChain,
3896
- "no-runtime-state-guard": noRuntimeStateGuard
4111
+ "no-runtime-state-guard": noRuntimeStateGuard,
4112
+ "package-requires-typed-exports": packageRequiresTypedExports,
4113
+ "untrusted-module-requires-adapter": untrustedModuleRequiresAdapter
3897
4114
  };
3898
4115
 
3899
4116
  export {
3900
4117
  rules
3901
4118
  };
3902
- //# sourceMappingURL=chunk-X2DU5BAB.mjs.map
4119
+ //# sourceMappingURL=chunk-PD77UDUY.mjs.map