143 lines
5.6 KiB
JavaScript
143 lines
5.6 KiB
JavaScript
import js from '@eslint/js';
|
|
import globals from 'globals';
|
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
import tseslint from 'typescript-eslint';
|
|
|
|
// Flat config scoped to the React client. The point is enforcement of the two
|
|
// rules that catch real React bugs — rules-of-hooks (conditional hooks) and
|
|
// exhaustive-deps (stale closures) — which nothing previously checked. Server
|
|
// code (CommonJS Node) is out of scope here; `npm run check:server` covers it.
|
|
export default [
|
|
{
|
|
ignores: [
|
|
'dist/**',
|
|
'node_modules/**',
|
|
'coverage/**',
|
|
'mkdocs/**',
|
|
'data/**',
|
|
'client/**/*.test.*',
|
|
],
|
|
},
|
|
{
|
|
// Server (CommonJS TypeScript `.cts`, run via Node type-stripping). Node globals,
|
|
// TS parser. Same "focused signal" philosophy as the client: catch real bugs,
|
|
// keep stylistic noise as warnings. `check:server` (node --check) still covers
|
|
// syntax; tsconfig.server handles types.
|
|
files: [
|
|
'{db,routes,services,middleware,workers,utils,setup}/**/*.cts',
|
|
'server.cts',
|
|
'server.mts',
|
|
],
|
|
languageOptions: {
|
|
parser: tseslint.parser,
|
|
ecmaVersion: 2023,
|
|
sourceType: 'module', // .cts files carry `import type` alongside require()
|
|
globals: { ...globals.node },
|
|
},
|
|
plugins: { '@typescript-eslint': tseslint.plugin },
|
|
rules: {
|
|
...js.configs.recommended.rules,
|
|
'no-undef': 'off', // TypeScript + Node globals handle this
|
|
'no-unused-vars': 'off',
|
|
'@typescript-eslint/no-unused-vars': [
|
|
'warn',
|
|
{ varsIgnorePattern: '^[A-Z_]', argsIgnorePattern: '^_' },
|
|
],
|
|
'no-empty': ['warn', { allowEmptyCatch: true }],
|
|
'no-useless-escape': 'warn',
|
|
'no-control-regex': 'off', // CSV/import parsers use control chars deliberately
|
|
// Pre-existing intentional/harmless patterns → advisory for now, ratchet later.
|
|
'no-irregular-whitespace': 'warn', // CSV/import parsers handle exotic whitespace
|
|
'no-misleading-character-class': 'warn',
|
|
'no-extra-boolean-cast': 'warn',
|
|
// Pillar D ratchet: server code logs through utils/logger.cts only.
|
|
// The two sanctioned exceptions (logger itself + env bootstrap, which runs
|
|
// before the logger exists) are carved out in the override block below.
|
|
'no-console': 'error',
|
|
},
|
|
},
|
|
{
|
|
// Sanctioned console users: the logger's own sink and fatal-config
|
|
// bootstrap output that must work before the logger is importable.
|
|
files: ['utils/logger.cts', 'utils/env.cts', 'utils/apiError.cts'],
|
|
rules: { 'no-console': 'off' },
|
|
},
|
|
{
|
|
// Error-shape ratchet (CODE_STANDARDS "Error responses", now enforced):
|
|
// routes throw utils/apiError.cts factories; hand-rolled standardizeError
|
|
// bodies can't come back. transactions.cts (internal {error} result
|
|
// convention rethrown at the boundary) and import.cts (sendImportError
|
|
// error-id envelope) are the two documented exceptions.
|
|
files: ['routes/**/*.cts'],
|
|
ignores: ['routes/transactions.cts', 'routes/import.cts'],
|
|
rules: {
|
|
// no-restricted-imports can't see require() — match the call itself.
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector: "CallExpression[callee.name='require'] Literal[value=/errorFormatter/]",
|
|
message:
|
|
'Routes emit errors by throwing utils/apiError.cts factories (see CODE_STANDARDS.md); the terminal handler formats them.',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
files: ['client/**/*.{js,jsx}'],
|
|
languageOptions: {
|
|
ecmaVersion: 2023,
|
|
sourceType: 'module',
|
|
globals: { ...globals.browser },
|
|
parserOptions: { ecmaFeatures: { jsx: true } },
|
|
},
|
|
plugins: {
|
|
'react-hooks': reactHooks,
|
|
'react-refresh': reactRefresh,
|
|
},
|
|
rules: {
|
|
...js.configs.recommended.rules,
|
|
// The two that matter most for correctness:
|
|
'react-hooks/rules-of-hooks': 'error',
|
|
'react-hooks/exhaustive-deps': 'warn',
|
|
// HMR safety (Vite fast-refresh); allow non-component constant exports.
|
|
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
|
// Keep the js-recommended noise as warnings so hook signal isn't drowned out.
|
|
'no-unused-vars': ['warn', { varsIgnorePattern: '^[A-Z_]', argsIgnorePattern: '^_' }],
|
|
'no-empty': ['warn', { allowEmptyCatch: true }],
|
|
},
|
|
},
|
|
{
|
|
// TypeScript files: same react-hooks/react-refresh enforcement, via the
|
|
// TS parser. TS itself handles undefined identifiers + unused vars, so the
|
|
// core rules that don't understand types are swapped for their TS-aware
|
|
// equivalents. (Not the full typescript-eslint recommended set — this keeps
|
|
// the signal focused on the same correctness rules as the JS config.)
|
|
files: ['client/**/*.{ts,tsx}'],
|
|
languageOptions: {
|
|
parser: tseslint.parser,
|
|
ecmaVersion: 2023,
|
|
sourceType: 'module',
|
|
globals: { ...globals.browser },
|
|
parserOptions: { ecmaFeatures: { jsx: true } },
|
|
},
|
|
plugins: {
|
|
'react-hooks': reactHooks,
|
|
'react-refresh': reactRefresh,
|
|
'@typescript-eslint': tseslint.plugin,
|
|
},
|
|
rules: {
|
|
'react-hooks/rules-of-hooks': 'error',
|
|
'react-hooks/exhaustive-deps': 'warn',
|
|
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
|
'no-undef': 'off', // TypeScript checks this
|
|
'no-unused-vars': 'off',
|
|
'@typescript-eslint/no-unused-vars': [
|
|
'warn',
|
|
{ varsIgnorePattern: '^[A-Z_]', argsIgnorePattern: '^_' },
|
|
],
|
|
'no-empty': ['warn', { allowEmptyCatch: true }],
|
|
},
|
|
},
|
|
];
|