import { fileURLToPath } from "node:url"; import { includeIgnoreFile } from "@eslint/compat"; import { defineConfig } from "eslint/config"; import tsPlugin from "@typescript-eslint/eslint-plugin"; import tsParser from "@typescript-eslint/parser"; import reactHooks from "eslint-plugin-react-hooks"; import core from "ultracite/eslint/core"; import next from "ultracite/eslint/next"; import noInternalMocks from "./scripts/eslint-rules/no-internal-mocks.mjs"; import { VERCEL_SANDBOX_SDK_IMPORTERS } from "./scripts/eslint-rules/vercel-sandbox-importers.mjs"; // Glob patterns read `[id]` as a character class; match route folders literally. // Only `[` and `]` are escaped: allowlisted paths are source files, which never // contain `*`, `?`, `{`, `}` or a leading `!`. Extend this if one ever does. const escapeGlob = (path) => path.replaceAll(/[[\]]/g, String.raw`\$&`); const sanitizedCore = core .map((config) => { if (!config || typeof config !== "object") return config; const nextConfig = { ...config }; if ("files" in nextConfig && Array.isArray(nextConfig.files)) { nextConfig.files = nextConfig.files.filter( (pattern) => pattern !== "**/*.json" && pattern !== "**/*.html" ); } if ("rules" in nextConfig && nextConfig.rules) { nextConfig.rules = { ...nextConfig.rules, "import-x/enforce-node-protocol-usage": "off", "import-x/no-unused-modules": "off", // Unbounded cycle detection walks the whole import graph per file // (~60% of total lint time). Shallow cycles are the real bugs. "import-x/no-cycle": ["error", { maxDepth: 3, ignoreExternal: true }], }; } return nextConfig; }) .filter( (config) => !("files" in config) || !Array.isArray(config.files) || config.files.length > 0 ); export default defineConfig([ // Everything git ignores, eslint must ignore too. Local build artifacts // (.vercel/output alone holds ~500 minified bundles) are absent on CI, so // linting them turned an 86-second lint into 75 minutes locally. includeIgnoreFile(fileURLToPath(new URL(".gitignore", import.meta.url))), ...sanitizedCore, ...next, { files: ["**/*.{ts,tsx}"], languageOptions: { parser: tsParser, parserOptions: { ecmaVersion: "latest", sourceType: "module", ecmaFeatures: { jsx: true }, }, }, plugins: { "@typescript-eslint": tsPlugin, "react-hooks": reactHooks, }, rules: { ...reactHooks.configs.recommended.rules, // Source files cap at 500 lines — split by concern instead of growing a // file. Pre-existing violators are grandfathered to "warn" in a block // near the end of this config; that list only shrinks. "max-lines": ["error", { max: 500 }], // Keep initial migration focused on formatting, imports, and low-risk fixes. "@typescript-eslint/array-type": "off", "@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/consistent-type-imports": "off", "@typescript-eslint/dot-notation": "off", "@typescript-eslint/explicit-member-accessibility": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/init-declarations": "off", "@typescript-eslint/max-params": "off", "@typescript-eslint/naming-convention": "off", "@typescript-eslint/no-base-to-string": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-object-type": "off", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-require-imports": "off", "@typescript-eslint/no-shadow": "off", "@typescript-eslint/no-unnecessary-condition": "off", "@typescript-eslint/no-unnecessary-type-assertion": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-unsafe-type-assertion": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/promise-function-async": "off", "@typescript-eslint/prefer-destructuring": "off", "@typescript-eslint/prefer-nullish-coalescing": "off", "@typescript-eslint/prefer-regexp-exec": "off", "@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/require-await": "off", "@typescript-eslint/return-await": "off", "@typescript-eslint/no-unused-vars": [ "warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, ], "no-await-in-loop": "off", "no-continue": "off", "no-console": "off", "no-eq-null": "off", "no-void": "off", "no-nested-ternary": "off", eqeqeq: "off", "default-case": "off", "github/no-then": "off", "no-unused-vars": "off", "n/no-sync": "off", "no-magic-numbers": "off", "no-undef": "off", "compat/compat": "off", "func-style": "off", "id-length": "off", "import-x/consistent-type-specifier-style": "off", "import-x/dynamic-import-chunkname": "off", "import-x/no-nodejs-modules": "off", "import-x/order": "off", "prefer-destructuring": "off", "prefer-named-capture-group": "off", "prefer-template": "off", "promise/avoid-new": "off", "promise/prefer-await-to-callbacks": "off", "promise/prefer-await-to-then": "off", "require-atomic-updates": "off", "require-unicode-regexp": "off", "react-hooks/immutability": "warn", "react-hooks/invariant": "warn", "react-hooks/preserve-manual-memoization": "warn", "react-hooks/purity": "warn", "react-hooks/set-state-in-effect": "warn", "sonarjs/arrow-function-convention": "off", complexity: ["warn", { max: 10 }], "sonarjs/cognitive-complexity": "off", "sonarjs/cyclomatic-complexity": ["warn", { threshold: 10 }], "sonarjs/declarations-in-global-scope": "off", "sonarjs/expression-complexity": "off", "sonarjs/file-header": "off", "sonarjs/function-name": "off", "sonarjs/max-union-size": "off", "sonarjs/nested-control-flow": "off", "sonarjs/no-alphabetical-sort": "off", "sonarjs/no-duplicate-string": "off", "sonarjs/no-misleading-array-reverse": "off", "sonarjs/no-nested-conditional": "off", "sonarjs/no-reference-error": "off", "sonarjs/no-undefined-assignment": "off", "sonarjs/prefer-regexp-exec": "off", "sonarjs/shorthand-property-grouping": "off", "sonarjs/slow-regex": "off", "sonarjs/too-many-break-or-continue-in-loop": "off", "unused-imports/no-unused-vars": "off", "unicorn/better-regex": "off", "unicorn/catch-error-name": "off", "unicorn/consistent-destructuring": "off", "unicorn/no-array-sort": "off", "unicorn/no-nested-ternary": "off", "unicorn/no-useless-switch-case": "off", "unicorn/no-useless-undefined": "off", "unicorn/numeric-separators-style": "off", "unicorn/prefer-at": "off", "unicorn/prefer-abbreviations": "off", "unicorn/prevent-abbreviations": "off", "unicorn/prefer-global-this": "off", "unicorn/prefer-number-properties": "off", "unicorn/prefer-response-static-json": "off", "unicorn/prefer-spread": "off", "unicorn/prefer-string-raw": "off", "unicorn/prefer-string-replace-all": "off", "unicorn/switch-case-braces": "off", "unicorn/text-encoding-identifier-case": "off", }, }, { files: ["**/*.{js,jsx,ts,tsx,mjs,cjs}"], plugins: { "@typescript-eslint": tsPlugin, }, rules: { "@typescript-eslint/await-thenable": "off", "@typescript-eslint/consistent-return": "off", "@typescript-eslint/no-dynamic-delete": "off", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-redundant-type-constituents": "off", "@typescript-eslint/no-unnecessary-type-conversion": "off", "@typescript-eslint/parameter-properties": "off", "@typescript-eslint/strict-void-return": "off", "@typescript-eslint/switch-exhaustiveness-check": "off", "@typescript-eslint/unbound-method": "off", "@typescript-eslint/use-unknown-in-catch-callback-variable": "off", "consistent-return": "off", "compat/compat": "off", "github/async-preventdefault": "off", "logical-assignment-operators": "off", "max-depth": "off", "new-cap": "off", "no-inline-comments": "off", "no-magic-numbers": "off", "no-plusplus": "off", "no-promise-executor-return": "off", "no-redeclare": "off", "no-underscore-dangle": "off", "no-warning-comments": "off", "promise/catch-or-return": "off", "promise/param-names": "off", radix: "off", "sonarjs/arrow-function-convention": "off", "sonarjs/declarations-in-global-scope": "off", "sonarjs/file-header": "off", "sonarjs/max-lines": "off", "sonarjs/max-lines-per-function": "off", "sonarjs/no-identical-functions": "off", "sonarjs/no-inconsistent-returns": "off", "sonarjs/no-nested-functions": "off", "sonarjs/no-unused-vars": "off", "sonarjs/no-wildcard-import": "off", "sonarjs/publicly-writable-directories": "off", "unicorn/consistent-function-scoping": "off", "unicorn/import-style": "off", "unicorn/no-array-reduce": "off", "unicorn/no-process-exit": "off", "unicorn/no-thenable": "off", "unicorn/prefer-code-point": "off", "unicorn/prefer-structured-clone": "off", }, }, { files: ["**/*.{js,jsx,ts,tsx,mjs,cjs}"], plugins: { "@typescript-eslint": tsPlugin, }, rules: { "@next/next/no-img-element": "off", "@typescript-eslint/member-ordering": "off", "@typescript-eslint/no-deprecated": "off", "@typescript-eslint/no-invalid-void-type": "off", "@typescript-eslint/no-misused-spread": "off", "@typescript-eslint/no-unnecessary-type-parameters": "off", "@typescript-eslint/prefer-promise-reject-errors": "off", "@typescript-eslint/restrict-plus-operands": "off", "array-callback-return": "off", "func-style": "off", "github/no-then": "off", "import-x/no-deprecated": "off", "import-x/no-nodejs-modules": "off", "import-x/no-rename-default": "off", "n/no-process-exit": "off", "n/no-sync": "off", "no-bitwise": "off", "no-div-regex": "off", "no-await-in-loop": "off", "no-use-before-define": "off", "no-useless-return": "off", "no-void": "off", "prefer-const": "off", "prefer-named-capture-group": "off", "promise/always-return": "off", "promise/avoid-new": "off", "promise/no-multiple-resolved": "off", "promise/prefer-await-to-then": "off", "require-await": "off", "require-unicode-regexp": "off", "sonarjs/declarations-in-global-scope": "off", "sonarjs/deprecation": "off", complexity: ["warn", { max: 10 }], "sonarjs/cognitive-complexity": "off", "sonarjs/cyclomatic-complexity": "off", "sonarjs/file-header": "off", "sonarjs/file-name-differ-from-class": "off", "sonarjs/nested-control-flow": "off", "sonarjs/no-all-duplicated-branches": "off", "sonarjs/no-duplicated-branches": "off", "sonarjs/no-incorrect-string-concat": "off", "sonarjs/no-nested-assignment": "off", "sonarjs/no-nested-template-literals": "off", "sonarjs/no-os-command-from-path": "off", "sonarjs/no-unused-function-argument": "off", "sonarjs/no-nested-incdec": "off", "sonarjs/redundant-type-aliases": "off", "sonarjs/shorthand-property-grouping": "off", "sonarjs/single-char-in-character-classes": "off", "sonarjs/todo-tag": "off", "sonarjs/variable-name": "off", "sonarjs/void-use": "off", "unicorn/custom-error-definition": "off", "unicorn/no-array-reverse": "off", "unicorn/no-await-expression-member": "off", "unicorn/no-document-cookie": "off", "unicorn/no-for-loop": "off", "unicorn/prefer-default-parameters": "off", "unicorn/prefer-logical-operator-over-ternary": "off", "unicorn/prevent-abbreviations": "off", "unicorn/text-encoding-identifier-case": "off", "vars-on-top": "off", }, }, { // Sentry requires these exact filenames; exempt them from filename regex. files: ["sentry.server.config.ts", "sentry.edge.config.ts"], rules: { "github/filenames-match-regex": "off", }, }, { // components/ai-elements is vendored from the AI SDK Elements registry // (npx ai-elements). Registry updates regenerate these files wholesale, so // hand-splitting them to satisfy source-file rules would fight upstream. files: ["components/ai-elements/**/*.{jsx,tsx}"], rules: { "max-lines": "off", "react-hooks/refs": "off", "react-hooks/static-components": "off", }, }, { files: [ "app/**/flows/**/*.{jsx,tsx}", "app/**/workflows/**/*.{jsx,tsx}", "components/control/**/*.{jsx,tsx}", "components/flows/**/*.{jsx,tsx}", "components/panes/{flow,workflow}*.{jsx,tsx}", "components/panes/{flow,workflow}*/**/*.{jsx,tsx}", ], rules: { "no-restricted-syntax": [ "error", { selector: "JSXOpeningElement[name.name=/^(select|datalist)$/]", message: "Project and workflow selection controls must use app-styled components instead of browser-native select or datalist elements.", }, ], }, }, { // Sandbox backends are pluggable (lib/sandbox/provider). Only the Vercel // adapter may use the Vercel SDK; the frozen list of older importers // shrinks as Track B migrates them. files: ["**/*.{ts,tsx,mjs}"], ignores: [ "lib/sandbox/provider/vercel/**", "**/*.test.{ts,tsx}", "tests/**", ...VERCEL_SANDBOX_SDK_IMPORTERS.map(escapeGlob), ], rules: { "no-restricted-imports": [ "error", { paths: [ { name: "@vercel/sandbox", message: "Import sandbox types from @/lib/sandbox/provider/types and get a backend from @/lib/sandbox/provider. Only lib/sandbox/provider/vercel may use the Vercel SDK.", }, ], patterns: [ { group: ["@vercel/sandbox/*"], message: "Only lib/sandbox/provider/vercel may use the Vercel SDK.", }, ], }, ], }, }, { // TESTING.md: mock only at declared boundaries. The allowlist lives in // tests/support/mockable-boundaries.mjs and is owner-gated in CODEOWNERS. files: ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"], plugins: { testing: { rules: { "no-internal-mocks": noInternalMocks } }, }, rules: { "testing/no-internal-mocks": "error", }, }, { ignores: [ ".agents/", ".claude/", ".next/", "node_modules/", "playwright-report/", "supabase/functions/", "test-results/", "next-env.d.ts", "tsconfig.json", "vercel.json", "**/*.md", // pnpm requires this exact filename and CJS format. ".pnpmfile.cjs", // CJS require hook preloaded via `tsx --require` in test:unit; must be // CommonJS to patch require.extensions / Module._load. "tests/support/bundler-shims.cjs", ], }, ]);