2025-08-27 10:54:05 +09:00
/* eslint-env node */
import process from "node:process" ;
2025-12-12 11:02:59 +09:00
2025-08-22 17:02:49 +09:00
import js from "@eslint/js" ;
2025-12-12 11:02:59 +09:00
import nextPlugin from "@next/eslint-plugin-next" ;
2026-01-15 11:28:25 +09:00
import importX from "eslint-plugin-import-x" ;
2025-12-12 11:02:59 +09:00
import reactHooks from "eslint-plugin-react-hooks" ;
2026-01-15 11:28:25 +09:00
import sonarjs from "eslint-plugin-sonarjs" ;
import unicorn from "eslint-plugin-unicorn" ;
2025-08-23 17:24:37 +09:00
import globals from "globals" ;
2025-12-12 14:50:12 +09:00
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 ,
} ) ) ;
2025-08-22 17:02:49 +09:00
export default [
2025-12-12 14:50:12 +09:00
// =============================================================================
2025-08-22 17:02:49 +09:00
// Global ignores
2025-12-12 14:50:12 +09:00
// =============================================================================
2025-08-22 17:02:49 +09:00
{
ignores : [
"**/node_modules/**" ,
"**/dist/**" ,
"**/.next/**" ,
2025-12-12 13:10:29 +09:00
"**/.turbo/**" ,
"**/.cache/**" ,
"**/.pnpm-store/**" ,
"**/.typecheck/**" ,
2025-08-22 17:02:49 +09:00
"**/build/**" ,
"**/coverage/**" ,
"**/next-env.d.ts" ,
2025-12-10 16:08:34 +09:00
"**/prisma/**" ,
2025-08-22 17:02:49 +09:00
] ,
} ,
2025-12-12 14:50:12 +09:00
// =============================================================================
// Base JS
// =============================================================================
2025-08-22 17:02:49 +09:00
js . configs . recommended ,
2026-01-15 11:28:25 +09:00
// =============================================================================
// 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" ,
} ,
} ,
2025-12-12 14:50:12 +09:00
// =============================================================================
// TypeScript (fast rules, no type information)
// =============================================================================
... withFiles ( tseslint . configs . recommended , TS _FILES ) ,
2025-12-12 11:47:17 +09:00
2025-12-12 14:50:12 +09:00
// =============================================================================
// TypeScript (type-aware rules) — only where we want the cost
// =============================================================================
... tseslint . configs . recommendedTypeChecked . map ( config => ( {
2025-12-12 11:47:17 +09:00
... config ,
2025-12-12 14:50:12 +09:00
files : [ ... BFF _TS _FILES , ... PACKAGES _TS _FILES ] ,
2025-12-12 11:47:17 +09:00
languageOptions : {
... ( config . languageOptions || { } ) ,
parserOptions : {
... ( ( config . languageOptions && config . languageOptions . parserOptions ) || { } ) ,
projectService : true ,
tsconfigRootDir : process . cwd ( ) ,
} ,
2025-08-23 17:24:37 +09:00
} ,
2025-08-22 17:02:49 +09:00
} ) ) ,
2025-12-10 13:59:41 +09:00
2025-12-12 14:50:12 +09:00
// =============================================================================
// Backend + domain packages: sensible defaults
// =============================================================================
2025-08-22 17:02:49 +09:00
{
2025-12-26 17:27:22 +09:00
files : [ "packages/domain/**/*.ts" ] ,
2025-08-22 17:02:49 +09:00
rules : {
"@typescript-eslint/consistent-type-imports" : "error" ,
2025-12-25 17:30:02 +09:00
"@typescript-eslint/no-unused-vars" : [
"warn" ,
{ argsIgnorePattern : "^_" , varsIgnorePattern : "^_" } ,
] ,
2025-08-22 17:02:49 +09:00
"no-console" : [ "warn" , { allow : [ "warn" , "error" ] } ] ,
2025-12-26 17:27:22 +09:00
"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." ,
} ,
] ,
} ,
] ,
2025-08-22 17:02:49 +09:00
} ,
} ,
2025-12-12 14:50:12 +09:00
// =============================================================================
// Shared packages: stricter safety
// =============================================================================
2025-08-22 17:02:49 +09:00
{
2025-12-12 14:50:12 +09:00
files : PACKAGES _TS _FILES ,
2025-08-22 17:02:49 +09:00
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" ,
} ,
} ,
2025-12-12 14:50:12 +09:00
// =============================================================================
// Portal (Next.js)
// =============================================================================
2025-12-12 11:02:59 +09:00
{
... nextPlugin . configs [ "core-web-vitals" ] ,
2025-12-12 14:50:12 +09:00
files : PORTAL _FILES ,
2025-12-12 11:02:59 +09:00
} ,
{
files : [ "apps/portal/**/*.{jsx,tsx}" ] ,
2025-12-12 11:47:17 +09:00
plugins : { "react-hooks" : reactHooks } ,
rules : {
"react-hooks/rules-of-hooks" : "error" ,
"react-hooks/exhaustive-deps" : "warn" ,
} ,
2025-12-12 11:02:59 +09:00
} ,
2025-08-22 17:02:49 +09:00
{
files : [ "apps/portal/**/*.{ts,tsx}" ] ,
2025-08-23 17:24:37 +09:00
rules : {
2025-12-12 11:47:17 +09:00
"@typescript-eslint/no-unused-vars" : [
"error" ,
2025-12-12 14:50:12 +09:00
{
argsIgnorePattern : "^_" ,
varsIgnorePattern : "^_" ,
} ,
2025-12-12 11:47:17 +09:00
] ,
2025-08-23 17:24:37 +09:00
"@next/next/no-html-link-for-pages" : "off" ,
2025-12-29 15:07:11 +09:00
// Prevent console.log in production code - use logger instead
"no-console" : [ "error" , { allow : [ "warn" , "error" ] } ] ,
2025-08-23 17:24:37 +09:00
} ,
2025-08-22 17:02:49 +09:00
} ,
2025-12-12 14:50:12 +09:00
// =============================================================================
// Portal navigation guardrails
// =============================================================================
2025-09-11 14:23:33 +09:00
{
2025-09-18 16:39:57 +09:00
files : [ "apps/portal/src/app/(authenticated)/**/*.{ts,tsx}" ] ,
2025-09-11 14:23:33 +09:00
rules : {
"no-restricted-syntax" : [
"error" ,
{
2025-09-17 18:43:43 +09:00
selector : "MemberExpression[object.name='window'][property.name='location']" ,
2025-12-10 13:59:41 +09:00
message : "Use next/link or useRouter for navigation." ,
2025-09-11 14:23:33 +09:00
} ,
] ,
} ,
} ,
{
2025-09-18 16:39:57 +09:00
files : [ "apps/portal/src/app/(authenticated)/layout.tsx" ] ,
2025-12-10 13:59:41 +09:00
rules : { "no-restricted-imports" : "off" } ,
2025-09-11 14:23:33 +09:00
} ,
{
2025-09-18 16:39:57 +09:00
files : [ "apps/portal/src/app/(authenticated)/billing/invoices/[id]/page.tsx" ] ,
2025-12-10 13:59:41 +09:00
rules : { "no-restricted-syntax" : "off" } ,
2025-09-11 14:23:33 +09:00
} ,
2025-12-12 14:50:12 +09:00
// =============================================================================
// Architecture: import boundaries
// =============================================================================
2025-09-17 18:43:43 +09:00
{
2025-10-03 14:26:55 +09:00
files : [ "apps/portal/src/**/*.{ts,tsx}" , "apps/bff/src/**/*.ts" ] ,
2025-09-17 18:43:43 +09:00
rules : {
"no-restricted-imports" : [
"error" ,
{
2025-12-26 14:53:03 +09:00
paths : [
{
name : "@customer-portal/domain" ,
message :
"Do not import @customer-portal/domain (root). Use @customer-portal/domain/<module> instead." ,
} ,
] ,
patterns : [
{
group : [ "@customer-portal/domain/**/src/**" ] ,
message : "Import from @customer-portal/domain/<module> instead of internals." ,
} ,
{
group : [ "@customer-portal/domain/*/*" , "!@customer-portal/domain/*/providers" ] ,
message :
"No deep @customer-portal/domain imports. Use @customer-portal/domain/<module> (or BFF-only: .../<module>/providers)." ,
} ,
{
group : [ "@customer-portal/domain/*/providers/*" ] ,
message :
"Do not deep-import provider internals. Import from @customer-portal/domain/<module>/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/<module> instead." ,
} ,
] ,
2025-09-17 18:43:43 +09:00
patterns : [
2025-12-26 17:27:22 +09:00
{
group : [ "@/hooks/*" ] ,
message :
"Do not import from @/hooks. Use @/lib/hooks (app-level hooks) or feature hooks under src/features/<feature>/hooks." ,
} ,
2025-10-03 14:26:55 +09:00
{
2025-10-03 17:33:39 +09:00
group : [ "@customer-portal/domain/**/src/**" ] ,
2025-12-10 13:59:41 +09:00
message : "Import from @customer-portal/domain/<module> instead of internals." ,
2025-09-17 18:43:43 +09:00
} ,
2025-12-26 14:53:03 +09:00
{
group : [ "@customer-portal/domain/*/providers" ] ,
message :
"Portal must not import provider adapters/types. Import normalized domain models from @customer-portal/domain/<module> instead." ,
} ,
{
group : [ "@customer-portal/domain/*/*" , "!@customer-portal/domain/*/providers" ] ,
2025-12-29 18:41:28 +09:00
message :
"No deep @customer-portal/domain imports. Use @customer-portal/domain/<module> only." ,
2025-12-26 14:53:03 +09:00
} ,
{
group : [ "@customer-portal/domain/*/providers/*" ] ,
message :
"Do not deep-import provider internals. Import from @customer-portal/domain/<module>/providers only (BFF-only)." ,
} ,
2025-09-17 18:43:43 +09:00
] ,
} ,
] ,
2025-08-22 17:02:49 +09:00
} ,
} ,
2025-12-26 14:53:03 +09:00
{
files : [ "apps/portal/src/app/(authenticated)/layout.tsx" ] ,
rules : { "no-restricted-imports" : "off" } ,
} ,
2025-08-22 17:02:49 +09:00
2025-12-12 14:50:12 +09:00
// =============================================================================
// BFF: stricter type safety (type-aware)
// =============================================================================
2025-08-22 17:02:49 +09:00
{
2025-12-12 14:50:12 +09:00
files : BFF _TS _FILES ,
2025-08-22 17:02:49 +09:00
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" ,
2026-01-15 11:28:25 +09:00
"@typescript-eslint/no-misused-promises" : "error" ,
"@typescript-eslint/promise-function-async" : "warn" ,
"@typescript-eslint/await-thenable" : "error" ,
2025-12-29 15:07:11 +09:00
// Prevent console.log in production code - use logger instead
"no-console" : [ "error" , { allow : [ "warn" , "error" ] } ] ,
2025-10-03 14:26:55 +09:00
} ,
} ,
2025-12-26 13:04:15 +09:00
// =============================================================================
// 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." ,
} ,
] ,
} ,
} ,
2026-01-15 18:57:00 +09:00
// =============================================================================
// 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/<module> instead." ,
} ,
] ,
patterns : [
{
group : [ "@customer-portal/domain/**/src/**" ] ,
message : "Import from @customer-portal/domain/<module> instead of internals." ,
} ,
{
group : [ "@customer-portal/domain/*/*" , "!@customer-portal/domain/*/providers" ] ,
message :
"No deep @customer-portal/domain imports. Use @customer-portal/domain/<module> (or BFF-only: .../<module>/providers)." ,
} ,
{
group : [ "@customer-portal/domain/*/providers/*" ] ,
message :
"Do not deep-import provider internals. Import from @customer-portal/domain/<module>/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)." ,
} ,
] ,
} ,
} ,
2025-12-12 14:50:12 +09:00
// =============================================================================
// Node globals for tooling/config files
// =============================================================================
2025-10-03 14:26:55 +09:00
{
2025-12-12 14:35:19 +09:00
files : [
"*.config.*" ,
"apps/portal/next.config.mjs" ,
"config/**/*.{js,cjs,mjs}" ,
"scripts/**/*.{js,cjs,mjs}" ,
2025-12-12 14:50:12 +09:00
"apps/**/scripts/**/*.{js,cjs,mjs}" ,
2025-12-12 14:35:19 +09:00
] ,
2025-12-10 13:59:41 +09:00
languageOptions : {
globals : { ... globals . node } ,
2025-08-22 17:02:49 +09:00
} ,
} ,
] ;