eslint.config.mjs
Rama main · réplica de lectura del repositorio original.
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",
],
},
]);