taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

eslint.config.mjs (9101B)


      1 // This file has been placed into the public domain.
      2 
      3 import { createRequire } from "node:module";
      4 const require = createRequire(import.meta.url);
      5 
      6 import { dirname, join } from "node:path";
      7 import { fileURLToPath } from "node:url";
      8 
      9 const __dirname = dirname(fileURLToPath(import.meta.url));
     10 
     11 const resolve = (pkg) => {
     12   const qaToolingDir = join(__dirname, "packages/qa-tooling");
     13   try {
     14     return require.resolve(pkg, { paths: [qaToolingDir] });
     15   } catch {
     16     return pkg;
     17   }
     18 };
     19 
     20 const eslint = require(resolve("@eslint/js"));
     21 const tseslint = require(resolve("typescript-eslint"));
     22 const reactPlugin = require(resolve("eslint-plugin-react"));
     23 const reactHooksPlugin = require(resolve("eslint-plugin-react-hooks"));
     24 const importPlugin = require(resolve("eslint-plugin-import"));
     25 const jsxA11yPlugin = require(resolve("eslint-plugin-jsx-a11y"));
     26 const prettierConfig = require(resolve("eslint-config-prettier"));
     27 const globals = require(resolve("globals"));
     28 
     29 /**
     30  * Barrel modules of a package, which legitimately re-export everything.
     31  */
     32 const barrelFiles = ["**/index.ts", "**/index.*.ts"];
     33 
     34 const barrelImportMessage =
     35   "Importing the package barrel from inside the package creates an import " +
     36   "cycle. Import from the module that defines the symbol.";
     37 
     38 /**
     39  * Ban importing a package's own barrel (src/index.ts and its platform
     40  * variants) from the modules of that package.
     41  *
     42  * The relative path to the barrel depends on how deep the importing file
     43  * sits, and only that exact path may be banned: a folder's own index.ts is
     44  * an ordinary module, and several packages deliberately split a component
     45  * across index/state/views.  So one config block per depth, each pairing the
     46  * files at that depth with the exact way up to the package root.
     47  */
     48 const barrelImportBan = (maxDepth) =>
     49   Array.from({ length: maxDepth }, (_, i) => {
     50     const depth = i + 1;
     51     const subdirs = "*/".repeat(depth - 1);
     52     const toRoot = depth === 1 ? "./" : "../".repeat(depth - 1);
     53     return {
     54       files: [`packages/*/src/${subdirs}*.{ts,tsx}`],
     55       rules: {
     56         "no-restricted-imports": [
     57           "error",
     58           {
     59             patterns: [
     60               {
     61                 group: [`${toRoot}index.js`, `${toRoot}index.*.js`],
     62                 message: barrelImportMessage,
     63               },
     64             ],
     65           },
     66         ],
     67       },
     68     };
     69   });
     70 
     71 export default tseslint.config(
     72   {
     73     ignores: [
     74       "prebuilt/**",
     75       "**/dist/**",
     76       "**/lib/**",
     77       "**/node_modules/**",
     78       "**/tsconfig.tsbuildinfo",
     79       "**/.eslintrc.js",
     80       "**/.eslintrc.cjs",
     81       // Third-party code kept in-tree.  It is maintained by syncing with
     82       // upstream, so it has to stay close to it; upstream's style is not ours
     83       // to fix, and reformatting it would make the next sync harder.
     84       "packages/taler-util/src/globbing/**", // minimatch
     85       "packages/taler-util/src/punycode.ts", // punycode.js
     86       "packages/taler-util/src/whatwg-url.ts", // jsdom/whatwg-url
     87       "packages/taler-util/src/bech32.ts", // BIP-173 reference code
     88       "packages/taler-util/src/segwit_addr.ts", // BIP-173 reference code
     89       "packages/idb-bridge/src/idbtypes.ts", // TypeScript's DOM IndexedDB types
     90       // Web Platform Tests ported verbatim, deliberately kept close to upstream.
     91       "packages/idb-bridge/src/idb-wpt-ported/**",
     92       // Input fixtures for the pogen extractor, not code that runs.
     93       "packages/pogen/example/**",
     94     ],
     95   },
     96   eslint.configs.recommended,
     97   ...tseslint.configs.recommended,
     98   {
     99     files: ["**/*.{ts,tsx,js,jsx,mjs,cjs}"],
    100     plugins: {
    101       react: reactPlugin,
    102       "react-hooks": reactHooksPlugin,
    103       "jsx-a11y": jsxA11yPlugin,
    104       import: importPlugin,
    105     },
    106     languageOptions: {
    107       globals: {
    108         ...globals.browser,
    109         ...globals.node,
    110         ...globals.es2021,
    111       },
    112       parserOptions: {
    113         ecmaVersion: "latest",
    114         sourceType: "module",
    115         ecmaFeatures: {
    116           jsx: true,
    117         },
    118       },
    119     },
    120     settings: {
    121       react: {
    122         version: "18.0",
    123         pragma: "h",
    124       },
    125     },
    126     rules: {
    127       // ---------------------------------------------------------------
    128       // Correctness.  These flag code that is, or is about to be, wrong.
    129       // ---------------------------------------------------------------
    130       ...reactHooksPlugin.configs.recommended.rules,
    131       // Calling a hook conditionally desynchronizes the hook order between
    132       // renders, which mixes up one hook's state with another's. These are
    133       // real defects; there are too many to unpick here, so they are visible
    134       // rather than blocking.
    135       "react-hooks/rules-of-hooks": "warn",
    136       // A list rendered without keys re-uses component state across items.
    137       "react/jsx-key": "error",
    138       "react/jsx-no-undef": "error",
    139       // Not diagnostics: these two exist so that no-unused-vars can see the
    140       // identifiers a JSX expression references.  Without them every
    141       // component imported for use in JSX is reported as unused.
    142       "react/jsx-uses-vars": "error",
    143       "react/jsx-uses-react": "error",
    144       // Node16 module resolution: an import without the extension fails at
    145       // run time, and tsc does not catch it in every configuration.
    146       "import/extensions": ["error", "ignorePackages"],
    147       // An infinite loop is written as `while (true)` on purpose.
    148       "no-constant-condition": ["error", { checkLoops: false }],
    149       // An empty catch is a deliberate "ignore this", an empty block is not.
    150       "no-empty": ["error", { allowEmptyCatch: true }],
    151       // `cond && fn()` is a call, not a stray expression; the codebase uses it
    152       // throughout. What is left is the genuine case: a statement with no
    153       // effect at all.
    154       "@typescript-eslint/no-unused-expressions": [
    155         "error",
    156         { allowShortCircuit: true, allowTernary: true },
    157       ],
    158 
    159       // ---------------------------------------------------------------
    160       // Known debt.  Real problems, too many to fix in one go, so they
    161       // stay visible without failing the run.
    162       // ---------------------------------------------------------------
    163       "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
    164       // Accessibility findings are real, but there are too many to clear here.
    165       // Downgraded rather than dropped, and only the ones the recommended set
    166       // actually turns on -- mapping the whole table would switch on rules
    167       // that upstream deliberately leaves off.
    168       ...Object.fromEntries(
    169         Object.entries(jsxA11yPlugin.configs.recommended.rules)
    170           .filter(([, setting]) => {
    171             const severity = Array.isArray(setting) ? setting[0] : setting;
    172             return severity !== "off" && severity !== 0;
    173           })
    174           .map(([rule]) => [rule, "warn"]),
    175       ),
    176 
    177       // ---------------------------------------------------------------
    178       // Style and preference.  Prettier owns layout; the rest below is a
    179       // matter of taste and should not be reported as a defect.
    180       // ---------------------------------------------------------------
    181       "no-var": "off",
    182       "prefer-const": "off",
    183       "no-extra-boolean-cast": "off",
    184       "no-prototype-builtins": "off",
    185       "@typescript-eslint/explicit-function-return-type": "off",
    186       "@typescript-eslint/no-use-before-define": "off",
    187       "@typescript-eslint/no-this-alias": "off",
    188       "@typescript-eslint/no-empty-object-type": "off",
    189       "@typescript-eslint/no-explicit-any": "off",
    190       "@typescript-eslint/no-namespace": "off",
    191       "@typescript-eslint/ban-ts-comment": "off",
    192       // preact/compat provides the JSX pragma; React is not in scope.
    193       "react/react-in-jsx-scope": "off",
    194     },
    195   },
    196   // A module that imports its own package barrel pulls in every other module
    197   // of the package, which is how the import cycles here are formed: the cycle
    198   // then breaks whichever module happens to be initialized first.
    199   ...barrelImportBan(5),
    200   {
    201     // A barrel re-exporting the package is exactly what these files are for.
    202     files: barrelFiles,
    203     rules: {
    204       "no-restricted-imports": "off",
    205     },
    206   },
    207   {
    208     // chai states an assertion as a property access -- `expect(x).undefined`
    209     // is the assertion, not a statement someone forgot to finish.
    210     files: ["**/*.test.{ts,tsx}"],
    211     rules: {
    212       "@typescript-eslint/no-unused-expressions": "off",
    213     },
    214   },
    215   {
    216     // Shims that hand CommonJS built-ins to esbuild-bundled code. Reaching for
    217     // require() is the whole point of the file.
    218     files: ["**/import-meta-url.js"],
    219     rules: {
    220       "@typescript-eslint/no-require-imports": "off",
    221     },
    222   },
    223   {
    224     // No React here: taler-harness is a CLI, and its `use*` helpers are test
    225     // fixtures, not hooks.
    226     files: ["packages/taler-harness/**"],
    227     rules: {
    228       "react-hooks/rules-of-hooks": "off",
    229       "react-hooks/exhaustive-deps": "off",
    230     },
    231   },
    232   {
    233     files: ["**/*.js", "**/*.mjs", "**/*.cjs"],
    234     ...tseslint.configs.disableTypeChecked,
    235   },
    236   prettierConfig,
    237 );