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 );