eslint.config.mjs
Rama main · réplica de lectura del repositorio original.
/**
* eslint.config.mjs - flat config (ESLint 9+).
*
* The interesting rules in this file are the *import boundaries*: they enforce the
* three-layer architecture from REBUILD.md § 4. Examples of what they prevent:
*
* • A `components/**` file importing `lib/db/**` (UI must not query the DB directly).
* • A `lib/db/**` file importing from `lib/rbac/**` (the DB layer must not know about RBAC).
* • A `lib/pdns/**` file importing from `lib/rbac/**` or `lib/audit/**` (PDNS client is
* a pure protocol adapter, RBAC and audit happen above it).
*
* When you find yourself wanting to disable one of these rules, that's a signal you're
* about to break the architecture. Stop and either refactor or write an ADR explaining
* why this case is the exception.
*/
import js from "@eslint/js";
import tseslint from "typescript-eslint";
// `eslint-plugin-import-x` is the actively-maintained, ESLint-10-compatible
// fork of `eslint-plugin-import` (the original crashes on ESLint 10's rule
// API). Registered under the `import` plugin key below so the existing
// `import/...` rule names and inline disable directives keep working.
import importPlugin from "eslint-plugin-import-x";
// Next's own `eslint-config-next` bundles `eslint-plugin-react`, which calls
// APIs ESLint 10 removed (`context.getFilename`) and crashes the entire lint
// run. Rather than run that brittle config, we compose the two highest-value,
// ESLint-10-compatible pieces it vendors - the React Hooks rules and the
// @next/next framework checks - directly. `eslint-config-next` stays a
// devDependency precisely because it's what installs these into node_modules.
import reactHooks from "eslint-plugin-react-hooks";
import nextPlugin from "@next/eslint-plugin-next";
export default tseslint.config(
// === Files to ignore globally ===
{
ignores: [
".next/**",
"node_modules/**",
"dist/**",
"build/**",
"coverage/**",
"drizzle/meta/**",
// Legacy reference - never lint, never modify.
"PowerDNS-Admin/**",
// postcss.config.mjs / eslint.config.mjs aren't in tsconfig.json's
// `include` (which is .ts/.tsx only). The type-aware lint rules
// need the file to be part of the TS project service to resolve
// types; without that we get a "not found by the project
// service" parser error. Both files are declarative config with
// no runtime logic worth linting - skip them.
"postcss.config.mjs",
"eslint.config.mjs",
"docker/entrypoint.mjs",
// Plain Node ESM scripts in scripts/ that aren't part of the TS
// project (Playwright + docker-stack drivers). The .ts ones in the
// same folder stay linted.
"scripts/*.mjs",
],
},
// === Base recommended configs ===
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
...tseslint.configs.stylisticTypeChecked,
// === TypeScript project setup ===
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
// === Project-wide rules ===
{
plugins: { import: importPlugin },
// `import/no-default-export` operates on the AST - no resolver needed. The
// three-layer import boundary (ADR-0004) is enforced separately by
// `no-restricted-imports` patterns in the per-layer `files` blocks below.
// (It used to be `import/no-restricted-paths`, which silently never fired:
// that rule needs a resolver to map the `@/` alias to a path, and none was
// configured - see ADR-0013.)
rules: {
// CONTRIBUTING.md § Language: no default exports. Named exports refactor cleanly.
"import/no-default-export": "error",
// CONTRIBUTING.md § Errors: no silent catches.
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"no-empty": ["error", { allowEmptyCatch: false }],
// Type safety. `any` is reserved for marked boundaries; everywhere else it's an error.
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-return": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/await-thenable": "error",
// Relax `checksVoidReturn.attributes` because React internally
// void-wraps event-handler return values - `<button onClick={async ()
// => {…}}>` is the idiomatic pattern and not a real bug. The rule
// still catches the dangerous cases (passing an async function
// where a sync-void callback is *actually* required, e.g.
// `Array.prototype.forEach`, `setTimeout`).
"@typescript-eslint/no-misused-promises": [
"error",
{ checksVoidReturn: { attributes: false } },
],
// Style preferences. Most are pre-commit-hook-fixable.
"@typescript-eslint/consistent-type-imports": [
"error",
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
],
"@typescript-eslint/array-type": ["error", { default: "array-simple" }],
// The three-layer import boundary (ADR-0004) lives in the per-layer
// `files` blocks below, via `no-restricted-imports` (specifier-based).
},
},
// === Import boundary: components must not reach into server-only layers ===
// UI renders data passed in as props / from server components; it never
// queries the DB, talks to PDNS, or touches auth internals directly.
{
files: ["components/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["@/lib/db", "@/lib/db/**"],
message:
"UI components must not import lib/db. Data goes through server components, route handlers, or domain logic.",
},
{
group: ["@/lib/pdns", "@/lib/pdns/**"],
message:
"UI components must not import lib/pdns. PDNS access goes through server components or route handlers.",
},
{
group: ["@/lib/auth", "@/lib/auth/**"],
message:
"UI components must not import lib/auth. Receive `currentUser` as a prop from a server component.",
},
],
},
],
},
},
// === Import boundary: the DB layer must not know about RBAC ===
{
files: ["lib/db/**/*.ts"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["@/lib/rbac", "@/lib/rbac/**"],
message:
"lib/db must not import lib/rbac. Authorization happens above the DB layer, not inside it.",
},
],
},
],
},
},
// === Import boundary: the PDNS client is a pure protocol adapter ===
// RBAC, audit, DB writes, and auth happen in the calling business logic, not
// inside the client. The four sanctioned DB-bridge files (registry,
// request-log, cluster-picker, sync) carry a file-top disable referencing
// ADR-0013.
{
files: ["lib/pdns/**/*.ts"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: [
"@/lib/rbac",
"@/lib/rbac/**",
"@/lib/audit",
"@/lib/audit/**",
"@/lib/db",
"@/lib/db/**",
"@/lib/auth",
"@/lib/auth/**",
],
message:
"lib/pdns is a pure protocol adapter (ADR-0004). RBAC/audit/DB/auth happen in the caller. If this is a deliberate DB bridge, see ADR-0013.",
},
],
},
],
},
},
// === Exceptions for Next.js conventions ===
// App Router files (`page.tsx`, `layout.tsx`, `route.ts`, etc.) MUST use default exports.
// This is a framework requirement; the rule is an explicit, scoped exception.
{
files: [
"app/**/page.tsx",
"app/**/layout.tsx",
"app/**/loading.tsx",
"app/**/error.tsx",
"app/**/not-found.tsx",
"app/**/template.tsx",
"app/**/default.tsx",
"app/**/route.ts",
"app/**/icon.tsx",
"app/**/apple-icon.tsx",
"app/**/opengraph-image.tsx",
"app/**/twitter-image.tsx",
"app/**/sitemap.ts",
"app/**/robots.ts",
"app/**/manifest.ts",
"proxy.ts",
"instrumentation.ts",
"next.config.ts",
"drizzle.config.ts",
"playwright.config.ts",
"vitest.config.ts",
"vitest.config.integration.ts",
"tailwind.config.ts",
"postcss.config.mjs",
"eslint.config.mjs",
"drizzle.sqlite.config.ts",
],
rules: { "import/no-default-export": "off" },
},
// === React Hooks rules ===
// `rules-of-hooks` is a hard correctness invariant (error). `exhaustive-deps`
// catches stale-closure bugs in effects/callbacks (warn - gated to a hard
// failure by `--max-warnings=0`). From eslint-plugin-react-hooks, which -
// unlike the bundled eslint-plugin-react - runs on ESLint 10.
{
files: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
plugins: { "react-hooks": reactHooks },
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
},
// === Next.js framework checks (@next/next) ===
// no-img-element, no-html-link-for-pages, no-sync-scripts, etc. Also
// satisfies the existing `@next/next/no-img-element` disable directives.
{
files: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}", "proxy.ts", "instrumentation.ts"],
plugins: { "@next/next": nextPlugin },
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs["core-web-vitals"].rules,
},
},
);