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