/* eslint-env node */ import process from "node:process"; import js from "@eslint/js"; import nextPlugin from "@next/eslint-plugin-next"; import reactHooks from "eslint-plugin-react-hooks"; import globals from "globals"; import tseslint from "typescript-eslint"; const TS_FILES = ["**/*.{ts,tsx}"]; const BFF_TS_FILES = ["apps/bff/**/*.ts"]; const PACKAGES_TS_FILES = ["packages/**/*.ts"]; const PORTAL_FILES = ["apps/portal/**/*.{js,jsx,ts,tsx}"]; const withFiles = (configs, files) => configs.map(config => ({ ...config, files, })); export default [ // ============================================================================= // Global ignores // ============================================================================= { ignores: [ "**/node_modules/**", "**/dist/**", "**/.next/**", "**/.turbo/**", "**/.cache/**", "**/.pnpm-store/**", "**/.typecheck/**", "**/build/**", "**/coverage/**", "**/next-env.d.ts", "**/prisma/**", ], }, // ============================================================================= // Base JS // ============================================================================= js.configs.recommended, // ============================================================================= // TypeScript (fast rules, no type information) // ============================================================================= ...withFiles(tseslint.configs.recommended, TS_FILES), // ============================================================================= // TypeScript (type-aware rules) — only where we want the cost // ============================================================================= ...tseslint.configs.recommendedTypeChecked.map(config => ({ ...config, files: [...BFF_TS_FILES, ...PACKAGES_TS_FILES], languageOptions: { ...(config.languageOptions || {}), parserOptions: { ...((config.languageOptions && config.languageOptions.parserOptions) || {}), projectService: true, tsconfigRootDir: process.cwd(), }, }, })), // ============================================================================= // Backend + domain packages: sensible defaults // ============================================================================= { files: [...BFF_TS_FILES, "packages/domain/**/*.ts"], rules: { "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], "no-console": ["warn", { allow: ["warn", "error"] }], }, }, // ============================================================================= // Shared packages: stricter safety // ============================================================================= { files: PACKAGES_TS_FILES, rules: { "@typescript-eslint/no-redundant-type-constituents": "error", "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-unsafe-return": "error", "@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-member-access": "error", }, }, // ============================================================================= // Portal (Next.js) // ============================================================================= { ...nextPlugin.configs["core-web-vitals"], files: PORTAL_FILES, }, { files: ["apps/portal/**/*.{jsx,tsx}"], plugins: { "react-hooks": reactHooks }, rules: { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", }, }, { files: ["apps/portal/**/*.{ts,tsx}"], rules: { "@typescript-eslint/no-unused-vars": [ "error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", }, ], "@next/next/no-html-link-for-pages": "off", }, }, // ============================================================================= // Portal navigation guardrails // ============================================================================= { files: ["apps/portal/src/app/(authenticated)/**/*.{ts,tsx}"], rules: { "no-restricted-syntax": [ "error", { selector: "MemberExpression[object.name='window'][property.name='location']", message: "Use next/link or useRouter for navigation.", }, ], }, }, { files: ["apps/portal/src/app/(authenticated)/layout.tsx"], rules: { "no-restricted-imports": "off" }, }, { files: ["apps/portal/src/app/(authenticated)/billing/invoices/[id]/page.tsx"], rules: { "no-restricted-syntax": "off" }, }, // ============================================================================= // Architecture: import boundaries // ============================================================================= { files: ["apps/portal/src/**/*.{ts,tsx}", "apps/bff/src/**/*.ts"], rules: { "no-restricted-imports": [ "error", { patterns: [ { group: ["@customer-portal/domain/**/src/**"], message: "Import from @customer-portal/domain/ instead of internals.", }, ], }, ], }, }, // ============================================================================= // BFF: stricter type safety (type-aware) // ============================================================================= { files: BFF_TS_FILES, rules: { "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-unsafe-assignment": "error", "@typescript-eslint/no-unsafe-member-access": "error", "@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-return": "error", "@typescript-eslint/no-unsafe-argument": "error", "@typescript-eslint/require-await": "error", "@typescript-eslint/no-floating-promises": "error", }, }, // ============================================================================= // Node globals for tooling/config files // ============================================================================= { files: [ "*.config.*", "apps/portal/next.config.mjs", "config/**/*.{js,cjs,mjs}", "scripts/**/*.{js,cjs,mjs}", "apps/**/scripts/**/*.{js,cjs,mjs}", ], languageOptions: { globals: { ...globals.node }, }, }, ];