/* eslint-env node */ import process from "node:process"; import js from "@eslint/js"; import tseslint from "typescript-eslint"; import prettier from "eslint-plugin-prettier"; import { FlatCompat } from "@eslint/eslintrc"; import path from "node:path"; import globals from "globals"; // Use FlatCompat to consume Next.js' legacy shareable configs under apps/portal const compat = new FlatCompat({ baseDirectory: path.resolve("apps/portal") }); export default [ // Global ignores { ignores: [ "**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**", "**/coverage/**", "**/next-env.d.ts", ], }, // Base JS recommendations for all files js.configs.recommended, // Prettier integration (warn-only) { plugins: { prettier }, rules: { "prettier/prettier": "warn", }, }, // TypeScript (type-checked) for TS files only ...tseslint.configs.recommendedTypeChecked.map(config => ({ ...config, files: ["**/*.ts", "**/*.tsx"], languageOptions: { ...(config.languageOptions || {}), globals: { ...globals.node, }, }, })), { files: ["apps/bff/**/*.ts", "packages/shared/**/*.ts"], languageOptions: { parserOptions: { // Enable project service for monorepos without per-invocation project config 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"] }], }, }, // Enforce consistent strict rules across shared as well { files: ["packages/shared/**/*.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: apply Next's recommended config; TS rules only on TS files ...compat .extends("next/core-web-vitals") .map(config => ({ ...config, files: ["apps/portal/**/*.{js,jsx,ts,tsx}"] })), ...compat .extends("next/typescript") .map(config => ({ ...config, files: ["apps/portal/**/*.{ts,tsx}"] })), // Ensure type-aware rules in portal have parser services { files: ["apps/portal/**/*.{ts,tsx}"], languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: process.cwd(), }, }, rules: { // App Router: disable pages-directory specific rule "@next/next/no-html-link-for-pages": "off", }, }, // Prevent importing the DashboardLayout directly in (authenticated) pages. // Pages should rely on the shared route-group layout at (authenticated)/layout.tsx. { files: ["apps/portal/src/app/(authenticated)/**/*.{ts,tsx}"], rules: { "no-restricted-imports": [ "error", { patterns: [ { group: ["@/components/layout/dashboard-layout"], message: "Use the shared (authenticated)/layout.tsx instead of importing DashboardLayout in pages.", }, ], }, ], // Prefer Next.js and router, forbid window/location hard reload in portal pages "no-restricted-syntax": [ "error", { selector: "MemberExpression[object.name='window'][property.name='location']", message: "Use next/link or useRouter for navigation, not window.location.", }, { selector: "MemberExpression[object.name='location'][property.name=/^(href|assign|replace)$/]", message: "Use next/link or useRouter for navigation, not location.*.", }, ], }, }, // Allow the shared layout file itself to import the layout component { files: ["apps/portal/src/app/(authenticated)/layout.tsx"], rules: { "no-restricted-imports": "off", }, }, // Allow controlled window.location usage for invoice SSO download { files: ["apps/portal/src/app/(authenticated)/billing/invoices/[id]/page.tsx"], rules: { "no-restricted-syntax": "off", }, }, // Prevent type duplication and enforce modern patterns { files: ["apps/portal/src/**/*.{ts,tsx}", "packages/domain/src/**/*.ts"], rules: { "no-restricted-imports": [ "error", { patterns: [ { group: ["**/utils/ui-state*"], message: "ui-state.ts has been removed. Use patterns from @customer-portal/domain instead.", }, { group: ["@/types"], message: "Avoid importing from @/types. Import types directly from @customer-portal/domain or define locally.", }, ], }, ], // Prevent defining deprecated type patterns "no-restricted-syntax": [ "error", { selector: "TSInterfaceDeclaration[id.name=/^(LegacyAsyncState|PaginatedState|FilteredState)$/]", message: "These legacy state types are deprecated. Use AsyncState, PaginatedAsyncState, or FilterState from @customer-portal/domain instead.", }, { selector: "TSTypeAliasDeclaration[id.name=/^(LegacyAsyncState|PaginatedState|FilteredState)$/]", message: "These legacy state types are deprecated. Use AsyncState, PaginatedAsyncState, or FilterState from @customer-portal/domain instead.", }, ], }, }, // Node globals for Next config file { files: ["apps/portal/next.config.mjs"], languageOptions: { globals: { ...globals.node, }, }, }, // BFF: strict rules enforced { 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", }, }, ];