eslint.config.mjs
Rama main · réplica de lectura del repositorio original.
import js from '@eslint/js';
import typescript from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactCompiler from 'eslint-plugin-react-compiler';
import integrationBoundaries from './eslint-rules/integration-boundaries.mjs';
import moduleSdkBoundary from './eslint-rules/module-sdk-boundary.mjs';
import { MAX_LINES_TODO } from './eslint-rules/max-lines-todo.mjs';
import {
ELSE_RETURN,
NESTED_TERNARY,
NON_NULL_ASSERTION,
OBJECT_LITERAL_ASSERTION,
PREFER_CONST,
TYPE_IMPORTS,
} from './eslint-rules/cleanliness-todo.mjs';
import {
NULLISH_COALESCING,
OPTIONAL_CHAIN,
SWITCH_EXHAUSTIVE,
UNNECESSARY_ASSERTION,
UNNECESSARY_CONDITION,
} from './eslint-rules/typed-lint-todo.mjs';
export default [
js.configs.recommended,
// Node build config (esbuild)
{
files: ['api/esbuild.config.js'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'script',
globals: {
require: 'readonly',
module: 'readonly',
process: 'readonly',
__dirname: 'readonly',
},
},
},
// Backend TypeScript files (api/src/**/*.ts)
{
files: ['api/src/**/*.ts'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './api/tsconfig.json',
},
globals: {
console: 'readonly',
process: 'readonly',
Buffer: 'readonly',
URL: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
module: 'readonly',
require: 'readonly',
exports: 'readonly',
global: 'readonly',
globalThis: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
setImmediate: 'readonly',
clearImmediate: 'readonly',
Promise: 'readonly',
NodeJS: 'readonly',
},
},
plugins: {
'@typescript-eslint': typescript,
},
rules: {
...typescript.configs.recommended.rules,
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': 'off',
// Use `log` from utils/logger. Stray console.log/debug dumps reached
// production logs unfiltered; console.warn/error remain in catch blocks.
'no-console': ['error', { allow: ['warn', 'error'] }],
},
},
// Scripts TypeScript files (scripts/**/*.ts)
{
files: ['scripts/**/*.ts'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
// No project requirement - scripts don't need strict type checking
},
globals: {
console: 'readonly',
process: 'readonly',
Buffer: 'readonly',
URL: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
module: 'readonly',
require: 'readonly',
exports: 'readonly',
global: 'readonly',
globalThis: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
setImmediate: 'readonly',
clearImmediate: 'readonly',
Promise: 'readonly',
NodeJS: 'readonly',
// Some scripts evaluate browser-side code snippets (e.g. Playwright helpers)
window: 'readonly',
},
},
plugins: {
'@typescript-eslint': typescript,
},
rules: {
...typescript.configs.recommended.rules,
// Scripts are allowed to have helpers and any-typed data structures
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'no-console': 'off',
},
},
// Repo tooling scripts (scripts/**/*.mjs, tests/*.mjs) — plain Node ESM.
{
files: ['scripts/**/*.mjs', 'tests/*.mjs'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
console: 'readonly',
process: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
URL: 'readonly',
},
},
rules: {
'no-console': 'off',
},
},
// Frontend tooling scripts (client/scripts/**/*.mjs)
// These run in Node, but may use modern Web APIs available in Node (fetch, AbortController, etc).
{
files: ['client/scripts/**/*.mjs'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
console: 'readonly',
process: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
fetch: 'readonly',
AbortController: 'readonly',
structuredClone: 'readonly',
Promise: 'readonly',
},
},
rules: {
'no-console': 'off',
},
},
// Tests TypeScript files (tests/**/*.ts)
{
files: ['tests/**/*.ts'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
// No project requirement - tests don't need strict type checking
},
globals: {
console: 'readonly',
process: 'readonly',
Buffer: 'readonly',
URL: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
module: 'readonly',
require: 'readonly',
exports: 'readonly',
global: 'readonly',
globalThis: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
setImmediate: 'readonly',
clearImmediate: 'readonly',
Promise: 'readonly',
NodeJS: 'readonly',
// Playwright / test globals
test: 'readonly',
expect: 'readonly',
describe: 'readonly',
it: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
page: 'readonly',
browser: 'readonly',
context: 'readonly',
// Browser-like globals used in tests & helpers
window: 'readonly',
document: 'readonly',
navigator: 'readonly',
localStorage: 'readonly',
sessionStorage: 'readonly',
URLSearchParams: 'readonly',
CustomEvent: 'readonly',
},
},
plugins: {
'@typescript-eslint': typescript,
},
rules: {
...typescript.configs.recommended.rules,
// Tests are allowed to have helpers and any-typed fixtures
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'no-console': 'off',
},
},
// Frontend React files (client/**/*.tsx, client/**/*.ts)
{
files: ['client/**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
globals: {
console: 'readonly',
document: 'readonly',
window: 'readonly',
navigator: 'readonly',
localStorage: 'readonly',
sessionStorage: 'readonly',
fetch: 'readonly',
FormData: 'readonly',
HTMLElement: 'readonly',
HTMLDivElement: 'readonly',
HTMLAudioElement: 'readonly',
HTMLInputElement: 'readonly',
HTMLSelectElement: 'readonly',
HTMLTextAreaElement: 'readonly',
HTMLImageElement: 'readonly',
SVGSVGElement: 'readonly',
DOMParser: 'readonly',
Element: 'readonly',
Audio: 'readonly',
URLSearchParams: 'readonly',
URL: 'readonly',
CustomEvent: 'readonly',
MouseEvent: 'readonly',
Event: 'readonly',
DOMException: 'readonly',
DocumentFragment: 'readonly',
File: 'readonly',
FileReader: 'readonly',
alert: 'readonly',
requestAnimationFrame: 'readonly',
cancelAnimationFrame: 'readonly',
ResizeObserver: 'readonly',
globalThis: 'readonly',
RequestInit: 'readonly',
Response: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
Promise: 'readonly',
__APP_VERSION__: 'readonly', // Defined by Vite
},
},
plugins: {
'@typescript-eslint': typescript,
react,
'react-hooks': reactHooks,
'react-compiler': reactCompiler,
},
rules: {
...typescript.configs.recommended.rules,
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'react/react-in-jsx-scope': 'off', // Not needed in React 17+
'react/prop-types': 'off', // Using TypeScript
'react/no-unescaped-entities': 'off', // Allow quotes in text content
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-expressions': [
'error',
{ allowShortCircuit: true, allowTernary: true },
],
'no-console': 'off',
// Disable React Compiler warnings – we still keep core React/TS rules
'react-compiler/react-compiler': 'off',
},
settings: {
react: {
version: 'detect',
},
},
},
// Game integration boundaries (3.0 module split). Core code reaches a game
// only through integrations/registry and integrations/types; an integration
// never imports another one or the registry; api/src/core never imports
// rconService or the MatchZy Enhanced event types by any path. An error everywhere,
// with no allowlist. See eslint-rules/integration-boundaries.mjs.
{
files: ['api/src/**/*.ts', 'client/src/**/*.{ts,tsx}'],
plugins: {
local: integrationBoundaries,
},
rules: {
'local/integration-boundaries': 'error',
},
},
// Module SDK boundary (item 8c). A client game module reaches platform code
// only through client/src/module-sdk and integrations/types, and uses the
// host's copy of the shared packages. An error, so no new bypass creeps in.
// See eslint-rules/module-sdk-boundary.mjs.
{
files: ['client/src/integrations/**/*.{ts,tsx}'],
plugins: {
'module-sdk': moduleSdkBoundary,
},
rules: {
'module-sdk/module-sdk-boundary': 'error',
},
},
// File length. A file over 450 lines of code (blank lines and comments not
// counted) is too long to keep in your head: split it. Files from before the
// rule are listed; a PR that splits one takes it off the list.
{
files: ['api/src/**/*.ts', 'client/src/**/*.{ts,tsx}'],
ignores: MAX_LINES_TODO,
rules: {
'max-lines': ['error', { max: 450, skipBlankLines: true, skipComments: true }],
},
},
// Typed internals. Values our own code makes or stores carry their real
// type, so mistakes show up here instead of as runtime checks. Data from
// outside (a request body, a fetch, a server message, a feed) comes in as
// `Untrusted` (utils/untrusted) and is checked once where it arrives.
// A caught error keeps `unknown` (anything can be thrown): in `catch`, in
// `.catch()` callbacks and in a parameter named error/err/e/reason/cause.
{
files: ['api/src/**/*.ts', 'client/src/**/*.{ts,tsx}'],
rules: {
'no-restricted-syntax': [
'error',
{
selector:
'TSUnknownKeyword:not(CatchClause TSUnknownKeyword):not(CallExpression[callee.property.name="catch"] TSUnknownKeyword):not(Identifier[name=/^(e|err|error|reason|cause)$/] > TSTypeAnnotation > TSUnknownKeyword)',
message:
'Give it its real type. Outside data is `Untrusted` (utils/untrusted), checked once where it arrives.',
},
{
selector:
'TSAsExpression[expression.type="TSAsExpression"][expression.typeAnnotation.type="TSUnknownKeyword"]',
message: '`as unknown as` switches type checking off. Fix the types instead.',
},
],
},
},
// Cleanliness. One rule per object so each can skip its own list of files
// from before the rule (eslint-rules/cleanliness-todo.mjs); new files follow
// all of them. max-depth is a warning until we agree a limit.
...[
['no-nested-ternary', 'error', NESTED_TERNARY],
['no-else-return', 'error', ELSE_RETURN],
['prefer-const', 'error', PREFER_CONST],
['@typescript-eslint/no-non-null-assertion', 'error', NON_NULL_ASSERTION],
[
'@typescript-eslint/consistent-type-assertions',
['error', { assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }],
OBJECT_LITERAL_ASSERTION,
],
['@typescript-eslint/consistent-type-imports', 'error', TYPE_IMPORTS],
].map(([rule, setting, ignores]) => ({
files: ['api/src/**/*.ts', 'client/src/**/*.{ts,tsx}'],
ignores,
rules: { [rule]: setting },
})),
{
files: ['api/src/**/*.ts', 'client/src/**/*.{ts,tsx}'],
rules: { eqeqeq: ['error', 'smart'], 'max-depth': ['warn', 4] },
},
// Type-aware (api/src only: the client has no typed linting yet). No
// floating promises (a fire-and-forget call says so with `void`), no promise
// where a void callback is expected (Express handlers excepted), and five
// more that skip their own list of files from before the rule
// (eslint-rules/typed-lint-todo.mjs).
{
files: ['api/src/**/*.ts'],
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
{ checksVoidReturn: { arguments: false } },
],
},
},
...[
['@typescript-eslint/switch-exhaustiveness-check', SWITCH_EXHAUSTIVE],
['@typescript-eslint/no-unnecessary-type-assertion', UNNECESSARY_ASSERTION],
['@typescript-eslint/no-unnecessary-condition', UNNECESSARY_CONDITION],
['@typescript-eslint/prefer-nullish-coalescing', NULLISH_COALESCING],
['@typescript-eslint/prefer-optional-chain', OPTIONAL_CHAIN],
].map(([rule, ignores]) => ({ files: ['api/src/**/*.ts'], ignores, rules: { [rule]: 'error' } })),
// Ignore patterns
{
ignores: [
'node_modules/**',
'dist/**',
'public/**',
'api/dist/**',
'api/public/**',
'client/dist/**',
'playwright-report/**',
'logs/**',
'site/**', // Generated documentation site
'data/**',
'.venv/**', // (legacy) Python virtual environment at repo root
'docs/.venv/**', // Python virtual environment for docs
'*.js',
'*.mjs',
'vite.config.ts',
'client/vite.config.ts',
'eslint.config.mjs',
'client/src/brackets-viewer/**', // Vendored code - ignore linting errors
// Standalone example projects: their own package.json, lockfile and
// tsconfig, installed separately and typechecked on their own in CI.
// Linting them from here would apply this project's type-aware rules to
// a tsconfig they are not part of.
'examples/**',
// The MCP server: its own package, typechecked on its own in CI.
'mcp/**',
'.pnp.cjs', // Yarn PnP file
],
},
];