- Implemented FormStep component for user input (name, email, address). - Created OtpStep component for OTP verification. - Developed SuccessStep component to display success messages based on account creation. - Introduced eligibility-check.store for managing state throughout the eligibility check process. - Added commitlint configuration for standardized commit messages. - Configured knip for workspace management and project structure.
399 lines
14 KiB
JavaScript
399 lines
14 KiB
JavaScript
/* 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/<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.",
|
|
},
|
|
],
|
|
patterns: [
|
|
{
|
|
group: ["@/hooks/*"],
|
|
message:
|
|
"Do not import from @/hooks. Use @/lib/hooks (app-level hooks) or feature hooks under src/features/<feature>/hooks.",
|
|
},
|
|
{
|
|
group: ["@customer-portal/domain/**/src/**"],
|
|
message: "Import from @customer-portal/domain/<module> instead of internals.",
|
|
},
|
|
{
|
|
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"],
|
|
message:
|
|
"No deep @customer-portal/domain imports. Use @customer-portal/domain/<module> only.",
|
|
},
|
|
{
|
|
group: ["@customer-portal/domain/*/providers/*"],
|
|
message:
|
|
"Do not deep-import provider internals. Import from @customer-portal/domain/<module>/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.",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// =============================================================================
|
|
// 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 },
|
|
},
|
|
},
|
|
];
|