This is an automated email from the ASF dual-hosted git repository.

bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new c1ad31f66c3 UI: Move most lint rules from ESLint to oxlint (#76491)
c1ad31f66c3 is described below

commit c1ad31f66c30557903527a963a9aa291085298bf
Author: Brent Bovenzi <[email protected]>
AuthorDate: Wed Nov 11 14:13:39 2026 -0500

    UI: Move most lint rules from ESLint to oxlint (#76491)
    
    ESLint runs every UI lint rule today, and the slow ones are a minority: 
oxlint
    covers 281 of the 340 enabled rules natively or through its JS plugin 
support,
    including perfectionist, stylistic, i18next and the local airflow plugin. 
ESLint
    keeps the 59 it cannot run yet, mostly type-aware typescript-eslint rules 
that
    need TypeScript 7.
    
    The oxlint config is the migrate tool's output with the differences that 
would
    otherwise change what gets reported fixed by hand: the core rules stay 
scoped to
    rules/, src/ and root files, i18next still skips test files, 
jsx-curly-brace-presence
    ignores JSX prop values and id-length ignores type parameters, as ESLint 
did.
    
    Four rules report code ESLint never flagged and are off in oxlint for now:
    jsx-max-depth, prefer-dom-node-dataset, consistent-function-scoping and
    no-useless-switch-case. ESLint still runs them, so nothing loses coverage.
    Five source files take an inline disable or a rename where oxlint reports a 
site
    ESLint accepted; the prefer-spread autofix would drop an optional chain 
there.
    
    ESLint no longer reports unused disable directives: most rules now run in
    oxlint, so ESLint would call their directives unused and --fix would delete 
them.
    
    Co-authored-by: Claude <[email protected]>
---
 .github/dependabot.yml                             |   1 +
 .rat-excludes                                      |   2 +
 airflow-core/.pre-commit-config.yaml               |   2 +-
 airflow-core/src/airflow/ui/.oxlintrc.json         | 611 +++++++++++++++++++++
 airflow-core/src/airflow/ui/eslint.config.js       |  18 +-
 airflow-core/src/airflow/ui/package.json           |   6 +-
 airflow-core/src/airflow/ui/pnpm-lock.yaml         | 227 ++++++++
 airflow-core/src/airflow/ui/rules/core.js          |  19 -
 airflow-core/src/airflow/ui/rules/i18next.js       |  68 ---
 airflow-core/src/airflow/ui/rules/perfectionist.js | 143 -----
 .../src/airflow/ui/rules/plugin-patch.d.ts         |   4 -
 airflow-core/src/airflow/ui/rules/stylistic.js     |  90 ---
 .../airflow/ui/src/components/GraphTaskFilters.tsx |   2 +-
 .../ui/src/components/ReactMarkdownBlocks.tsx      |   4 +-
 .../src/airflow/ui/src/layouts/Nav/Nav.tsx         |   3 +-
 .../airflow/ui/src/pages/Dag/Overview/Overview.tsx |   1 +
 .../ui/src/pages/Task/Overview/Overview.tsx        |   6 +-
 contributing-docs/15_node_environment_setup.rst    |   5 +-
 scripts/ci/prek/ts_compile_lint_ui.py              |   1 +
 19 files changed, 873 insertions(+), 340 deletions(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 761fc2de44b..717aa58adf8 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -137,6 +137,7 @@ updates:
       oxc:
         patterns:
           - "oxfmt"
+          - "oxlint*"
       typescript:
         patterns:
           - "typescript*"
diff --git a/.rat-excludes b/.rat-excludes
index 7dfd209b529..377201e658c 100644
--- a/.rat-excludes
+++ b/.rat-excludes
@@ -15,6 +15,7 @@
 .eslintignore
 .eslintrc
 .oxfmtrc.json
+.oxlintrc.json
 .prettierignore
 .prettierrc
 .rat-excludes
@@ -213,6 +214,7 @@ www-hash.txt
 **/.rat-excludes
 **/.gitignore
 **/.oxfmtrc.json
+**/.oxlintrc.json
 **/.prettierignore
 **/.prettierrc
 **/.airflowignore
diff --git a/airflow-core/.pre-commit-config.yaml 
b/airflow-core/.pre-commit-config.yaml
index b6fc7153d05..6d593e504af 100644
--- a/airflow-core/.pre-commit-config.yaml
+++ b/airflow-core/.pre-commit-config.yaml
@@ -242,7 +242,7 @@ repos:
         name: Compile / format / lint UI
         env:
           PYTHONPYCACHEPREFIX: .build/pycache
-        description: TS types generation / ESLint / Oxfmt new UI files
+        description: TS types generation / oxlint / ESLint / Oxfmt new UI files
         language: node
         files: |
           (?x)
diff --git a/airflow-core/src/airflow/ui/.oxlintrc.json 
b/airflow-core/src/airflow/ui/.oxlintrc.json
new file mode 100644
index 00000000000..94f79fd0440
--- /dev/null
+++ b/airflow-core/src/airflow/ui/.oxlintrc.json
@@ -0,0 +1,611 @@
+{
+  "$schema": "./node_modules/oxlint/configuration_schema.json",
+  "plugins": ["jsx-a11y", "react", "unicorn", "typescript"],
+  "jsPlugins": ["eslint-plugin-perfectionist", "@stylistic/eslint-plugin"],
+  "categories": {
+    "correctness": "off"
+  },
+  "env": {
+    "builtin": true
+  },
+  "settings": {
+    "react": {
+      "version": "19"
+    }
+  },
+  "ignorePatterns": ["**/dist/", "**coverage/", "**/openapi-gen/", 
"**/.vite/**"],
+  "rules": {
+    "perfectionist/sort-array-includes": "error",
+    "perfectionist/sort-classes": "error",
+    "perfectionist/sort-enums": "error",
+    "perfectionist/sort-interfaces": "error",
+    "perfectionist/sort-intersection-types": "error",
+    "perfectionist/sort-jsx-props": "error",
+    "perfectionist/sort-maps": "error",
+    "perfectionist/sort-object-types": "error",
+    "perfectionist/sort-objects": [
+      "error",
+      {
+        "type": "natural"
+      }
+    ],
+    "perfectionist/sort-switch-case": "error",
+    "perfectionist/sort-union-types": [
+      "error",
+      {
+        "groups": [
+          ["conditional", "function", "import"],
+          ["intersection", "keyword", "named"],
+          ["literal", "object", "operator", "tuple", "union"],
+          "nullish"
+        ]
+      }
+    ],
+    "jsx-a11y/alt-text": "error",
+    "jsx-a11y/anchor-ambiguous-text": "error",
+    "jsx-a11y/anchor-has-content": "error",
+    "jsx-a11y/anchor-is-valid": "error",
+    "jsx-a11y/aria-activedescendant-has-tabindex": "error",
+    "jsx-a11y/aria-props": "error",
+    "jsx-a11y/aria-proptypes": "error",
+    "jsx-a11y/aria-role": "error",
+    "jsx-a11y/aria-unsupported-elements": "error",
+    "jsx-a11y/autocomplete-valid": "error",
+    "jsx-a11y/click-events-have-key-events": "error",
+    "jsx-a11y/control-has-associated-label": "error",
+    "jsx-a11y/heading-has-content": "error",
+    "jsx-a11y/html-has-lang": "error",
+    "jsx-a11y/iframe-has-title": "error",
+    "jsx-a11y/img-redundant-alt": "error",
+    "jsx-a11y/interactive-supports-focus": "error",
+    "jsx-a11y/label-has-associated-control": "error",
+    "jsx-a11y/lang": "error",
+    "jsx-a11y/media-has-caption": "error",
+    "jsx-a11y/mouse-events-have-key-events": "error",
+    "jsx-a11y/no-access-key": "error",
+    "jsx-a11y/no-aria-hidden-on-focusable": "error",
+    "jsx-a11y/no-autofocus": "error",
+    "jsx-a11y/no-distracting-elements": "error",
+    "jsx-a11y/no-interactive-element-to-noninteractive-role": "error",
+    "jsx-a11y/no-noninteractive-element-interactions": "error",
+    "jsx-a11y/no-noninteractive-element-to-interactive-role": "error",
+    "jsx-a11y/no-noninteractive-tabindex": "error",
+    "jsx-a11y/no-redundant-roles": "error",
+    "jsx-a11y/no-static-element-interactions": "error",
+    "jsx-a11y/prefer-tag-over-role": "error",
+    "jsx-a11y/role-has-required-aria-props": "error",
+    "jsx-a11y/role-supports-aria-props": "error",
+    "jsx-a11y/scope": "error",
+    "jsx-a11y/tabindex-no-positive": "error",
+    "react/button-has-type": "error",
+    "react/checked-requires-onchange-or-readonly": "error",
+    "react/forbid-elements": [
+      "error",
+      {
+        "forbid": [
+          {
+            "element": "acronym",
+            "message": "Use <abbr> instead."
+          },
+          {
+            "element": "big",
+            "message": "Use CSS instead."
+          },
+          {
+            "element": "center",
+            "message": "Use CSS instead."
+          },
+          {
+            "element": "dir",
+            "message": "Use <ul> instead."
+          },
+          {
+            "element": "embed",
+            "message": "Use <object> instead."
+          },
+          {
+            "element": "font",
+            "message": "Use CSS instead."
+          },
+          {
+            "element": "frame",
+            "message": "Use <iframe> instead."
+          },
+          {
+            "element": "frameset",
+            "message": "Use <iframe> instead."
+          },
+          {
+            "element": "marquee",
+            "message": "Use CSS instead."
+          },
+          {
+            "element": "nobr",
+            "message": "Use CSS instead."
+          },
+          {
+            "element": "noembed",
+            "message": "Put fallback code inside an <object> instead."
+          },
+          {
+            "element": "noframes",
+            "message": "Use <iframe> instead."
+          },
+          {
+            "element": "param",
+            "message": "Use the <object> element with a data attribute to set 
the URL of an external resource."
+          },
+          {
+            "element": "plaintext",
+            "message": "Use <pre> instead."
+          },
+          {
+            "element": "rb",
+            "message": "Use UTF-8 instead."
+          },
+          {
+            "element": "rtc",
+            "message": "Use UTF-8 and CSS instead."
+          },
+          {
+            "element": "strike",
+            "message": "Use <del> or <s> instead."
+          },
+          {
+            "element": "tt",
+            "message": "Use <code>, <kbd>, <samp>, <var> or <pre> instead."
+          },
+          {
+            "element": "xmp",
+            "message": "Use <code> or <pre> instead."
+          }
+        ]
+      }
+    ],
+    "react/function-component-definition": [
+      "error",
+      {
+        "namedComponents": "arrow-function",
+        "unnamedComponents": "arrow-function"
+      }
+    ],
+    "react/hook-use-state": "error",
+    "react/iframe-missing-sandbox": "error",
+    "react/jsx-curly-brace-presence": [
+      "error",
+      {
+        "props": "never",
+        "children": "never",
+        "propElementValues": "ignore"
+      }
+    ],
+    "react/jsx-fragments": ["error", "syntax"],
+    "react/jsx-key": "error",
+    "react/jsx-max-depth": "off",
+    "react/jsx-no-comment-textnodes": "error",
+    "react/jsx-no-script-url": "error",
+    "react/jsx-no-target-blank": "error",
+    "react/jsx-no-useless-fragment": "error",
+    "react/jsx-pascal-case": "error",
+    "react/jsx-props-no-spread-multi": "error",
+    "react/no-array-index-key": "error",
+    "react/no-danger": "error",
+    "react/no-multi-comp": [
+      "warn",
+      {
+        "ignoreStateless": true
+      }
+    ],
+    "react/no-namespace": "error",
+    "react/no-object-type-as-default-prop": "error",
+    "react/no-string-refs": "error",
+    "react/no-unescaped-entities": "error",
+    "react/no-unstable-nested-components": "error",
+    "react/self-closing-comp": "error",
+    "react/void-dom-elements-no-children": "error",
+    "@stylistic/padding-line-between-statements": [
+      "error",
+      {
+        "blankLine": "always",
+        "next": "return",
+        "prev": "*"
+      },
+      {
+        "blankLine": "always",
+        "next": "*",
+        "prev": ["import", "const", "let", "var"]
+      },
+      {
+        "blankLine": "any",
+        "next": ["const", "let", "var"],
+        "prev": ["const", "let", "var"]
+      },
+      {
+        "blankLine": "any",
+        "next": "import",
+        "prev": "import"
+      },
+      {
+        "blankLine": "always",
+        "next": "export",
+        "prev": "*"
+      },
+      {
+        "blankLine": "any",
+        "next": "export",
+        "prev": "export"
+      }
+    ],
+    "@stylistic/spaced-comment": [
+      "error",
+      "always",
+      {
+        "exceptions": ["!"]
+      }
+    ],
+    "unicorn/catch-error-name": "error",
+    "unicorn/consistent-empty-array-spread": "error",
+    "unicorn/consistent-function-scoping": "off",
+    "unicorn/error-message": "error",
+    "unicorn/escape-case": "error",
+    "unicorn/no-abusive-eslint-disable": "error",
+    "unicorn/no-anonymous-default-export": "error",
+    "unicorn/no-await-in-promise-methods": "error",
+    "unicorn/no-console-spaces": "error",
+    "unicorn/no-hex-escape": "error",
+    "unicorn/no-instanceof-array": "error",
+    "unicorn/no-invalid-fetch-options": "error",
+    "unicorn/no-invalid-remove-event-listener": "error",
+    "unicorn/no-length-as-slice-end": "error",
+    "unicorn/no-magic-array-flat-depth": "error",
+    "unicorn/no-negated-condition": "error",
+    "unicorn/no-negation-in-equality-check": "error",
+    "unicorn/no-new-array": "error",
+    "unicorn/no-new-buffer": "error",
+    "unicorn/no-single-promise-in-promise-methods": "error",
+    "unicorn/no-useless-fallback-in-spread": "error",
+    "unicorn/no-useless-length-check": "error",
+    "unicorn/no-useless-spread": "error",
+    "unicorn/no-useless-switch-case": "off",
+    "unicorn/numeric-separators-style": "error",
+    "unicorn/prefer-add-event-listener": "error",
+    "unicorn/prefer-array-find": "error",
+    "unicorn/prefer-array-flat-map": "error",
+    "unicorn/prefer-array-flat": "error",
+    "unicorn/prefer-array-index-of": "error",
+    "unicorn/prefer-array-some": "error",
+    "unicorn/prefer-date-now": "error",
+    "unicorn/prefer-default-parameters": "error",
+    "unicorn/prefer-dom-node-append": "error",
+    "unicorn/prefer-dom-node-dataset": "off",
+    "unicorn/prefer-dom-node-remove": "error",
+    "unicorn/prefer-dom-node-text-content": "error",
+    "unicorn/prefer-event-target": "error",
+    "unicorn/prefer-export-from": "error",
+    "unicorn/prefer-includes": "error",
+    "unicorn/prefer-keyboard-event-key": "error",
+    "unicorn/prefer-logical-operator-over-ternary": "error",
+    "unicorn/prefer-modern-dom-apis": "error",
+    "unicorn/prefer-modern-math-apis": "error",
+    "unicorn/prefer-node-protocol": "error",
+    "unicorn/prefer-object-from-entries": "error",
+    "unicorn/prefer-optional-catch-binding": "error",
+    "unicorn/prefer-query-selector": "error",
+    "unicorn/prefer-spread": "error",
+    "unicorn/prefer-string-replace-all": "error",
+    "unicorn/prefer-string-slice": "error",
+    "unicorn/prefer-string-starts-ends-with": "error",
+    "unicorn/prefer-string-trim-start-end": "error",
+    "unicorn/prefer-structured-clone": "error",
+    "unicorn/prefer-ternary": "error",
+    "unicorn/prefer-type-error": "error",
+    "unicorn/require-array-join-separator": "error",
+    "unicorn/text-encoding-identifier-case": "error",
+    "react/exhaustive-deps": "error",
+    "react/rules-of-hooks": "error",
+    "react/todo": "error",
+    "react/only-export-components": [
+      "warn",
+      {
+        "allowConstantExport": true
+      }
+    ]
+  },
+  "overrides": [
+    {
+      "files": ["rules/**/*.{js,jsx,ts,tsx}", "src/**/*.{js,jsx,ts,tsx}", 
"./*.{js,jsx,ts,tsx}"],
+      "rules": {
+        "airflow/no-react-namespace-types": "error",
+        "accessor-pairs": "error",
+        "array-callback-return": "error",
+        "arrow-body-style": "error",
+        "curly": "error",
+        "default-case": "error",
+        "default-case-last": "error",
+        "eqeqeq": "error",
+        "for-direction": "error",
+        "func-style": "error",
+        "grouped-accessor-pairs": "error",
+        "guard-for-in": "error",
+        "id-length": [
+          "error",
+          {
+            "exceptions": ["_", "x", "y", "z"],
+            "checkGeneric": false
+          }
+        ],
+        "logical-assignment-operators": "error",
+        "max-classes-per-file": ["error", 1],
+        "max-depth": [
+          "error",
+          {
+            "max": 3
+          }
+        ],
+        "max-lines": [
+          "error",
+          {
+            "max": 500,
+            "skipBlankLines": true,
+            "skipComments": true
+          }
+        ],
+        "max-nested-callbacks": [
+          "error",
+          {
+            "max": 3
+          }
+        ],
+        "no-alert": "error",
+        "no-await-in-loop": "error",
+        "no-bitwise": "warn",
+        "no-caller": "error",
+        "no-compare-neg-zero": "error",
+        "no-console": "warn",
+        "no-constructor-return": "error",
+        "no-continue": "error",
+        "no-control-regex": "error",
+        "no-debugger": "error",
+        "no-duplicate-case": "error",
+        "no-duplicate-imports": [
+          "error",
+          {
+            "allowSeparateTypeImports": true
+          }
+        ],
+        "no-empty": "error",
+        "no-empty-character-class": "error",
+        "no-empty-pattern": "error",
+        "no-empty-static-block": "error",
+        "no-eq-null": "error",
+        "no-eval": "error",
+        "no-ex-assign": "error",
+        "no-extend-native": "error",
+        "no-extra-bind": "error",
+        "no-implicit-coercion": "error",
+        "no-import-assign": "error",
+        "no-invalid-regexp": "error",
+        "no-iterator": "error",
+        "no-lone-blocks": "error",
+        "no-loss-of-precision": "error",
+        "no-multi-str": "error",
+        "no-negated-condition": "error",
+        "no-new": "error",
+        "no-new-func": "error",
+        "no-new-native-nonconstructor": "error",
+        "no-new-wrappers": "error",
+        "no-object-constructor": "error",
+        "no-param-reassign": "error",
+        "no-plusplus": "error",
+        "no-proto": "error",
+        "no-prototype-builtins": "error",
+        "no-regex-spaces": "error",
+        "no-restricted-globals": [
+          "error",
+          {
+            "message": "Use `globalThis` instead.",
+            "name": "window"
+          }
+        ],
+        "no-return-assign": ["error", "always"],
+        "no-self-assign": "error",
+        "no-self-compare": "error",
+        "no-shadow-restricted-names": "error",
+        "no-sparse-arrays": "error",
+        "no-template-curly-in-string": "error",
+        "no-unmodified-loop-condition": "error",
+        "no-unneeded-ternary": "error",
+        "no-unsafe-finally": "error",
+        "no-useless-call": "error",
+        "no-useless-catch": "error",
+        "no-useless-computed-key": "error",
+        "no-useless-concat": "error",
+        "no-useless-escape": "error",
+        "no-useless-rename": "error",
+        "no-useless-return": "error",
+        "no-var": "error",
+        "object-shorthand": "error",
+        "one-var": ["error", "never"],
+        "operator-assignment": "error",
+        "prefer-arrow-callback": "error",
+        "prefer-const": "error",
+        "prefer-exponentiation-operator": "warn",
+        "prefer-named-capture-group": "warn",
+        "prefer-object-has-own": "error",
+        "prefer-object-spread": "warn",
+        "prefer-regex-literals": "error",
+        "prefer-rest-params": "error",
+        "prefer-spread": "error",
+        "prefer-template": "error",
+        "radix": "error",
+        "require-unicode-regexp": "error",
+        "require-yield": "error",
+        "symbol-description": "error",
+        "yoda": "error"
+      },
+      "jsPlugins": [
+        {
+          "name": "airflow",
+          "specifier": "./rules/airflow-plugin/index.js"
+        }
+      ]
+    },
+    {
+      "files": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
+      "rules": {
+        "class-methods-use-this": "error",
+        "default-param-last": "error",
+        "max-params": [
+          "error",
+          {
+            "max": 3
+          }
+        ],
+        "no-array-constructor": "error",
+        "no-empty-function": "error",
+        "no-loop-func": "error",
+        "no-restricted-imports": [
+          "error",
+          {
+            "paths": [
+              {
+                "importNames": ["default"],
+                "message": "The automatic JSX runtime makes the React 
namespace import unnecessary. Import what you need by name, e.g. `import { 
forwardRef, type ReactNode } from \"react\"`.",
+                "name": "react"
+              }
+            ],
+            "patterns": [
+              {
+                "group": ["jquery", "lodash", "lodash-es", "lodash.*", 
"lodash/*", "underscore"],
+                "message": "Just use vanilla JavaScript."
+              },
+              {
+                "group": ["jquery-ui", "moment"],
+                "message": "Use a modern dependency instead."
+              }
+            ]
+          }
+        ],
+        "no-shadow": [
+          "error",
+          {
+            "allow": ["_"],
+            "hoist": "all"
+          }
+        ],
+        "no-unused-expressions": "error",
+        "no-use-before-define": "error",
+        "no-useless-constructor": "error",
+        "typescript/adjacent-overload-signatures": "error",
+        "typescript/array-type": [
+          "error",
+          {
+            "default": "generic"
+          }
+        ],
+        "typescript/ban-ts-comment": [
+          "error",
+          {
+            "minimumDescriptionLength": 10,
+            "ts-check": false,
+            "ts-expect-error": "allow-with-description",
+            "ts-ignore": true,
+            "ts-nocheck": true
+          }
+        ],
+        "typescript/class-literal-property-style": ["error", "getters"],
+        "typescript/consistent-generic-constructors": ["error", "constructor"],
+        "typescript/consistent-indexed-object-style": ["error", "record"],
+        "typescript/consistent-type-assertions": [
+          "error",
+          {
+            "assertionStyle": "as",
+            "objectLiteralTypeAssertions": "allow"
+          }
+        ],
+        "typescript/consistent-type-definitions": ["error", "type"],
+        "typescript/consistent-type-imports": [
+          "error",
+          {
+            "fixStyle": "inline-type-imports"
+          }
+        ],
+        "typescript/explicit-member-accessibility": [
+          "error",
+          {
+            "accessibility": "explicit",
+            "overrides": {
+              "accessors": "explicit",
+              "constructors": "explicit"
+            }
+          }
+        ],
+        "typescript/method-signature-style": ["error", "property"],
+        "typescript/no-duplicate-enum-values": "error",
+        "typescript/no-dynamic-delete": "error",
+        "typescript/no-empty-object-type": "error",
+        "typescript/no-explicit-any": "error",
+        "typescript/no-extraneous-class": "error",
+        "typescript/no-import-type-side-effects": "error",
+        "typescript/no-invalid-void-type": "error",
+        "typescript/no-misused-new": "error",
+        "typescript/no-namespace": "error",
+        "typescript/no-non-null-asserted-nullish-coalescing": "error",
+        "typescript/no-non-null-assertion": "warn",
+        "typescript/no-require-imports": "error",
+        "typescript/no-restricted-types": [
+          "error",
+          {
+            "types": {
+              "FC": "Do not type components with `FC`. Annotate the props 
parameter instead: `const Foo = ({ bar }: FooProps) => ...`, or use 
`ComponentType` for a component value.",
+              "FunctionComponent": "Do not type components with 
`FunctionComponent`. Annotate the props parameter instead: `const Foo = ({ bar 
}: FooProps) => ...`, or use `ComponentType` for a component value.",
+              "React.FC": "Do not type components with `React.FC`. Annotate 
the props parameter instead: `const Foo = ({ bar }: FooProps) => ...`, or use 
`ComponentType` for a component value.",
+              "React.FunctionComponent": "Do not type components with 
`React.FunctionComponent`. Annotate the props parameter instead: `const Foo = 
({ bar }: FooProps) => ...`, or use `ComponentType` for a component value.",
+              "React.VFC": "Do not type components with `React.VFC`. Annotate 
the props parameter instead: `const Foo = ({ bar }: FooProps) => ...`, or use 
`ComponentType` for a component value.",
+              "VFC": "Do not type components with `VFC`. Annotate the props 
parameter instead: `const Foo = ({ bar }: FooProps) => ...`, or use 
`ComponentType` for a component value."
+            }
+          }
+        ],
+        "typescript/no-this-alias": "error",
+        "typescript/no-unnecessary-parameter-property-assignment": "error",
+        "typescript/no-unnecessary-type-constraint": "error",
+        "typescript/no-unsafe-declaration-merging": "error",
+        "typescript/no-useless-empty-export": "error",
+        "typescript/no-wrapper-object-types": "error",
+        "typescript/parameter-properties": [
+          "error",
+          {
+            "prefer": "class-property"
+          }
+        ],
+        "typescript/prefer-as-const": "error",
+        "typescript/prefer-enum-initializers": "error",
+        "typescript/prefer-for-of": "error",
+        "typescript/prefer-function-type": "error",
+        "typescript/prefer-literal-enum-member": [
+          "error",
+          {
+            "allowBitwiseExpressions": true
+          }
+        ],
+        "typescript/prefer-namespace-keyword": "error",
+        "typescript/triple-slash-reference": "error",
+        "typescript/unified-signatures": "error"
+      },
+      "plugins": ["typescript"]
+    },
+    {
+      "files": ["src/**/*.{j,t}s{x,}"],
+      "rules": {
+        "i18next/no-literal-string": [
+          "error",
+          {
+            "markupOnly": true
+          }
+        ]
+      },
+      "jsPlugins": ["eslint-plugin-i18next"],
+      "excludeFiles": ["src/**/*.test.tsx"]
+    }
+  ]
+}
diff --git a/airflow-core/src/airflow/ui/eslint.config.js 
b/airflow-core/src/airflow/ui/eslint.config.js
index d2b74a317b3..6a76c803b17 100644
--- a/airflow-core/src/airflow/ui/eslint.config.js
+++ b/airflow-core/src/airflow/ui/eslint.config.js
@@ -19,11 +19,12 @@
 /**
  * @import { FlatConfig } from "@typescript-eslint/utils/ts-eslint";
  */
+import i18next from "eslint-plugin-i18next";
+import oxlint from "eslint-plugin-oxlint";
+import perfectionist from "eslint-plugin-perfectionist";
+
 import { coreRules } from "./rules/core.js";
-import { i18nextRules } from "./rules/i18next.js";
-import { perfectionistRules } from "./rules/perfectionist.js";
 import { reactRules } from "./rules/react.js";
-import { stylisticRules } from "./rules/stylistic.js";
 import { typescriptRules } from "./rules/typescript.js";
 import { unicornRules } from "./rules/unicorn.js";
 
@@ -34,13 +35,18 @@ import { unicornRules } from "./rules/unicorn.js";
 export default /** @type {const} @satisfies {ReadonlyArray<FlatConfig.Config>} 
*/ ([
   // Global ignore of dist directory
   { ignores: ["**/dist/", "**coverage/", "**/openapi-gen/", "**/.vite/**"] },
+  // Most rules run in oxlint now, so ESLint would call their disable comments 
unused and `--fix` would
+  // delete them.
+  { linterOptions: { reportUnusedDisableDirectives: "off" } },
   // Base rules
   coreRules,
   typescriptRules,
   // Da rest
-  perfectionistRules,
   reactRules,
-  stylisticRules,
   unicornRules,
-  i18nextRules,
+  // Registered without rules so `eslint-disable` comments naming them still 
resolve; oxlint runs them.
+  { plugins: { i18next, perfectionist } },
+  // oxlint runs every rule it supports (see `.oxlintrc.json`, including the 
perfectionist, stylistic,
+  // i18next and local `airflow` plugins); this turns those off here so ESLint 
only runs the rest.
+  ...(await oxlint.buildFromConfigFile("./.oxlintrc.json")),
 ]);
diff --git a/airflow-core/src/airflow/ui/package.json 
b/airflow-core/src/airflow/ui/package.json
index 528cd8260ef..18cbc622d77 100644
--- a/airflow-core/src/airflow/ui/package.json
+++ b/airflow-core/src/airflow/ui/package.json
@@ -10,8 +10,8 @@
   "scripts": {
     "dev": "vite --port 5173 --strictPort",
     "build": "vite build",
-    "lint": "eslint --quiet && tsc --p tsconfig.app.json",
-    "lint:fix": "eslint --fix && tsc --p tsconfig.app.json",
+    "lint": "oxlint --quiet && eslint --quiet && tsc --p tsconfig.app.json",
+    "lint:fix": "oxlint --fix && eslint --fix && tsc --p tsconfig.app.json",
     "format": "oxfmt --write .",
     "format:check": "oxfmt --check .",
     "preview": "vite preview",
@@ -97,6 +97,7 @@
     "eslint": "^10.11.0",
     "eslint-plugin-i18next": "^6.1.5",
     "eslint-plugin-jsx-a11y": "^6.10.2",
+    "eslint-plugin-oxlint": "1.90.0",
     "eslint-plugin-perfectionist": "^5.12.1",
     "eslint-plugin-react": "^7.37.5",
     "eslint-plugin-react-hooks": "^7.1.1",
@@ -107,6 +108,7 @@
     "msw": "^3.0.3",
     "openapi-merge-cli": "^2.0.3",
     "oxfmt": "0.75.0",
+    "oxlint": "1.90.0",
     "ts-morph": "^27.0.2",
     "typescript": "^6.0.3",
     "typescript-eslint": "^8.72.1",
diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml 
b/airflow-core/src/airflow/ui/pnpm-lock.yaml
index c2ba2d5959f..27a99cb0ed1 100644
--- a/airflow-core/src/airflow/ui/pnpm-lock.yaml
+++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml
@@ -251,6 +251,9 @@ importers:
       eslint-plugin-jsx-a11y:
         specifier: ^6.10.2
         version: 6.10.2([email protected]([email protected]))
+      eslint-plugin-oxlint:
+        specifier: 1.90.0
+        version: 1.90.0([email protected])
       eslint-plugin-perfectionist:
         specifier: ^5.12.1
         version: 5.12.1([email protected]([email protected]))([email protected])
@@ -281,6 +284,9 @@ importers:
       oxfmt:
         specifier: 0.75.0
         version: 0.75.0
+      oxlint:
+        specifier: 1.90.0
+        version: 1.90.0
       ts-morph:
         specifier: ^27.0.2
         version: 27.0.2
@@ -896,6 +902,120 @@ packages:
     cpu: [x64]
     os: [win32]
 
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-sgfalb1HuxYjiAa4wESZ626z1FGusqNcXYHyB5oIOEprS2sbPg0UYEfo83uF1GNG70DIfZxHkXuE6KFrlSdn8Q==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm]
+    os: [android]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-RcKhz1ETMq+fazE9TKS2+5NIRGwigm/nW8egqF0LI+A8Yw2iW5r0E/C3XAPRiwIm/vcPiutIRIa6IQCIKhZsTQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [android]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-PSrxMvP6mx+p1karZcUxLdy9lpaP+JWQPti4al9FgTaPhxE+pT/BjhGExloVVSWgZgIfOlLOjVyyq8BXlodhLQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-OLbU9qsrV2Q8QxT2OEzOPdO8zCCpyI8qB+hDI8yr6RsYELtkf0FAZhJdfjM/RnLHKzt44HWwAPnk92vBtKofvw==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [x64]
+    os: [darwin]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-fBA4pWyVDARmNucQqpyFuPC49xYf1r+sX7dxPL5fNPQrSMLum39Sn56tZ9JPleWC7FzLPwSQ7plYYfirYBHF8A==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-zhxH78eP2nR6/pGdhBc7MIfjhhcgATeu8fkmkFJbuzfk6Cj0FzBxyEhL85yDqrCybjx+G2YDzZGsxIO9kvO4Iw==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm]
+    os: [linux]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-MvYmpHyJqUJjEvkZbJHdwfdL45cGn8EVBkYmoXFiaYFW9suoD3dbJl5mBdpy0DVgdjlyddmldbRtk3cCgETadQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm]
+    os: [linux]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-L13Sy+Kg0nZhwTsToniqjUFPA2TuM6962qCZ5Wp5KeB517Tw7ApdLTTtlb+bF86fcZZa2EtvCVvAJxk4Dya/MQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [linux]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-wFDdZWGpzIPFdJFjSLWnKB0TGzFriTKUIrl04iMqMyM9BBIJTuerfCHYa0WpDSjr5x3lcW/CHLyacSt886WpGg==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [linux]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-FRywfn+MoKn8xpBSzgNwNVliSKRI2QXkxi/iF7o6FEQs8Bh8z2RpVTaFAcO3pchXjvB2eGNFIoLLQABIQ+u7VA==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-IS2PaUgpc/qeLADTyhUNmdx3x4ijs9pjJdPKBYHaT+mAE7k3FsjnHPTjhpsak1b++h6CIPhv6WX9Z+y7Pw0voQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-s7JFKRkGS8O7i2ZVODfuO++NC7sH6HDqpMn8lYxw91w0SXJOxnAVjQ8utvmDwLMMT0jaSkuOKuUYJDGDtu4sHQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-b0GxrOQr5VONCkd7KLSqmP6b6nqZuTpuS8QPQHzltmqdXP5bY0Jl0CZ0QmlCTrS3ExaLYbioFYOARzNbYvcApQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [s390x]
+    os: [linux]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-iaJeShJtgUp8rZbt9YUdTAGOpmX5KogCLba17Jp1tWdJ3F4M3D2g2TKUyI1FMTHxKH9NNHuQcuC2kZRg6IV7yQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [x64]
+    os: [linux]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-/aZoXnL5WBLCXuzfHqvEFXcm4iwYmr/cYByHDTXzLFzJdE5DC7bm6phpWqHv6BeqpVgY9IpBAxGDv2WEkl5JCg==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [x64]
+    os: [linux]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-SMhk7ozwoxYDKfYqGZCQEfULOKDneF9uhaoIf2neoDLWvNtjY9yVeL7YHmE5L9WdKvMpWNwwZxhenxBu6AgfOg==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-KBvR3A9fO7fCi2vCP8ifWvlia7tGEQV9NaYh+IGwa/7Groy48Mt/UaYqMK9/zOtAWl20BUCf6RPqzJ32fRw33Q==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [arm64]
+    os: [win32]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-+x42rDHQGbTCuT3EzpWAzhP3NoVPE/cJsG4gnrakOSg0olV+GCdMpPU23Nvrl3zZMnXxFZdTtDv4D3SuaA5ZeQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [ia32]
+    os: [win32]
+
+  '@oxlint/[email protected]':
+    resolution: {integrity: 
sha512-30JiaFEBUB9peEOcvVM8ggo88SoN/IdSr9DW505JdBji/R6dM66gKEiG65hw6qcaF2f4mO0cEJ+RwRZprmun9g==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    cpu: [x64]
+    os: [win32]
+
   '@pandacss/[email protected]':
     resolution: {integrity: 
sha512-JFmahiSKrpBE48NHl8NIsNO5e5y1SrOrOGvyle9h9rkNrYw+RJrm5CLeGimmyhg2FIymsmZE76sDnw5Zuu2LVA==}
     engines: {node: '>=20'}
@@ -2513,6 +2633,11 @@ packages:
     peerDependencies:
       eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
 
+  [email protected]:
+    resolution: {integrity: 
sha512-ejRIGrB/d8HTINUgytWXT0TD8CJM4XG3O7S4VP2oOvCO9wa3jKe4m5R3SaOIRyqvu7ktQNf0Mr7abW2GKeYQYA==}
+    peerDependencies:
+      oxlint: ~1.90.0
+
   [email protected]:
     resolution: {integrity: 
sha512-krdSE7Dz+TCtcVr2+rfNhlOIFFBJcUdoRvjaN7JQnsaVnxJw+BZh9wMkY3MSLCJat+yW+UPTQLKS82Epc7MGZA==}
     engines: {node: ^20.0.0 || >=22.0.0}
@@ -3123,6 +3248,9 @@ packages:
     engines: {node: '>=6'}
     hasBin: true
 
+  [email protected]:
+    resolution: {integrity: 
sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
+
   [email protected]:
     resolution: {integrity: 
sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
     engines: {node: '>=0.10.0'}
@@ -3618,6 +3746,19 @@ packages:
       vite-plus:
         optional: true
 
+  [email protected]:
+    resolution: {integrity: 
sha512-RkTN9EmgIUpLuhNoLhO3OliMGJlvb3Dk7XVjBZnfoMCHrVpuz5ESd97klfiyYeV7XfA2lNLnDCPMkUI+Qw5How==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    hasBin: true
+    peerDependencies:
+      oxlint-tsgolint: '>=7.0.2005'
+      vite-plus: '*'
+    peerDependenciesMeta:
+      oxlint-tsgolint:
+        optional: true
+      vite-plus:
+        optional: true
+
   [email protected]:
     resolution: {integrity: 
sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
     engines: {node: '>=10'}
@@ -5253,6 +5394,63 @@ snapshots:
   '@oxfmt/[email protected]':
     optional: true
 
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
+  '@oxlint/[email protected]':
+    optional: true
+
   '@pandacss/[email protected]': {}
 
   '@pkgjs/[email protected]':
@@ -7329,6 +7527,11 @@ snapshots:
       safe-regex-test: 1.1.0
       string.prototype.includes: 2.0.1
 
+  [email protected]([email protected]):
+    dependencies:
+      jsonc-parser: 3.3.1
+      oxlint: 1.90.0
+
   
[email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       '@typescript-eslint/types': 8.71.0
@@ -8036,6 +8239,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]:
@@ -8774,6 +8979,28 @@ snapshots:
       '@oxfmt/binding-win32-ia32-msvc': 0.75.0
       '@oxfmt/binding-win32-x64-msvc': 0.75.0
 
+  [email protected]:
+    optionalDependencies:
+      '@oxlint/binding-android-arm-eabi': 1.90.0
+      '@oxlint/binding-android-arm64': 1.90.0
+      '@oxlint/binding-darwin-arm64': 1.90.0
+      '@oxlint/binding-darwin-x64': 1.90.0
+      '@oxlint/binding-freebsd-x64': 1.90.0
+      '@oxlint/binding-linux-arm-gnueabihf': 1.90.0
+      '@oxlint/binding-linux-arm-musleabihf': 1.90.0
+      '@oxlint/binding-linux-arm64-gnu': 1.90.0
+      '@oxlint/binding-linux-arm64-musl': 1.90.0
+      '@oxlint/binding-linux-ppc64-gnu': 1.90.0
+      '@oxlint/binding-linux-riscv64-gnu': 1.90.0
+      '@oxlint/binding-linux-riscv64-musl': 1.90.0
+      '@oxlint/binding-linux-s390x-gnu': 1.90.0
+      '@oxlint/binding-linux-x64-gnu': 1.90.0
+      '@oxlint/binding-linux-x64-musl': 1.90.0
+      '@oxlint/binding-openharmony-arm64': 1.90.0
+      '@oxlint/binding-win32-arm64-msvc': 1.90.0
+      '@oxlint/binding-win32-ia32-msvc': 1.90.0
+      '@oxlint/binding-win32-x64-msvc': 1.90.0
+
   [email protected]:
     dependencies:
       yocto-queue: 0.1.0
diff --git a/airflow-core/src/airflow/ui/rules/core.js 
b/airflow-core/src/airflow/ui/rules/core.js
index fb99da6449b..4f9cff6264f 100644
--- a/airflow-core/src/airflow/ui/rules/core.js
+++ b/airflow-core/src/airflow/ui/rules/core.js
@@ -19,7 +19,6 @@
 /**
  * @import { FlatConfig } from "@typescript-eslint/utils/ts-eslint";
  */
-import airflowPlugin, { airflowNamespace } from "./airflow-plugin/index.js";
 import { ERROR, WARN } from "./levels.js";
 
 const allExtensions = "*.{j,t}s{x,}";
@@ -35,25 +34,7 @@ export const coreRules = /** @type {const} @satisfies 
{FlatConfig.Config} */ ({
     // Files in the root directory
     allExtensions,
   ],
-  plugins: { [airflowNamespace]: airflowPlugin },
   rules: {
-    /**
-     * Disallow the `React` type namespace. `@types/react` declares `export as
-     * namespace React`, so `React.ReactNode` type-checks even in files that
-     * never import React — only a syntax rule can catch those. `React.FC` and
-     * friends are reported by `@typescript-eslint/no-restricted-types`.
-     *
-     * @example
-     * ```typescript
-     * // ❌ Incorrect
-     * const node: React.ReactNode = null;
-     *
-     * // ✅ Correct
-     * const node: ReactNode = null;
-     * ```
-     */
-    [`${airflowNamespace}/no-react-namespace-types`]: ERROR,
-
     /**
      * Enforce getter and setter pairs in objects and classes.
      *
diff --git a/airflow-core/src/airflow/ui/rules/i18next.js 
b/airflow-core/src/airflow/ui/rules/i18next.js
deleted file mode 100644
index 740ee20c862..00000000000
--- a/airflow-core/src/airflow/ui/rules/i18next.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/*!
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-/**
- * @import { FlatConfig } from "@typescript-eslint/utils/ts-eslint";
- */
-import i18nextPlugin from "eslint-plugin-i18next";
-
-import { ERROR } from "./levels.js";
-
-const allExtensions = "*.{j,t}s{x,}";
-
-/**
- * ESLint rules for i18next to enforce internationalization best practices.
- * This is a customized configuration.
- *
- * @see 
[eslint-plugin-i18next](https://github.com/edvardchen/eslint-plugin-i18next)
- */
-export const i18nextRules = /** @type {const} @satisfies {FlatConfig.Config} 
*/ ({
-  files: [
-    // Check files in the ui/src directory
-    `src/**/${allExtensions}`,
-  ],
-  ignores: [
-    // Ignore test files
-    "src/**/*.test.tsx",
-  ],
-  plugins: {
-    i18next: i18nextPlugin,
-  },
-  rules: {
-    /**
-     * Enforce no literal strings in JSX/TSX markup.
-     * This rule helps ensure all user-facing strings are properly 
internationalized.
-     *
-     * @example
-     * ```typescript
-     * // ❌ Incorrect
-     * <div>Hello, world!</div>
-     *
-     * // ✅ Correct
-     * <div>{translate('greeting')}</div>
-     * ```
-     * @see 
[i18next/no-literal-string](https://github.com/edvardchen/eslint-plugin-i18next#no-literal-string)
-     */
-    "i18next/no-literal-string": [
-      ERROR,
-      {
-        markupOnly: true,
-      },
-    ],
-  },
-});
diff --git a/airflow-core/src/airflow/ui/rules/perfectionist.js 
b/airflow-core/src/airflow/ui/rules/perfectionist.js
deleted file mode 100644
index f8b524ce58c..00000000000
--- a/airflow-core/src/airflow/ui/rules/perfectionist.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/*!
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-/**
- * @import { FlatConfig } from "@typescript-eslint/utils/ts-eslint";
- */
-import perfectionist from "eslint-plugin-perfectionist";
-
-import { ERROR } from "./levels.js";
-
-/**
- * ESLint `perfectionist` namespace.
- */
-export const perfectionistNamespace = "perfectionist";
-
-/**
- * ESLint `perfectionist` rules.
- * @see [perfectionist](https://perfectionist.dev/rules)
- */
-export const perfectionistRules = /** @type {const} @satisfies 
{FlatConfig.Config} */ ({
-  plugins: {
-    [perfectionistNamespace]: perfectionist,
-  },
-  rules: {
-    /**
-     * Enforce sorted array values if the `includes` method is
-     * immediately called after the array is created.
-     *
-     * @see 
[perfectionist/sort-array-includes](https://perfectionist.dev/rules/sort-array-includes)
-     */
-    [`${perfectionistNamespace}/sort-array-includes`]: ERROR,
-
-    /**
-     * Enforce sorted class members.
-     *
-     * @see 
[perfectionist/sort-classes](https://perfectionist.dev/rules/sort-classes)
-     */
-    [`${perfectionistNamespace}/sort-classes`]: ERROR,
-
-    /**
-     * Enforce sorted TypeScript enum members.
-     *
-     * @see 
[perfectionist/sort-enums](https://perfectionist.dev/rules/sort-enums)
-     */
-    [`${perfectionistNamespace}/sort-enums`]: ERROR,
-
-    /*
-     * `sort-imports` is intentionally left off: import order is owned by
-     * oxfmt's `sortImports` (see `.oxfmtrc.json`). The prek hook runs
-     * `eslint --fix` before `oxfmt --write`, so enabling it here would let
-     * oxfmt overwrite ESLint's ordering and leave `pnpm lint` failing with no
-     * possible fix.
-     */
-
-    /**
-     * Enforce sorted TypeScript interface properties.
-     *
-     * @see 
[perfectionist/sort-interfaces](https://perfectionist.dev/rules/sort-interfaces)
-     */
-    [`${perfectionistNamespace}/sort-interfaces`]: ERROR,
-
-    /**
-     * Enforce sorted intersection types in TypeScript.
-     *
-     * @see 
[perfectionist/sort-intersection-types](https://perfectionist.dev/rules/sort-intersection-types)
-     */
-    [`${perfectionistNamespace}/sort-intersection-types`]: ERROR,
-
-    /**
-     * Enforce sorted JSX props within JSX elements.
-     *
-     * @see 
[perfectionist/sort-jsx-props](https://perfectionist.dev/rules/sort-jsx-props)
-     */
-    [`${perfectionistNamespace}/sort-jsx-props`]: ERROR,
-
-    /**
-     * Enforce sorted elements within JavaScript Map object.
-     *
-     * @see 
[perfectionist/sort-maps](https://perfectionist.dev/rules/sort-maps)
-     */
-    [`${perfectionistNamespace}/sort-maps`]: ERROR,
-
-    /**
-     * Enforce sorted object types.
-     *
-     * @see 
[perfectionist/sort-object-types](https://perfectionist.dev/rules/sort-object-types)
-     */
-    [`${perfectionistNamespace}/sort-object-types`]: ERROR,
-
-    /**
-     * Enforce sorted object types.
-     *
-     * @see 
[perfectionist/sort-objects](https://perfectionist.dev/rules/sort-objects)
-     */
-    [`${perfectionistNamespace}/sort-objects`]: [
-      ERROR,
-      {
-        type: "natural",
-      },
-    ],
-
-    /**
-     * Enforce sorted switch case statements.
-     *
-     * @see 
[perfectionist/sort-switch-case](https://perfectionist.dev/rules/sort-switch-case)
-     */
-    [`${perfectionistNamespace}/sort-switch-case`]: ERROR,
-
-    /**
-     * Enforce sorted TypeScript union types.
-     *
-     * nullish groups will leave null and undefined at the end
-     *
-     * @see 
[perfectionist/sort-union-types](https://perfectionist.dev/rules/sort-union-types)
-     */
-    [`${perfectionistNamespace}/sort-union-types`]: [
-      ERROR,
-      {
-        groups: [
-          ["conditional", "function", "import"],
-          ["intersection", "keyword", "named"],
-          ["literal", "object", "operator", "tuple", "union"],
-          "nullish",
-        ],
-      },
-    ],
-  },
-});
diff --git a/airflow-core/src/airflow/ui/rules/plugin-patch.d.ts 
b/airflow-core/src/airflow/ui/rules/plugin-patch.d.ts
index ce155a444df..76c378b59cd 100644
--- a/airflow-core/src/airflow/ui/rules/plugin-patch.d.ts
+++ b/airflow-core/src/airflow/ui/rules/plugin-patch.d.ts
@@ -28,10 +28,6 @@ declare module "eslint-plugin-local-patch" {
   export default plugin;
 }
 
-declare module "@stylistic/eslint-plugin" {
-  export { default } from "eslint-plugin-local-patch";
-}
-
 declare module "eslint-plugin-jsx-a11y" {
   export { default } from "eslint-plugin-local-patch";
 }
diff --git a/airflow-core/src/airflow/ui/rules/stylistic.js 
b/airflow-core/src/airflow/ui/rules/stylistic.js
deleted file mode 100644
index 416b9a6196f..00000000000
--- a/airflow-core/src/airflow/ui/rules/stylistic.js
+++ /dev/null
@@ -1,90 +0,0 @@
-/*!
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-/**
- * @import { FlatConfig } from "@typescript-eslint/utils/ts-eslint";
- */
-import stylistic from "@stylistic/eslint-plugin";
-
-import { ERROR } from "./levels.js";
-
-/**
- * ESLint `@stylistic/eslint-plugin` namespace.
- */
-export const stylisticESLintNamespace = "@stylistic";
-
-/**
- * ESLint `@stylistic/eslint-plugin` rules.
- * @see [@stylistic/eslint-plugin](https://eslint.style/packages/default#rules)
- */
-export const stylisticRules = /** @type {const} @satisfies {FlatConfig.Config} 
*/ ({
-  plugins: { [stylisticESLintNamespace]: stylistic },
-  rules: {
-    /**
-     * Require padding lines between statements:
-     *
-     * -   Always a new line before a `return` statement.
-     * -   Always a new line after an `import`, `const`, `let` or `var` 
statement.
-     * -   Indifferent if followed by same type (`const`, `let` or `var`).
-     * -   Indifferent in `import` followed by other `import`.
-     * -   Always a new line before an `export` statement.
-     *
-     * @see 
[padding-line-between-statements](https://eslint.style/rules/default/padding-line-between-statements)
-     */
-    [`${stylisticESLintNamespace}/padding-line-between-statements`]: [
-      ERROR,
-      {
-        blankLine: "always",
-        next: "return",
-        prev: "*",
-      },
-      {
-        blankLine: "always",
-        next: "*",
-        prev: ["import", "const", "let", "var"],
-      },
-      {
-        blankLine: "any",
-        next: ["const", "let", "var"],
-        prev: ["const", "let", "var"],
-      },
-      {
-        blankLine: "any",
-        next: "import",
-        prev: "import",
-      },
-      {
-        blankLine: "always",
-        next: "export",
-        prev: "*",
-      },
-      {
-        blankLine: "any",
-        next: "export",
-        prev: "export",
-      },
-    ],
-
-    /**
-     * Requires a whitespace (space or tab) beginning a comment.
-     *
-     * @see [spaced-comment](https://eslint.style/rules/default/spaced-comment)
-     */
-    [`${stylisticESLintNamespace}/spaced-comment`]: [ERROR, "always", { 
exceptions: ["!"] }],
-  },
-});
diff --git a/airflow-core/src/airflow/ui/src/components/GraphTaskFilters.tsx 
b/airflow-core/src/airflow/ui/src/components/GraphTaskFilters.tsx
index 09ac05e2a12..c9ab7047a34 100644
--- a/airflow-core/src/airflow/ui/src/components/GraphTaskFilters.tsx
+++ b/airflow-core/src/airflow/ui/src/components/GraphTaskFilters.tsx
@@ -90,7 +90,7 @@ export const GraphTaskFilters = () => {
   };
 
   const handleNumberChange =
-    (key: string, condition: (n: number) => boolean) => (details: 
NumberInputValueChangeDetails) => {
+    (key: string, condition: (value: number) => boolean) => (details: 
NumberInputValueChangeDetails) => {
       setSearchParams(
         (prev) => {
           const next = new URLSearchParams(prev);
diff --git a/airflow-core/src/airflow/ui/src/components/ReactMarkdownBlocks.tsx 
b/airflow-core/src/airflow/ui/src/components/ReactMarkdownBlocks.tsx
index 98eccc4c515..7dc588e0860 100644
--- a/airflow-core/src/airflow/ui/src/components/ReactMarkdownBlocks.tsx
+++ b/airflow-core/src/airflow/ui/src/components/ReactMarkdownBlocks.tsx
@@ -159,7 +159,7 @@ const MarkdownMathBlock = ({ style, value }: { readonly 
style: SyntaxTheme; read
       >
         <Box data-testid="markdown-math-content" maxWidth="100%" minWidth={0} 
overflowX="auto" p={3}>
           <Box
-            /* Trusting KaTeX to escape user content in the generated HTML */
+            /* oxlint-disable-next-line react/no-danger -- Trusting KaTeX to 
escape user content in the generated HTML */
             dangerouslySetInnerHTML={{ __html: markup }}
           />
         </Box>
@@ -252,7 +252,7 @@ export const MarkdownMermaid = ({
                 width: "100%",
               },
             }}
-            /* Trusting mermaid's strict-mode sanitizer */
+            /* oxlint-disable-next-line react/no-danger -- Trusting mermaid's 
strict-mode sanitizer */
             dangerouslySetInnerHTML={{ __html: svg }}
             data-testid="markdown-mermaid-diagram"
           />
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
index da4e42a4c8e..f74d515def8 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
@@ -211,10 +211,11 @@ export const Nav = () => {
         <UserSettingsButton externalViews={userItems} />
         {hasCustomLogo ? (
           <Text asChild color="fg.muted" fontSize="2xs" lineHeight="1" pb={2} 
textAlign="center">
-            {/* eslint-disable-next-line i18next/no-literal-string -- 
Trademark must not be translated */}
+            {/* eslint-disable i18next/no-literal-string -- Trademark must not 
be translated */}
             <a href="https://airflow.apache.org/"; rel="noopener noreferrer" 
target="_blank">
               Apache Airflow®
             </a>
+            {/* eslint-enable i18next/no-literal-string */}
           </Text>
         ) : undefined}
       </Flex>
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx
index c9fb6b7a3a6..da5ef69d75c 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx
@@ -167,6 +167,7 @@ export const Overview = () => {
             <Skeleton height="310px" w="full" />
           ) : (
             <DurationChart
+              // oxlint-disable-next-line unicorn/prefer-spread -- a spread 
would drop the optional chain
               entries={gridRuns?.slice().reverse()}
               isAutoRefreshing={isAutoRefreshing}
               kind="Dag Run"
diff --git a/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx 
b/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx
index 78b5f46357f..b688eabcce3 100644
--- a/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx
@@ -137,7 +137,11 @@ export const Overview = () => {
           {isLoadingTaskInstances ? (
             <Skeleton height="310px" w="full" />
           ) : (
-            <DurationChart entries={tiData?.task_instances.slice().reverse()} 
kind="Task Instance" />
+            <DurationChart
+              // oxlint-disable-next-line unicorn/prefer-spread -- a spread 
would drop the optional chain
+              entries={tiData?.task_instances.slice().reverse()}
+              kind="Task Instance"
+            />
           )}
         </Box>
       </HStack>
diff --git a/contributing-docs/15_node_environment_setup.rst 
b/contributing-docs/15_node_environment_setup.rst
index 3bcb15be124..715f57c2489 100644
--- a/contributing-docs/15_node_environment_setup.rst
+++ b/contributing-docs/15_node_environment_setup.rst
@@ -72,10 +72,11 @@ Follow the `nvm docs <https://github.com/nvm-sh/nvm>`__ to 
manage your node vers
     # Check formatting without writing changes
     pnpm format:check
 
-    # Check JS/TS code in .ts, .tsx, .html files and report any errors/warnings
+    # Lint .js, .ts, .tsx files with oxlint, then ESLint for the type-aware 
rules oxlint cannot run yet,
+    # then type-check
     pnpm lint
 
-    # Check JS/TS code in .ts, .tsx, .html files and report any 
errors/warnings and fix them if possible
+    # Same, applying autofixes where possible
     pnpm lint:fix
 
     # Run tests for all .test.ts, test.tsx files
diff --git a/scripts/ci/prek/ts_compile_lint_ui.py 
b/scripts/ci/prek/ts_compile_lint_ui.py
index 96227b33f3d..a62714b5af1 100755
--- a/scripts/ci/prek/ts_compile_lint_ui.py
+++ b/scripts/ci/prek/ts_compile_lint_ui.py
@@ -82,6 +82,7 @@ if __name__ == "__main__":
         run_command(["pnpm", "codegen"], cwd=dir)
     if all_non_yaml_files:
         if all_eslint_files:
+            run_command(["pnpm", "oxlint", "--fix", *all_eslint_files], 
cwd=dir)
             run_command(["pnpm", "eslint", "--fix", *all_eslint_files], 
cwd=dir)
         run_command(
             ["pnpm", "oxfmt", "--write", "--no-error-on-unmatched-pattern", 
*all_non_yaml_files], cwd=dir

Reply via email to