Download eslint.config.js from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 15.5 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/eslint.config.js
- Command line
-
hf download hf://SaylorTwift/openhands/eslint.config.js
-
curl -L -o eslint.config.js https://huggingface.co/SaylorTwift/openhands/resolve/main/eslint.config.js
15.5 kB
| // Flat ESLint config (ESLint 9+). | |
| // | |
| // Migration notes (see commit history for full context): | |
| // - Replaces the legacy `.eslintrc` file, which ESLint 9 deprecated and | |
| // ESLint 10 will remove entirely. | |
| // - Drops `eslint-config-airbnb` / `eslint-config-airbnb-typescript`: they | |
| // pin ESLint to v7/v8 and `@typescript-eslint/*` to v7, so they block any | |
| // future ESLint major bumps. The opinionated airbnb rules the codebase | |
| // actually leaned on (no-param-reassign, import/extensions, etc.) are | |
| // re-declared explicitly below, and the long list of airbnb rules the old | |
| // config already turned off has just been deleted instead of re-disabled. | |
| // - Replaces `eslint-plugin-import` (only declares an `^8 || ^9` peer) with | |
| // the actively-maintained, flat-config-native fork `eslint-plugin-import-x`. | |
| // The rule names are kept under the `import/` prefix below to minimise | |
| // churn on existing `// eslint-disable-next-line import/...` comments. | |
| // - `eslint-plugin-i18next` still ships only legacy config, so it is pulled | |
| // in via `FlatCompat` from `@eslint/eslintrc`. | |
| // - Targets ESLint 9, not 10. The original dependabot PR proposed eslint@10, | |
| // but as of this commit `eslint-plugin-react` still calls | |
| // `context.getFilename()` (removed in ESLint 10) at rule-load time, which | |
| // makes a v10 bump explode on every file. Re-evaluate once that plugin | |
| // ships an ESLint-10-compatible release. | |
| import { fileURLToPath } from "node:url"; | |
| import path from "node:path"; | |
| import { FlatCompat } from "@eslint/eslintrc"; | |
| import js from "@eslint/js"; | |
| import tsParser from "@typescript-eslint/parser"; | |
| import tsPlugin from "@typescript-eslint/eslint-plugin"; | |
| import reactPlugin from "eslint-plugin-react"; | |
| import reactHooksPlugin from "eslint-plugin-react-hooks"; | |
| import jsxA11yPlugin from "eslint-plugin-jsx-a11y"; | |
| import importXPlugin from "eslint-plugin-import-x"; | |
| import prettierPlugin from "eslint-plugin-prettier"; | |
| import prettierConfig from "eslint-config-prettier"; | |
| import unusedImportsPlugin from "eslint-plugin-unused-imports"; | |
| import tanstackQueryPlugin from "@tanstack/eslint-plugin-query"; | |
| import globals from "globals"; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| const compat = new FlatCompat({ | |
| baseDirectory: __dirname, | |
| resolvePluginsRelativeTo: __dirname, | |
| }); | |
| /** | |
| * ESLint rule: forbid raw `fetch` calls that target an agent-server `/api/...` | |
| * path, so API access goes through the typed @openhands/typescript-client | |
| * clients. Only statically-resolvable agent-server paths are flagged; external | |
| * URLs, dynamic identifiers, and non-`/api/` draws (cookie-auth static | |
| * fileserver, OAuth device verification) are left alone. | |
| */ | |
| function createNoDirectAgentServerFetchRule() { | |
| return { | |
| meta: { | |
| type: "problem", | |
| docs: { | |
| description: | |
| "Use typed @openhands/typescript-client clients instead of global fetch for agent-server API calls.", | |
| }, | |
| messages: { | |
| noRawFetch: | |
| "Use a typed @openhands/typescript-client client (or AgentServerClient.request) instead of global fetch for agent-server API calls.", | |
| }, | |
| }, | |
| create(context) { | |
| function resolveStaticUrl(node) { | |
| if (node.type === "Literal" && typeof node.value === "string") { | |
| return node.value; | |
| } | |
| if (node.type === "TemplateLiteral") { | |
| return node.quasis | |
| .map((q) => q.value.raw) | |
| .join(""); | |
| } | |
| return null; | |
| } | |
| return { | |
| CallExpression(node) { | |
| const callee = node.callee; | |
| if (callee.type !== "Identifier" || callee.name !== "fetch") return; | |
| const urlNode = node.arguments[0]; | |
| if (!urlNode) return; | |
| const url = resolveStaticUrl(urlNode); | |
| if (url === null || !url.includes("/api/")) return; | |
| context.report({ node, messageId: "noRawFetch" }); | |
| }, | |
| }; | |
| }, | |
| }; | |
| } | |
| export default [ | |
| // Files / dirs we never want to lint. | |
| { | |
| ignores: [ | |
| "node_modules/**", | |
| "dist/**", | |
| "build/**", | |
| "coverage/**", | |
| ".react-router/**", | |
| "playwright-report/**", | |
| "playwright-report-live/**", | |
| "test-results/**", | |
| "test-results-live/**", | |
| "public/mockServiceWorker.js", | |
| // Self-contained browser ES module served to extensions at runtime; | |
| // not part of the TypeScript project. | |
| "src/fixtures/canvas-extensions/**/*.js", | |
| "src/i18n/declaration.d.ts", | |
| ], | |
| }, | |
| // Base JS recommended rules. | |
| js.configs.recommended, | |
| // `eslint-plugin-import-x` ships flat configs directly. The rules are | |
| // registered under both `import-x/*` and `import/*` aliases so existing | |
| // `// eslint-disable-next-line import/*` comments still match. | |
| importXPlugin.flatConfigs.recommended, | |
| importXPlugin.flatConfigs.typescript, | |
| // `eslint-plugin-i18next` is still eslintrc-only — pull it in via compat. | |
| ...compat.extends("plugin:i18next/recommended"), | |
| // Project-wide settings + React/TS/etc. rules for source files. | |
| { | |
| files: ["**/*.{ts,tsx,js,jsx,mjs,cjs}"], | |
| // Surface (but don't fail on) `// eslint-disable-next-line ...` directives | |
| // that target rules this config no longer enables. The codebase has ~50 | |
| // such directives left over from the airbnb era; "warn" lets us clean | |
| // them up incrementally instead of either failing CI on day one or | |
| // letting new stale directives accumulate silently. | |
| linterOptions: { | |
| reportUnusedDisableDirectives: "warn", | |
| }, | |
| languageOptions: { | |
| ecmaVersion: "latest", | |
| sourceType: "module", | |
| parser: tsParser, | |
| parserOptions: { | |
| project: "./tsconfig.json", | |
| ecmaFeatures: { jsx: true }, | |
| }, | |
| globals: { | |
| ...globals.browser, | |
| ...globals.node, | |
| ...globals.es2024, | |
| }, | |
| }, | |
| plugins: { | |
| "@typescript-eslint": tsPlugin, | |
| react: reactPlugin, | |
| "react-hooks": reactHooksPlugin, | |
| "jsx-a11y": jsxA11yPlugin, | |
| "@tanstack/query": tanstackQueryPlugin, | |
| "unused-imports": unusedImportsPlugin, | |
| // Alias `import-x` rules under `import/*` so existing | |
| // `// eslint-disable-next-line import/foo` comments keep working. | |
| import: importXPlugin, | |
| prettier: prettierPlugin, | |
| // Local rule enforcing that agent-server API calls go through the typed | |
| // @openhands/typescript-client clients instead of a raw `fetch`. | |
| local: { | |
| rules: { | |
| "no-direct-agent-server-fetch": | |
| createNoDirectAgentServerFetchRule(), | |
| }, | |
| }, | |
| }, | |
| settings: { | |
| react: { version: "detect" }, | |
| // `eslint-import-resolver-typescript` resolves both TS path aliases | |
| // (via tsconfig) and regular node-style imports, so we don't also | |
| // configure the legacy `node` resolver shortcut here. | |
| "import-x/resolver": { | |
| typescript: true, | |
| }, | |
| }, | |
| rules: { | |
| // Recommended rule packs we want everywhere. | |
| ...tsPlugin.configs["eslint-recommended"].overrides[0].rules, | |
| ...tsPlugin.configs.recommended.rules, | |
| ...reactPlugin.configs.recommended.rules, | |
| ...reactPlugin.configs["jsx-runtime"].rules, | |
| ...reactHooksPlugin.configs.recommended.rules, | |
| ...jsxA11yPlugin.configs.recommended.rules, | |
| ...tanstackQueryPlugin.configs.recommended.rules, | |
| // Prettier integration. `eslint-config-prettier` turns off conflicting | |
| // stylistic rules; `eslint-plugin-prettier` re-reports prettier diffs | |
| // as lint errors so `npm run lint` fails on unformatted code (matching | |
| // the pre-flat-config behaviour). | |
| ...prettierConfig.rules, | |
| "prettier/prettier": "error", | |
| // Project conventions previously enforced via airbnb / custom rules. | |
| // Lint JSX *attributes* (not just text between tags) for hard-coded | |
| // user-facing strings. The plugin default (`mode: 'jsx-text-only'`) | |
| // never checks attribute values, which let untranslated strings like | |
| // `aria-label="Close"` / `placeholder="..."` slip past lint (cf. #1306). | |
| // | |
| // `jsx-only` checks every literal inside a JSX subtree, so we scope it: | |
| // - jsx-attributes.include: only attributes that carry translatable | |
| // text. Everything else (testId, name, color, to, href, className, | |
| // data-*, …) is ignored automatically — no brittle deny-list. | |
| // - callees/object-properties: re-list the plugin defaults (the option | |
| // merge is shallow, so providing a key replaces it) and add | |
| // `cn`/`className` so Tailwind class strings built via `cn(...)` or | |
| // `{ className: "..." }` aren't flagged. | |
| "i18next/no-literal-string": [ | |
| "error", | |
| { | |
| mode: "jsx-only", | |
| "jsx-attributes": { | |
| include: [ | |
| "placeholder", | |
| "alt", | |
| "aria-label", | |
| "title", | |
| "label", | |
| "heading", | |
| "text", | |
| ], | |
| }, | |
| callees: { | |
| exclude: [ | |
| "i18n(ext)?", | |
| "t", | |
| "require", | |
| "addEventListener", | |
| "removeEventListener", | |
| "postMessage", | |
| "getElementById", | |
| "dispatch", | |
| "commit", | |
| "includes", | |
| "indexOf", | |
| "endsWith", | |
| "startsWith", | |
| "cn", | |
| ], | |
| }, | |
| "object-properties": { | |
| exclude: ["[A-Z_-]+", "className"], | |
| }, | |
| }, | |
| ], | |
| "unused-imports/no-unused-imports": "error", | |
| "@typescript-eslint/prefer-optional-chain": "error", | |
| "no-restricted-imports": [ | |
| "error", | |
| { | |
| paths: [ | |
| { | |
| name: "@openhands/typescript-client/client/http-client", | |
| message: | |
| "Use typed @openhands/typescript-client clients instead of constructing HttpClient directly.", | |
| }, | |
| ], | |
| }, | |
| ], | |
| // All agent-server API access must go through the typed | |
| // @openhands/typescript-client clients. A raw global `fetch` against an | |
| // agent-server `/api/...` path bypasses the typed access layer, so it is | |
| // banned here (see also src/api/no-direct-agent-server-calls.test.ts). | |
| // Browser-cookie-auth and external (non-agent-server) fetches — e.g. the | |
| // workspace static fileserver, OAuth device verification, and the npm | |
| // registry version check — do not target `/api/...` and are unaffected. | |
| "local/no-direct-agent-server-fetch": "error", | |
| // Allow `interface Foo extends Bar<"foo"> {}` — the codebase uses this | |
| // discriminated-union pattern in `src/types/agent-server/**` and the | |
| // empty body is intentional. We still disallow `interface Foo {}` and | |
| // `type Foo = {}` (the default behaviour for the other two options). | |
| "@typescript-eslint/no-empty-object-type": [ | |
| "error", | |
| { allowInterfaces: "with-single-extends" }, | |
| ], | |
| // tsPlugin's `recommended` enables no-unused-vars; we already report | |
| // unused imports above, so make sure the TS rule still catches unused | |
| // locals/args (with the standard airbnb-style `_`-prefix escape hatch). | |
| // `caughtErrors: 'none'` keeps the v7 behaviour of allowing | |
| // `catch (error)` clauses where the error is intentionally unused. | |
| "no-unused-vars": "off", | |
| "@typescript-eslint/no-unused-vars": [ | |
| "error", | |
| { | |
| args: "after-used", | |
| argsIgnorePattern: "^_", | |
| varsIgnorePattern: "^_", | |
| ignoreRestSiblings: true, | |
| caughtErrors: "none", | |
| }, | |
| ], | |
| // React Compiler rules (added in eslint-plugin-react-hooks v5+) are | |
| // opt-in for projects that have actually adopted the compiler. Keep | |
| // them off so existing code isn't retroactively flagged. | |
| "react-hooks/immutability": "off", | |
| "react-hooks/preserve-manual-memoization": "off", | |
| "react-hooks/refs": "off", | |
| "react-hooks/set-state-in-effect": "off", | |
| // Match the legacy config: don't require .ts/.tsx import extensions, | |
| // but enforce them for everything else. See | |
| // https://stackoverflow.com/q/59265981 for background. | |
| "import-x/extensions": [ | |
| "error", | |
| "ignorePackages", | |
| { "": "never", ts: "never", tsx: "never", js: "never", jsx: "never" }, | |
| ], | |
| // The old config inherited a bunch of import rules from airbnb that | |
| // either don't apply or are now noise; explicitly turn off the ones | |
| // most likely to fire on this codebase. | |
| "import-x/prefer-default-export": "off", | |
| "import-x/no-extraneous-dependencies": "off", | |
| // `import-x/no-unresolved` is redundant with the TypeScript compiler: | |
| // `tsc` (run as `npm run typecheck` before `eslint`) already fails on | |
| // unresolved imports with much better error messages, and the rule | |
| // has known false positives with `paths`/exports-map resolution even | |
| // when the typescript resolver is configured. Keeping it on duplicates | |
| // tsc errors and produces noise on Vite's `?url` / `?worker` import | |
| // suffixes that ESLint can't see through. | |
| "import-x/no-unresolved": "off", | |
| // These two fire a lot of false positives on TypeScript projects that | |
| // import a namespace and then call methods off it (`import api from | |
| // './foo'; api.bar()` etc.). The TS compiler already catches anything | |
| // truly wrong here. | |
| "import-x/no-named-as-default": "off", | |
| "import-x/no-named-as-default-member": "off", | |
| }, | |
| }, | |
| // TypeScript-only overrides (re-applied airbnb-ish relaxations from the | |
| // previous `.eslintrc` overrides block). | |
| { | |
| files: ["**/*.{ts,tsx}"], | |
| ignores: ["src/hooks/query/query-keys.ts"], | |
| rules: { | |
| // Allow state mutation in reduce and Redux-style reducers. | |
| "no-param-reassign": [ | |
| "error", | |
| { | |
| props: true, | |
| ignorePropertyModificationsFor: ["acc", "state"], | |
| }, | |
| ], | |
| "no-restricted-syntax": [ | |
| "error", | |
| { | |
| selector: | |
| "Property[key.name='queryKey'] > ArrayExpression[elements.0.value='settings']", | |
| message: | |
| "Use SETTINGS_QUERY_KEYS helpers instead of raw settings query key arrays.", | |
| }, | |
| { | |
| selector: | |
| "CallExpression[callee.name='t'] > Literal:first-child[value=/^[A-Z0-9_]+\\$/]", | |
| message: "Use I18nKey instead of raw translation key strings.", | |
| }, | |
| { | |
| selector: | |
| "CallExpression[callee.property.name='t'] > Literal:first-child[value=/^[A-Z0-9_]+\\$/]", | |
| message: "Use I18nKey instead of raw translation key strings.", | |
| }, | |
| ], | |
| "react/require-default-props": "off", | |
| "no-underscore-dangle": "off", | |
| "jsx-a11y/no-static-element-interactions": "off", | |
| "jsx-a11y/click-events-have-key-events": "off", | |
| // For https://github.com/airbnb/javascript/issues/1885 | |
| "jsx-a11y/label-has-associated-control": [ | |
| 2, | |
| { | |
| required: { | |
| some: ["nesting", "id"], | |
| }, | |
| }, | |
| ], | |
| "react/prop-types": "off", | |
| "react/no-array-index-key": "off", | |
| "react-hooks/exhaustive-deps": "off", | |
| "react/react-in-jsx-scope": "off", | |
| }, | |
| }, | |
| ]; | |