/* eslint-env node */ import process from "node:process"; import path from "node:path"; import { fileURLToPath } from "node:url"; import js from "@eslint/js"; import tseslint from "typescript-eslint"; import prettier from "eslint-plugin-prettier"; import globals from "globals"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Dynamically import Next.js ESLint configs only when needed async function getNextConfigs() { try { const { FlatCompat } = await import("@eslint/eslintrc"); const compat = new FlatCompat({ baseDirectory: path.join(__dirname, "apps/portal") }); return { coreWebVitals: compat.extends("next/core-web-vitals"), typescript: compat.extends("next/typescript"), }; } catch { return { coreWebVitals: [], typescript: [] }; } } const nextConfigs = await getNextConfigs(); export default [ // Global ignores { ignores: [ "**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**", "**/coverage/**", "**/next-env.d.ts", "**/prisma/**", ], }, // Base JS recommendations js.configs.recommended, // Prettier integration { plugins: { prettier }, rules: { "prettier/prettier": "warn" }, }, // TypeScript type-checked rules for all TS files ...tseslint.configs.recommendedTypeChecked.map((config) => ({ ...config, files: ["**/*.ts", "**/*.tsx"], languageOptions: { ...(config.languageOptions || {}), globals: { ...globals.node }, }, })), // Backend & domain packages { files: ["apps/bff/**/*.ts", "packages/domain/**/*.ts"], languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: process.cwd(), }, }, rules: { "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], "no-console": ["warn", { allow: ["warn", "error"] }], }, }, // Strict rules for shared packages { files: ["packages/**/*.ts"], 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", }, }, // Next.js app config (only applies to portal files) ...nextConfigs.coreWebVitals.map((config) => ({ ...config, files: ["apps/portal/**/*.{js,jsx,ts,tsx}"], })), ...nextConfigs.typescript.map((config) => ({ ...config, files: ["apps/portal/**/*.{ts,tsx}"], })), // Portal type-aware rules { files: ["apps/portal/**/*.{ts,tsx}"], languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: process.cwd(), }, }, rules: { "@next/next/no-html-link-for-pages": "off", }, }, // Prevent hard navigation in authenticated pages { 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.", }, ], }, }, // Exceptions for specific files { 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" }, }, // Enforce domain imports architecture { 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 strict type safety { files: ["apps/bff/**/*.ts"], 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 config files { files: ["*.config.mjs", "apps/portal/next.config.mjs"], languageOptions: { globals: { ...globals.node }, }, }, ];