eslint.config.js 3.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. import js from '@eslint/js';
  2. import tseslint from 'typescript-eslint';
  3. import reactHooks from 'eslint-plugin-react-hooks';
  4. import jsxA11y from 'eslint-plugin-jsx-a11y';
  5. import globals from 'globals';
  6. export default [
  7. { ignores: ['node_modules/**', '../internal/web/dist/**'] },
  8. js.configs.recommended,
  9. ...tseslint.configs.recommended.map((config) => ({
  10. ...config,
  11. files: ['**/*.{ts,tsx}'],
  12. })),
  13. {
  14. files: ['**/*.{ts,tsx}'],
  15. plugins: {
  16. 'react-hooks': reactHooks,
  17. },
  18. languageOptions: {
  19. ecmaVersion: 2022,
  20. sourceType: 'module',
  21. globals: {
  22. ...globals.browser,
  23. },
  24. },
  25. rules: {
  26. ...reactHooks.configs.recommended.rules,
  27. '@typescript-eslint/no-unused-vars': ['warn', {
  28. argsIgnorePattern: '^_',
  29. varsIgnorePattern: '^_',
  30. caughtErrorsIgnorePattern: '^_',
  31. }],
  32. // Zod migration goal (Step 7): every production module is held to
  33. // strict no-explicit-any. The two legacy class files at the bottom
  34. // of the rule list keep their existing file-level eslint-disable
  35. // until DBInbound is migrated off Inbound.toInbound() — see the
  36. // migration spec Non-Goals section.
  37. '@typescript-eslint/no-explicit-any': 'error',
  38. 'no-empty': ['error', { allowEmptyCatch: true }],
  39. 'react-hooks/set-state-in-effect': 'off',
  40. 'react-hooks/purity': 'off',
  41. 'react-hooks/react-compiler': 'off',
  42. 'react-hooks/preserve-manual-memoization': 'off',
  43. 'react-hooks/immutability': 'off',
  44. 'react-hooks/refs': 'off',
  45. },
  46. },
  47. {
  48. files: ['**/*.tsx'],
  49. plugins: { 'jsx-a11y': jsxA11y },
  50. rules: {
  51. ...jsxA11y.flatConfigs.recommended.rules,
  52. 'jsx-a11y/no-autofocus': 'off',
  53. },
  54. },
  55. {
  56. // The settings and xray pages write numeric InputNumber changes straight
  57. // into state, so a null-collapsing handler (`Number(v) || N`, or the
  58. // ternary `typeof v === 'number' ? v : N`) turns a cleared field into a
  59. // stored N — the cleared-port bug, #6121. Handlers here go through
  60. // onNumber() (src/utils/onNumber.ts) instead. Known limit: a handler
  61. // extracted into a variable and passed as onChange={handler} is not
  62. // matched; the inline shapes below are the ones that drift in practice.
  63. files: ['src/pages/settings/**/*.tsx', 'src/pages/xray/**/*.tsx'],
  64. rules: {
  65. 'no-restricted-syntax': ['error', {
  66. selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] LogicalExpression[operator="||"] > CallExpression[callee.name="Number"]',
  67. message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).',
  68. }, {
  69. selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] ConditionalExpression[test.left.operator="typeof"][alternate.type="Literal"]',
  70. message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).',
  71. }, {
  72. selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] LogicalExpression[operator="??"][right.type="Literal"]',
  73. message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).',
  74. }],
  75. },
  76. },
  77. {
  78. // The xray form modals (OutboundFormModal, BalancerFormModal,
  79. // DnsServerModal, WarpModal, …) stage values behind Zod validation like
  80. // the clients/inbounds modals do, and some of their fields carry a
  81. // deliberate clear-means-zero semantic — the direct-write rule above
  82. // does not apply to them.
  83. files: ['src/pages/xray/**/*Modal.tsx'],
  84. rules: {
  85. 'no-restricted-syntax': 'off',
  86. },
  87. },
  88. ];