eslint.config.mjs (9205B)
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 "**/vendor/**", 79 "**/tsconfig.tsbuildinfo", 80 "**/.eslintrc.js", 81 "**/.eslintrc.cjs", 82 // Generated bundle, not source. 83 "packages/web-util/src/tailwind.js", 84 // Third-party code kept in-tree. It is maintained by syncing with 85 // upstream, so it has to stay close to it; upstream's style is not ours 86 // to fix, and reformatting it would make the next sync harder. 87 "packages/taler-util/src/globbing/**", // minimatch 88 "packages/taler-util/src/punycode.ts", // punycode.js 89 "packages/taler-util/src/whatwg-url.ts", // jsdom/whatwg-url 90 "packages/taler-util/src/bech32.ts", // BIP-173 reference code 91 "packages/taler-util/src/segwit_addr.ts", // BIP-173 reference code 92 "packages/idb-bridge/src/idbtypes.ts", // TypeScript's DOM IndexedDB types 93 // Web Platform Tests ported verbatim, deliberately kept close to upstream. 94 "packages/idb-bridge/src/idb-wpt-ported/**", 95 // Input fixtures for the pogen extractor, not code that runs. 96 "packages/pogen/example/**", 97 ], 98 }, 99 eslint.configs.recommended, 100 ...tseslint.configs.recommended, 101 { 102 files: ["**/*.{ts,tsx,js,jsx,mjs,cjs}"], 103 plugins: { 104 react: reactPlugin, 105 "react-hooks": reactHooksPlugin, 106 "jsx-a11y": jsxA11yPlugin, 107 import: importPlugin, 108 }, 109 languageOptions: { 110 globals: { 111 ...globals.browser, 112 ...globals.node, 113 ...globals.es2021, 114 }, 115 parserOptions: { 116 ecmaVersion: "latest", 117 sourceType: "module", 118 ecmaFeatures: { 119 jsx: true, 120 }, 121 }, 122 }, 123 settings: { 124 react: { 125 version: "18.0", 126 pragma: "h", 127 }, 128 }, 129 rules: { 130 // --------------------------------------------------------------- 131 // Correctness. These flag code that is, or is about to be, wrong. 132 // --------------------------------------------------------------- 133 ...reactHooksPlugin.configs.recommended.rules, 134 // Calling a hook conditionally desynchronizes the hook order between 135 // renders, which mixes up one hook's state with another's. These are 136 // real defects; there are too many to unpick here, so they are visible 137 // rather than blocking. 138 "react-hooks/rules-of-hooks": "warn", 139 // A list rendered without keys re-uses component state across items. 140 "react/jsx-key": "error", 141 "react/jsx-no-undef": "error", 142 // Not diagnostics: these two exist so that no-unused-vars can see the 143 // identifiers a JSX expression references. Without them every 144 // component imported for use in JSX is reported as unused. 145 "react/jsx-uses-vars": "error", 146 "react/jsx-uses-react": "error", 147 // Node16 module resolution: an import without the extension fails at 148 // run time, and tsc does not catch it in every configuration. 149 "import/extensions": ["error", "ignorePackages"], 150 // An infinite loop is written as `while (true)` on purpose. 151 "no-constant-condition": ["error", { checkLoops: false }], 152 // An empty catch is a deliberate "ignore this", an empty block is not. 153 "no-empty": ["error", { allowEmptyCatch: true }], 154 // `cond && fn()` is a call, not a stray expression; the codebase uses it 155 // throughout. What is left is the genuine case: a statement with no 156 // effect at all. 157 "@typescript-eslint/no-unused-expressions": [ 158 "error", 159 { allowShortCircuit: true, allowTernary: true }, 160 ], 161 162 // --------------------------------------------------------------- 163 // Known debt. Real problems, too many to fix in one go, so they 164 // stay visible without failing the run. 165 // --------------------------------------------------------------- 166 "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }], 167 // Accessibility findings are real, but there are too many to clear here. 168 // Downgraded rather than dropped, and only the ones the recommended set 169 // actually turns on -- mapping the whole table would switch on rules 170 // that upstream deliberately leaves off. 171 ...Object.fromEntries( 172 Object.entries(jsxA11yPlugin.configs.recommended.rules) 173 .filter(([, setting]) => { 174 const severity = Array.isArray(setting) ? setting[0] : setting; 175 return severity !== "off" && severity !== 0; 176 }) 177 .map(([rule]) => [rule, "warn"]), 178 ), 179 180 // --------------------------------------------------------------- 181 // Style and preference. Prettier owns layout; the rest below is a 182 // matter of taste and should not be reported as a defect. 183 // --------------------------------------------------------------- 184 "no-var": "off", 185 "prefer-const": "off", 186 "no-extra-boolean-cast": "off", 187 "no-prototype-builtins": "off", 188 "@typescript-eslint/explicit-function-return-type": "off", 189 "@typescript-eslint/no-use-before-define": "off", 190 "@typescript-eslint/no-this-alias": "off", 191 "@typescript-eslint/no-empty-object-type": "off", 192 "@typescript-eslint/no-explicit-any": "off", 193 "@typescript-eslint/no-namespace": "off", 194 "@typescript-eslint/ban-ts-comment": "off", 195 // preact/compat provides the JSX pragma; React is not in scope. 196 "react/react-in-jsx-scope": "off", 197 }, 198 }, 199 // A module that imports its own package barrel pulls in every other module 200 // of the package, which is how the import cycles here are formed: the cycle 201 // then breaks whichever module happens to be initialized first. 202 ...barrelImportBan(5), 203 { 204 // A barrel re-exporting the package is exactly what these files are for. 205 files: barrelFiles, 206 rules: { 207 "no-restricted-imports": "off", 208 }, 209 }, 210 { 211 // chai states an assertion as a property access -- `expect(x).undefined` 212 // is the assertion, not a statement someone forgot to finish. 213 files: ["**/*.test.{ts,tsx}"], 214 rules: { 215 "@typescript-eslint/no-unused-expressions": "off", 216 }, 217 }, 218 { 219 // Shims that hand CommonJS built-ins to esbuild-bundled code. Reaching for 220 // require() is the whole point of the file. 221 files: ["**/import-meta-url.js"], 222 rules: { 223 "@typescript-eslint/no-require-imports": "off", 224 }, 225 }, 226 { 227 // No React here: taler-harness is a CLI, and its `use*` helpers are test 228 // fixtures, not hooks. 229 files: ["packages/taler-harness/**"], 230 rules: { 231 "react-hooks/rules-of-hooks": "off", 232 "react-hooks/exhaustive-deps": "off", 233 }, 234 }, 235 { 236 files: ["**/*.js", "**/*.mjs", "**/*.cjs"], 237 ...tseslint.configs.disableTypeChecked, 238 }, 239 prettierConfig, 240 );