Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
316 lines
11 KiB
JavaScript
316 lines
11 KiB
JavaScript
import { defineConfig } from 'eslint/config'
|
|
import nextVitals from 'eslint-config-next/core-web-vitals'
|
|
import nextTs from 'eslint-config-next/typescript'
|
|
import prettierConfig from 'eslint-config-prettier'
|
|
import prettierPlugin from 'eslint-plugin-prettier'
|
|
import unusedImportsPlugin from 'eslint-plugin-unused-imports'
|
|
import tseslint from 'typescript-eslint'
|
|
|
|
/**
|
|
* Strip plugin registrations from typescript-eslint shared configs so they do not
|
|
* collide with eslint-config-next (which already registers @typescript-eslint).
|
|
*/
|
|
function rulesOnly(configs, files = ['**/*.{ts,tsx}']) {
|
|
return configs
|
|
.filter((config) => config.rules && Object.keys(config.rules).length > 0)
|
|
.map((config) => ({
|
|
files: config.files ?? files,
|
|
rules: config.rules,
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* Frontend ESLint — Next.js core-web-vitals + typescript-eslint strictTypeChecked.
|
|
*
|
|
* Intentional overrides (still a high quality bar):
|
|
* - no-unnecessary-condition: off — optional DTO/API fields produce many false positives
|
|
* - no-redundant-type-constituents: off — `string | 'literal'` is intentional public API style
|
|
* - prefer-nullish-coalescing: ignorePrimitives — flag object nullish only, not `x || 0`
|
|
* - react-hooks/set-state-in-effect: off — HeroUI / controlled-input patterns
|
|
*/
|
|
const eslintConfig = defineConfig([
|
|
{
|
|
ignores: [
|
|
'.next/**',
|
|
'out/**',
|
|
'build/**',
|
|
'next-env.d.ts',
|
|
'node_modules/**',
|
|
'.now/**',
|
|
'*.css',
|
|
'.changeset/**',
|
|
'dist/**',
|
|
'esm/**',
|
|
'public/**',
|
|
'tests/**',
|
|
'scripts/**',
|
|
'*.config.js',
|
|
'*.config.mjs',
|
|
'*.config.ts',
|
|
'!playwright.config.ts',
|
|
'.DS_Store',
|
|
'coverage/**',
|
|
'playwright-report/**',
|
|
'test-results/**',
|
|
'api/generated/**',
|
|
'orval.config.ts',
|
|
'vitest.config.ts',
|
|
],
|
|
},
|
|
...nextVitals,
|
|
...nextTs,
|
|
{
|
|
files: ['**/*.{ts,tsx}'],
|
|
languageOptions: {
|
|
parserOptions: {
|
|
projectService: true,
|
|
tsconfigRootDir: import.meta.dirname,
|
|
},
|
|
},
|
|
},
|
|
...rulesOnly(tseslint.configs.strictTypeChecked),
|
|
...rulesOnly(tseslint.configs.stylisticTypeChecked),
|
|
{
|
|
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
plugins: {
|
|
prettier: prettierPlugin,
|
|
'unused-imports': unusedImportsPlugin,
|
|
},
|
|
settings: {
|
|
react: {
|
|
version: 'detect',
|
|
},
|
|
'import/resolver': {
|
|
typescript: {
|
|
alwaysTryTypes: true,
|
|
},
|
|
},
|
|
},
|
|
rules: {
|
|
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
eqeqeq: ['error', 'smart'],
|
|
'react/prop-types': 'off',
|
|
'react/jsx-uses-react': 'off',
|
|
'react/react-in-jsx-scope': 'off',
|
|
'react-hooks/exhaustive-deps': 'error',
|
|
'react-hooks/set-state-in-effect': 'off',
|
|
'react-hooks/incompatible-library': 'off',
|
|
'react-hooks/preserve-manual-memoization': 'off',
|
|
'react/no-unescaped-entities': 'error',
|
|
'react/self-closing-comp': 'error',
|
|
'jsx-a11y/media-has-caption': 'off',
|
|
'jsx-a11y/no-autofocus': 'off',
|
|
'jsx-a11y/click-events-have-key-events': 'error',
|
|
'jsx-a11y/interactive-supports-focus': 'error',
|
|
'prettier/prettier': 'error',
|
|
'no-unused-vars': 'off',
|
|
'unused-imports/no-unused-vars': 'off',
|
|
'unused-imports/no-unused-imports': 'error',
|
|
'@typescript-eslint/no-unused-vars': [
|
|
'error',
|
|
{
|
|
args: 'after-used',
|
|
ignoreRestSiblings: true,
|
|
argsIgnorePattern: '^_',
|
|
varsIgnorePattern: '^_',
|
|
caughtErrorsIgnorePattern: '^_',
|
|
},
|
|
],
|
|
'@typescript-eslint/no-explicit-any': 'error',
|
|
'@typescript-eslint/no-unsafe-function-type': 'error',
|
|
'@typescript-eslint/consistent-type-imports': [
|
|
'error',
|
|
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
|
],
|
|
'@typescript-eslint/no-floating-promises': 'error',
|
|
'@typescript-eslint/no-misused-promises': [
|
|
'error',
|
|
{ checksVoidReturn: { attributes: false } },
|
|
],
|
|
'@typescript-eslint/restrict-template-expressions': [
|
|
'error',
|
|
{ allowNumber: true, allowBoolean: true, allowNullish: true },
|
|
],
|
|
'@typescript-eslint/prefer-optional-chain': 'error',
|
|
'@typescript-eslint/no-unnecessary-condition': 'off',
|
|
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
|
// Mostly flags intentional Boolean()/Number() coercions at API/UI boundaries.
|
|
'@typescript-eslint/no-unnecessary-type-conversion': 'off',
|
|
'@typescript-eslint/prefer-nullish-coalescing': [
|
|
'error',
|
|
{
|
|
ignorePrimitives: {
|
|
string: true,
|
|
number: true,
|
|
boolean: true,
|
|
bigint: true,
|
|
},
|
|
},
|
|
],
|
|
'@typescript-eslint/no-base-to-string': [
|
|
'error',
|
|
{
|
|
ignoredTypeNames: ['Error', 'DOMException', 'URL', 'URLSearchParams', 'RegExp'],
|
|
},
|
|
],
|
|
'@typescript-eslint/no-empty-function': [
|
|
'error',
|
|
{ allow: ['arrowFunctions', 'functions', 'methods'] },
|
|
],
|
|
'import/order': [
|
|
'error',
|
|
{
|
|
groups: ['type', 'builtin', 'object', 'external', 'internal', 'parent', 'sibling', 'index'],
|
|
pathGroups: [
|
|
{
|
|
pattern: '@/**',
|
|
group: 'internal',
|
|
position: 'before',
|
|
},
|
|
{
|
|
pattern: '~/**',
|
|
group: 'external',
|
|
position: 'after',
|
|
},
|
|
],
|
|
pathGroupsExcludedImportTypes: ['builtin'],
|
|
'newlines-between': 'always',
|
|
},
|
|
],
|
|
'react/jsx-sort-props': [
|
|
'warn',
|
|
{
|
|
callbacksLast: true,
|
|
shorthandFirst: true,
|
|
noSortAlphabetically: false,
|
|
reservedFirst: true,
|
|
},
|
|
],
|
|
'padding-line-between-statements': [
|
|
'error',
|
|
{ blankLine: 'always', prev: '*', next: 'return' },
|
|
{ blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
|
|
{ blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] },
|
|
],
|
|
},
|
|
},
|
|
{
|
|
files: ['app/**/*.{js,jsx,ts,tsx}', 'components/**/*.{js,jsx,ts,tsx}', 'features/**/*.{js,jsx,ts,tsx}'],
|
|
ignores: ['components/formElements/**', 'components/consumer/input/**', 'components/heroui/**', '**/*.{test,spec}.{js,jsx,ts,tsx}'],
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
paths: [
|
|
{
|
|
name: '@/components/heroui/Input',
|
|
message: 'Use `@/components/formElements/Input` (supports FormProvider and standalone value/onValueChange).',
|
|
},
|
|
{
|
|
name: '@/components/heroui/Select',
|
|
message: 'Use `@/components/formElements/Input` with generalType="select".',
|
|
},
|
|
{
|
|
name: '@/components/heroui/NumberInput',
|
|
message: 'Use `@/components/formElements/Input` with generalType="numberInput".',
|
|
},
|
|
{
|
|
name: '@/components/heroui/InputOtp',
|
|
message: 'Use `@/components/formElements/Input` with generalType="otp".',
|
|
},
|
|
{
|
|
name: '@/components/heroui/Autocomplete',
|
|
message: 'Use `@/components/formElements/Input` with generalType="combobox".',
|
|
},
|
|
{
|
|
name: '@/components/heroui/SelectionControls',
|
|
message:
|
|
'Use `@/components/formElements/Input` (checkbox/radio/switch) or `@/components/formElements/Checkbox`.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector: "JSXOpeningElement[name.name='button']",
|
|
message: 'Use `@/components/formElements/Button` instead of raw <button>.',
|
|
},
|
|
{
|
|
selector: "JSXOpeningElement[name.name='textarea']",
|
|
message: 'Use `@/components/formElements/Input` with generalType="textarea" instead of raw <textarea>.',
|
|
},
|
|
{
|
|
selector:
|
|
"JSXOpeningElement[name.name='input']:not(:has(JSXAttribute[name.name='type'][value.value='file']))",
|
|
message:
|
|
'Use `@/components/formElements/Input` or `@/components/formElements/Checkbox`. Raw <input> is only allowed for type="file".',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
files: ['components/consumer/ConsumerButton.tsx'],
|
|
rules: {
|
|
// ConsumerButton is the consumer primitive; it intentionally renders native <button>.
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector: "JSXOpeningElement[name.name='textarea']",
|
|
message: 'Use `@/components/formElements/Input` with generalType="textarea" instead of raw <textarea>.',
|
|
},
|
|
{
|
|
selector:
|
|
"JSXOpeningElement[name.name='input']:not(:has(JSXAttribute[name.name='type'][value.value='file']))",
|
|
message:
|
|
'Use `@/components/formElements/Input` or `@/components/formElements/Checkbox`. Raw <input> is only allowed for type="file".',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
files: ['components/consumer/input/**/*.{ts,tsx}'],
|
|
rules: {
|
|
// Same as formElements: RHF Controller field values are `any` until forms are zod-inferred.
|
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
'@typescript-eslint/no-unsafe-call': 'off',
|
|
'@typescript-eslint/no-unsafe-return': 'off',
|
|
},
|
|
},
|
|
{
|
|
files: ['components/formElements/**/*.{ts,tsx}'],
|
|
rules: {
|
|
// react-hook-form Controller `field.value` / `field.onChange` are typed as `any`
|
|
// until forms are end-to-end zod-inferred. Keep other strict rules on.
|
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
'@typescript-eslint/no-unsafe-call': 'off',
|
|
'@typescript-eslint/no-unsafe-return': 'off',
|
|
},
|
|
},
|
|
{
|
|
files: ['**/*.{test,spec}.{ts,tsx}', '**/__tests__/**/*.{ts,tsx}', 'e2e/**/*.{ts,tsx}'],
|
|
rules: {
|
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
'@typescript-eslint/no-unsafe-call': 'off',
|
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
'@typescript-eslint/no-unsafe-return': 'off',
|
|
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
'@typescript-eslint/no-explicit-any': 'off',
|
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
'@typescript-eslint/unbound-method': 'off',
|
|
'@typescript-eslint/no-base-to-string': 'off',
|
|
'@typescript-eslint/no-empty-function': 'off',
|
|
'@typescript-eslint/require-await': 'off',
|
|
'@typescript-eslint/no-floating-promises': 'off',
|
|
'no-console': 'off',
|
|
},
|
|
},
|
|
prettierConfig,
|
|
])
|
|
|
|
export default eslintConfig
|