// @ts-check import nextPlugin from "@next/eslint-plugin-next"; import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript"; import comments from "eslint-plugin-eslint-comments"; import importX from "eslint-plugin-import-x"; import reactHooks from "eslint-plugin-react-hooks"; import security from "eslint-plugin-security"; import sonarjs from "eslint-plugin-sonarjs"; import tseslint from "typescript-eslint"; import { CORE_RESTRICTED_SYNTAX, DESIGN_SYSTEM_RESTRICTED_SYNTAX, } from "./eslint.restricted-syntax.mjs"; export default tseslint.config( { // Exclude build artifacts, generated output, and dev-only parity analysis scripts. site/ is // the standalone marketing site (its own package + toolchain), so the app's type-aware config // does not apply to it. See docs/superpowers/specs/2026-07-18-landing-cloudflare-pages-design.md. ignores: [ "drizzle/**", ".next/**", "dist/**", "node_modules/**", "**/parity-tools/**", "site/**", "docs-site/**", ".claude/**", ], }, ...tseslint.configs.recommendedTypeChecked, { languageOptions: { parserOptions: { projectService: { // Allow files outside tsconfig.json to be parsed with the default project: // - *.mjs: ESLint config file itself (postcss.config.mjs etc.) // - src/features/demo/*.ts: virtual in-memory file paths used in lint tests allowDefaultProject: [ "*.mjs", "scripts/*.mjs", ".env.example.test.ts", "src/features/demo/*.ts", ], }, }, }, plugins: { "import-x": importX, sonarjs, security, "eslint-comments": comments, // Rules of hooks + exhaustive-deps had never run against the ~226 client components; Next's // own plugin catches / misuse the framework has purpose-built replacements for. "react-hooks": reactHooks, "@next/next": nextPlugin, }, settings: { "import-x/resolver-next": [ createTypeScriptImportResolver({ alwaysTryTypes: true, }), ], }, rules: { // --- React hooks + Next.js --- ...reactHooks.configs.recommended.rules, ...nextPlugin.configs.recommended.rules, // Duplicates biome's lint/performance/noImgElement, which already guards these and carries // the per-site `biome-ignore` explaining why the three fixed-size avatars stay . Two // linters cannot both own the comment line directly above the element. "@next/next/no-img-element": "off", // --- TypeScript type-aware rules --- "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": "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-unsafe-argument": "error", "@typescript-eslint/no-non-null-assertion": "error", "@typescript-eslint/strict-boolean-expressions": "error", "@typescript-eslint/switch-exhaustiveness-check": "error", "@typescript-eslint/no-unnecessary-condition": "error", "@typescript-eslint/consistent-type-imports": "error", // --- Import rules (eslint-plugin-import-x, ESLint 10 compatible) --- // no-unresolved is off: TypeScript's own resolver catches these at compile time // and import-x resolver-next config can silently miss path aliases in some setups. "import-x/no-unresolved": "off", "import-x/no-cycle": "error", // --- Code quality --- "sonarjs/cognitive-complexity": ["error", 15], // --- Console --- "no-console": ["error", { allow: ["warn", "error"] }], // --- Custom project rules --- // Ban raw process.env outside the single env boundary. "no-restricted-properties": [ "error", { object: "process", property: "env", message: "Read env only via src/config/env.ts", }, ], // Ban raw throw new Error (use AppError), fetch without signal, and hand-rolled // menus/modals (design-system enforcement). See the const definitions above. "no-restricted-syntax": [ "error", ...CORE_RESTRICTED_SYNTAX, ...DESIGN_SYSTEM_RESTRICTED_SYNTAX, ], }, }, // The design-system primitives ARE the sanctioned home for these patterns: dialog.tsx uses // `fixed inset-0` for its overlay. Keep core bans, drop the design-system ones. { files: ["src/components/ui/**"], rules: { "no-restricted-syntax": ["error", ...CORE_RESTRICTED_SYNTAX], }, }, // The env boundary is the one place process.env is legitimately read and // where a raw Error throw at boot time is intentional (fails fast before // AppError's domain system is available). { files: ["src/config/env.ts"], rules: { "no-restricted-properties": "off", "no-restricted-syntax": "off", }, }, // clientEnv.ts is the NEXT_PUBLIC_* counterpart of env.ts: it is the single // place browser-safe env vars are read. process.env is required here because // Next.js inlines NEXT_PUBLIC_* vars as literals at build time. { files: ["src/config/clientEnv.ts"], rules: { "no-restricted-properties": "off", }, }, // instrumentation.ts is the server boot entry. It reads NEXT_RUNTIME (Next-injected // framework metadata, not app config) so the edge runtime can bail out before the // node-only env module is imported. That single read is exempt from the env boundary. { files: ["src/instrumentation.ts"], rules: { "no-restricted-properties": "off", }, }, // assertNever in result.ts is a programmer-error guard that legitimately // throws a raw Error. errorIds.ts declares AppError itself so it cannot // import AppError from itself. { files: ["src/types/result.ts", "src/constants/errorIds.ts"], rules: { "no-restricted-syntax": "off" }, }, // Tooling config files run outside the app (no env.ts, no AppError available). // They use process.env directly by necessity and are not app code. { files: [ "next.config.ts", "drizzle.config.ts", "vitest.config.ts", "postcss.config.mjs", "eslint.config.mjs", "eslint.restricted-syntax.mjs", ], rules: { "no-restricted-properties": "off", "no-restricted-syntax": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-unsafe-argument": "off", }, }, // scripts/** are standalone dev-tooling programs that run outside the Next.js // app process. They use process.env and console.warn/error directly (no app // env.ts or AppError available without pulling in all required env vars). { files: ["scripts/**/*.ts"], rules: { "no-restricted-properties": "off", "no-restricted-syntax": "off", "no-console": ["error", { allow: ["warn", "error"] }], "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-unsafe-argument": "off", }, }, // scripts/**/*.mjs: same standalone-tooling rationale, but plain JS (no types), so the // type-aware conditionals rule and console policy don't fit either. CLI programs whose // stdout IS the product (parity gate reports) may use console.log. { files: ["scripts/**/*.mjs"], rules: { "no-restricted-properties": "off", "no-restricted-syntax": "off", "no-console": "off", "@typescript-eslint/strict-boolean-expressions": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-unsafe-argument": "off", }, }, // vitest.setup.ts seeds process.env for the test harness: process.env access // is intentional here, and it runs before app modules load. { files: ["vitest.setup.ts"], rules: { "no-restricted-properties": "off", "@typescript-eslint/strict-boolean-expressions": "off", }, }, // Test files, and the test-only helpers under src/test/**, relax rules that conflict with // idiomatic test patterns. src/test/** never ships: it is the Postgres harness and assertion // helpers, so a raw throw there is a test failure, not an app error path. // - no-unnecessary-condition: narrowing via `if (r.ok)` is intentional in tests // - no-non-null-assertion: test assertions often use ! for clarity // - no-restricted-syntax: tests may throw raw errors to verify error paths // - no-unsafe-*: ESLint API types return `any` for rule configs; acceptable in tests // Note: the process.env ban (no-restricted-properties) stays ON for test files; // only src/config/env.test.ts is exempted explicitly below. { files: ["**/*.test.ts", "**/*.test.tsx", "*.test.ts", "*.test.tsx", "src/test/**/*.ts"], rules: { "no-restricted-syntax": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-unnecessary-condition": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-unsafe-argument": "off", }, }, // env.test.ts spreads process.env to build production-config test inputs for // parseEnv. This is the ONLY test file that needs raw process.env; the ban // stays in force for all other test files. Placed last so it wins. { files: ["src/config/env.test.ts"], rules: { "no-restricted-properties": "off" }, }, );