// @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" },
},
);