/* 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: ["packages/domain/**/*.ts"], rules: { "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-unused-vars": [ "warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, ], "no-console": ["warn", { allow: ["warn", "error"] }], "no-restricted-imports": [ "error", { patterns: [ { group: ["#toolkit/whmcs", "#toolkit/whmcs/*"], message: "WHMCS provider utilities must not live under toolkit. Keep them under `packages/domain/common/providers/whmcs-utils/` and import via relative paths inside the domain package.", }, ], }, ], }, }, // ============================================================================= // 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", { paths: [ { name: "@customer-portal/domain", message: "Do not import @customer-portal/domain (root). Use @customer-portal/domain/ instead.", }, ], patterns: [ { group: ["@customer-portal/domain/**/src/**"], message: "Import from @customer-portal/domain/ instead of internals.", }, { group: ["@customer-portal/domain/*/*", "!@customer-portal/domain/*/providers"], message: "No deep @customer-portal/domain imports. Use @customer-portal/domain/ (or BFF-only: ...//providers).", }, { group: ["@customer-portal/domain/*/providers/*"], message: "Do not deep-import provider internals. Import from @customer-portal/domain//providers only.", }, ], }, ], }, }, // ============================================================================= // Portal: hard boundary — must not import provider adapters/types // ============================================================================= { files: ["apps/portal/src/**/*.{ts,tsx}"], rules: { "no-restricted-imports": [ "error", { paths: [ { name: "@customer-portal/domain", message: "Do not import @customer-portal/domain (root). Use @customer-portal/domain/ instead.", }, ], patterns: [ { group: ["@/hooks/*"], message: "Do not import from @/hooks. Use @/lib/hooks (app-level hooks) or feature hooks under src/features//hooks.", }, { group: ["@customer-portal/domain/**/src/**"], message: "Import from @customer-portal/domain/ instead of internals.", }, { group: ["@customer-portal/domain/*/providers"], message: "Portal must not import provider adapters/types. Import normalized domain models from @customer-portal/domain/ instead.", }, { group: ["@customer-portal/domain/*/*", "!@customer-portal/domain/*/providers"], message: "No deep @customer-portal/domain imports. Use @customer-portal/domain/ only.", }, { group: ["@customer-portal/domain/*/providers/*"], message: "Do not deep-import provider internals. Import from @customer-portal/domain//providers only (BFF-only).", }, ], }, ], }, }, { files: ["apps/portal/src/app/(authenticated)/layout.tsx"], rules: { "no-restricted-imports": "off" }, }, // ============================================================================= // 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", }, }, // ============================================================================= // BFF controllers: request DTOs must come from shared domain schemas (no inline zod) // ============================================================================= { files: ["apps/bff/src/modules/**/*.controller.ts"], rules: { "no-restricted-syntax": [ "error", { selector: "ImportDeclaration[source.value='zod']", message: "Do not import zod in controllers. Put request/param/query schemas in @customer-portal/domain and use createZodDto(schema) with the global ZodValidationPipe.", }, ], }, }, // ============================================================================= // 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 }, }, }, ];