/* eslint-env node */ import process from "node:process"; import js from "@eslint/js"; import nextPlugin from "@next/eslint-plugin-next"; import importX from "eslint-plugin-import-x"; import reactHooks from "eslint-plugin-react-hooks"; import sonarjs from "eslint-plugin-sonarjs"; import unicorn from "eslint-plugin-unicorn"; 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, // ============================================================================= // Loop safety & complexity limits (all files) // ============================================================================= { files: TS_FILES, rules: { // Prevent infinite loops "for-direction": "error", "no-constant-condition": "error", "no-unmodified-loop-condition": "error", "no-unreachable-loop": "error", // Complexity limits complexity: ["warn", { max: 15 }], "max-depth": ["warn", { max: 4 }], "max-nested-callbacks": ["warn", { max: 3 }], "max-params": ["warn", { max: 4 }], "max-lines-per-function": ["warn", { max: 100, skipBlankLines: true, skipComments: true }], // Common mistakes "no-await-in-loop": "warn", "no-promise-executor-return": "error", "require-atomic-updates": "error", }, }, // ============================================================================= // SonarJS: code smells & cognitive complexity // ============================================================================= { files: TS_FILES, plugins: { sonarjs }, rules: { "sonarjs/cognitive-complexity": ["warn", 15], "sonarjs/no-duplicate-string": ["warn", { threshold: 3 }], "sonarjs/no-identical-functions": "error", "sonarjs/no-collapsible-if": "warn", "sonarjs/no-redundant-jump": "error", "sonarjs/no-nested-switch": "error", "sonarjs/no-nested-template-literals": "warn", "sonarjs/prefer-single-boolean-return": "warn", "sonarjs/no-gratuitous-expressions": "error", }, }, // ============================================================================= // Unicorn: modern JS patterns & footgun prevention // ============================================================================= { files: TS_FILES, plugins: { unicorn }, rules: { "unicorn/no-array-for-each": "warn", "unicorn/no-array-push-push": "warn", "unicorn/no-await-expression-member": "warn", "unicorn/no-for-loop": "warn", "unicorn/no-lonely-if": "warn", "unicorn/no-negated-condition": "warn", "unicorn/no-nested-ternary": "error", "unicorn/no-useless-undefined": "warn", "unicorn/prefer-array-find": "error", "unicorn/prefer-array-flat-map": "error", "unicorn/prefer-array-some": "error", "unicorn/prefer-at": "warn", "unicorn/prefer-includes": "error", "unicorn/prefer-number-properties": "error", "unicorn/prefer-optional-catch-binding": "warn", "unicorn/prefer-spread": "warn", "unicorn/prefer-string-slice": "error", "unicorn/throw-new-error": "error", }, }, // ============================================================================= // Import-X: circular dependency prevention & import hygiene // ============================================================================= { files: [...BFF_TS_FILES, ...PACKAGES_TS_FILES, ...PORTAL_FILES], plugins: { "import-x": importX }, rules: { "import-x/no-cycle": ["error", { maxDepth: 3 }], "import-x/no-self-import": "error", "import-x/no-useless-path-segments": "warn", "import-x/no-duplicates": "error", "import-x/first": "error", "import-x/newline-after-import": "warn", "import-x/no-mutable-exports": "error", }, }, // ============================================================================= // 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", // Prevent console.log in production code - use logger instead "no-console": ["error", { allow: ["warn", "error"] }], }, }, // ============================================================================= // 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", "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/promise-function-async": "warn", "@typescript-eslint/await-thenable": "error", // Prevent console.log in production code - use logger instead "no-console": ["error", { allow: ["warn", "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.", }, ], }, }, // ============================================================================= // BFF Architecture: Controllers must use facades, not integration internals // See: docs/decisions/007-service-classification.md // Note: Set to "warn" for incremental adoption. Upgrade to "error" once violations are fixed. // ============================================================================= { files: ["apps/bff/src/modules/**/*.controller.ts"], rules: { "no-restricted-imports": [ "warn", { 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.", }, { group: ["@bff/integrations/*/services/*", "@bff/integrations/*/connection/*"], message: "Controllers should not import integration services or connection internals directly. Import from @bff/integrations/*/facades/* instead (ADR-007).", }, ], }, ], }, }, // ============================================================================= // BFF Architecture: Aggregators must be read-only (no mutation methods) // See: docs/decisions/007-service-classification.md // ============================================================================= { files: [ "apps/bff/src/aggregators/**/*.ts", "apps/bff/src/modules/me-status/me-status.service.ts", "apps/bff/src/modules/users/infra/user-profile.service.ts", ], rules: { "no-restricted-syntax": [ "warn", { selector: "MethodDefinition[key.name=/^(create|update|delete|save|remove|add|set)[A-Z]/]", message: "Aggregators must be read-only. Mutation methods should be moved to orchestrators or services (ADR-007).", }, ], }, }, // ============================================================================= // BFF Architecture: Services must use Facades, not direct connection internals // This enforces the facade pattern for integration layer access // Target: HTTP client services in connection/ directories (e.g., whmcs-http-client.service.ts) // Note: Domain services like WhmcsClientService (customer operations) are allowed // ============================================================================= { files: ["apps/bff/src/modules/**/*.service.ts"], rules: { "no-restricted-imports": [ "error", { patterns: [ { group: ["@bff/integrations/*/connection/**/*"], message: "Do not import connection internals (HTTP clients, etc.) directly. Use the facade from @bff/integrations/*/facades/* instead.", }, ], }, ], }, }, // ============================================================================= // 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 }, }, }, ];