feat: add ESLint configuration for vertical spacing and stylistic rules

- Introduced `.lintrc` for managing vertical spacing rules in the codebase.
- Updated `eslint.config.mjs` to integrate the new spacing rules with ESLint.
- Created documentation on code formatting and spacing guidelines in `formato-y-espaciado-del-codigo.md`.
- Implemented tests in `spacing.test.mjs` to ensure spacing rules are enforced correctly without altering syntax.
This commit is contained in:
David Arranz 2026-10-10 12:00:17 +02:00
parent dbac5dfa84
commit 2c827b8a7d
8 changed files with 943 additions and 2 deletions

124
.lintrc Normal file
View File

@ -0,0 +1,124 @@
{
"rules": {
"@stylistic/padding-line-between-statements": [
"error",
{
"blankLine": "always",
"prev": "*",
"next": [
"function",
"class",
"interface",
"type",
"enum",
"export",
{
"selector": "VariableDeclaration:has(VariableDeclarator > :matches(ArrowFunctionExpression, FunctionExpression))"
}
]
},
{
"blankLine": "always",
"prev": [
"function",
"class",
"interface",
"type",
"enum",
"export",
{
"selector": "VariableDeclaration:has(VariableDeclarator > :matches(ArrowFunctionExpression, FunctionExpression))"
}
],
"next": "*"
},
{
"blankLine": "always",
"prev": "import",
"next": "*"
},
{
"blankLine": "any",
"prev": "import",
"next": "import"
},
{
"blankLine": "always",
"prev": [
"const",
"let",
"var"
],
"next": "*"
},
{
"blankLine": "any",
"prev": [
"const",
"let",
"var"
],
"next": [
"const",
"let",
"var"
]
},
{
"blankLine": "always",
"prev": "*",
"next": "return"
},
{
"blankLine": "always",
"prev": {
"selector": "VariableDeclaration:has(VariableDeclarator > :matches(ArrowFunctionExpression, FunctionExpression))"
},
"next": "*"
},
{
"blankLine": "always",
"prev": "*",
"next": {
"selector": "VariableDeclaration:has(VariableDeclarator > :matches(ArrowFunctionExpression, FunctionExpression))"
}
},
{
"blankLine": "any",
"prev": "function-overload",
"next": [
"function-overload",
"function"
]
}
],
"@stylistic/lines-between-class-members": [
"error",
{
"enforce": [
{
"blankLine": "always",
"prev": "method",
"next": "*"
},
{
"blankLine": "always",
"prev": "*",
"next": "method"
}
]
},
{
"exceptAfterOverload": true
}
],
"@stylistic/no-multiple-empty-lines": [
"error",
{
"max": 1,
"maxBOF": 0,
"maxEOF": 0
}
]
}
}

View File

@ -13,6 +13,8 @@ No imprimir, copiar ni versionar .env locales o secretos.
## Convenciones ## Convenciones
El espaciado vertical se define en `.lintrc`, cargado por `eslint.config.mjs`. Separar funciones, clases y métodos con una línea vacía; mantener agrupadas propiedades y firmas de sobrecarga. `pnpm format` combina Biome con estas reglas. Véase [formato y espaciado](docs/formato-y-espaciado-del-codigo.md).
- Código e identificadores en inglés; comentarios, TSDoc y documentación en castellano. - Código e identificadores en inglés; comentarios, TSDoc y documentación en castellano.
- Ficheros y directorios en kebab-case. Clases, tipos e interfaces en PascalCase. - Ficheros y directorios en kebab-case. Clases, tipos e interfaces en PascalCase.
- Variables, funciones y métodos en camelCase; constantes globales en UPPER_SNAKE_CASE. - Variables, funciones y métodos en camelCase; constantes globales en UPPER_SNAKE_CASE.

View File

@ -10,6 +10,7 @@
"ignoreUnknown": true, "ignoreUnknown": true,
"includes": [ "includes": [
"**", "**",
"!!packages/shadcn-ui",
"!!**/node_modules", "!!**/node_modules",
"!!**/.next", "!!**/.next",
"!!**/dist", "!!**/dist",

View File

@ -0,0 +1,66 @@
# Formato y espaciado del código
Biome mantiene el formato general, la organización de imports y las reglas de calidad existentes. ESLint Stylistic complementa únicamente el espaciado vertical que Biome no inserta automáticamente. Las versiones están fijadas en package.json y pnpm-lock.yaml; el parser de TypeScript permite analizar TS, TSX y las sobrecargas sin añadir reglas de análisis duplicadas.
## Configuración
`.lintrc` es un JSON con las reglas de espaciado. No es un nombre de configuración que Biome o ESLint detecten por sí mismos: `eslint.config.mjs` lo carga y registra el plugin y el parser. Para ajustar la política, editar las reglas de `.lintrc`.
- Una línea vacía antes y después de funciones, clases, interfaces, tipos, enums y declaraciones exportadas.
- Separación entre funciones declaradas mediante variables, incluidas funciones flecha.
- Una línea vacía entre métodos de clase, constructor y accesores, y entre un grupo de propiedades y un método.
- Las propiedades de clase pueden seguir agrupadas sin líneas vacías individuales.
- Las firmas consecutivas de sobrecarga y su implementación se mantienen juntas.
- Separación al terminar un grupo de variables y antes de return cuando existe una instrucción previa.
- Como máximo una línea vacía consecutiva. Biome mantiene sus agrupaciones de imports.
El código de terceros de `packages/shadcn-ui`, los directorios generados, dependencias y declaraciones `.d.ts` quedan fuera del espaciado. La biblioteca vendor también queda excluida de Biome para respetar AGENTS.md.
## Comandos
```bash
pnpm format # Biome y después espaciado vertical, en todo el código propio
pnpm format:biome # Solo el formateador de Biome
pnpm format:spacing # Aplicar únicamente el espaciado vertical
pnpm lint:spacing # Comprobar espaciado sin modificar archivos
pnpm test:spacing # Validar las reglas, la sintaxis y la compatibilidad con Biome
```
También se puede actuar sobre un archivo concreto:
```bash
pnpm exec eslint modules/assets/src/api/domain/asset.aggregate.ts --fix
```
La incorporación de las reglas no reformatea automáticamente todo el código existente: `pnpm format:spacing` realiza esa adaptación cuando se ejecute. Los comandos habituales `lint` y `check` siguen siendo los de Biome; `lint:spacing` comprueba la política complementaria de forma explícita.
## Ejemplo
```ts
class Service {
private enabled = true;
private name = "Metis Desk";
constructor() {}
first() {
const name = this.name;
return name;
}
second() {
return this.enabled;
}
}
function first() {
return 1;
}
function second() {
return 2;
}
```
Fuentes de las reglas: [separación entre declaraciones](https://eslint.style/rules/padding-line-between-statements) y [separación entre miembros de clase](https://eslint.style/rules/lines-between-class-members).

32
eslint.config.mjs Normal file
View File

@ -0,0 +1,32 @@
import { readFileSync } from "node:fs";
import stylistic from "@stylistic/eslint-plugin";
import parser from "@typescript-eslint/parser";
// .lintrc contiene solo espaciado vertical; Biome conserva el resto de reglas.
const spacing = JSON.parse(readFileSync(new URL("./.lintrc", import.meta.url), "utf8"));
export default [
{
ignores: [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/coverage/**",
"**/.turbo/**",
"**/out/**",
"**/public/**",
"**/*.d.ts",
"packages/shadcn-ui/**",
],
},
{
files: ["**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}"],
languageOptions: {
parser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
plugins: { "@stylistic": stylistic },
rules: spacing.rules,
},
];

View File

@ -11,14 +11,21 @@
"typecheck": "turbo run typecheck", "typecheck": "turbo run typecheck",
"lint": "biome lint .", "lint": "biome lint .",
"check": "biome check .", "check": "biome check .",
"format": "biome format --write .", "format": "pnpm format:biome && pnpm format:spacing",
"test": "turbo run test", "test": "turbo run test",
"build": "turbo run build", "build": "turbo run build",
"db:up": "python3 scripts/prepare-local-env.py && docker compose up -d --wait" "db:up": "python3 scripts/prepare-local-env.py && docker compose up -d --wait",
"lint:spacing": "eslint .",
"format:spacing": "eslint . --fix",
"format:biome": "biome format --write .",
"test:spacing": "node --test scripts/spacing.test.mjs"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.11", "@biomejs/biome": "2.4.11",
"@stylistic/eslint-plugin": "5.10.0",
"@types/node": "25.6.0", "@types/node": "25.6.0",
"@typescript-eslint/parser": "8.71.1",
"eslint": "10.12.0",
"tsx": "4.21.0", "tsx": "4.21.0",
"turbo": "2.9.14", "turbo": "2.9.14",
"typescript": "6.0.2" "typescript": "6.0.2"

File diff suppressed because it is too large Load Diff

86
scripts/spacing.test.mjs Normal file
View File

@ -0,0 +1,86 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { test } from "node:test";
import { parse } from "@typescript-eslint/parser";
import { ESLint } from "eslint";
const eslint = new ESLint({ fix: true });
function syntax(source) {
const tree = parse(source, { sourceType: "module", ecmaFeatures: { jsx: true } });
return JSON.stringify(tree, (key, value) =>
["loc", "range", "start", "end", "tokens", "comments"].includes(key) ? undefined : value
);
}
const source = `import { helper } from "example";
export interface Example { value: number }
type Identifier = string;
function first() { return 1; }
function second() { return 2; }
const arrow = () => 3;
const other = () => 4;
class Service {
value = 1;
enabled = true;
constructor() {}
first() { return this.value; }
second() { return 2; }
}
function overloaded(value: string): string;
function overloaded(value: number): number;
function overloaded(value: string | number) { return value; }
export function View() { const label = "Nombre"; return <span>{label}</span>; }
`;
test("el espaciado separa declaraciones y métodos sin cambiar la sintaxis ni las sobrecargas", async () => {
const [result] = await eslint.lintText(source, { filePath: "apps/web/src/spacing-example.tsx" });
assert.ok(result);
assert.equal(result.errorCount, 0);
assert.ok(result.output);
assert.match(result.output, /}\n\nfunction second/);
assert.match(result.output, /const arrow = .*;\n\nconst other/);
assert.match(result.output, /enabled = true;\n\n {2}constructor/);
assert.match(result.output, /constructor\(\) {}\n\n {2}first/);
assert.match(result.output, /value = 1;\n {2}enabled/);
assert.match(result.output, /string;\nfunction overloaded/);
assert.equal(syntax(result.output), syntax(source));
const [repeated] = await eslint.lintText(result.output, {
filePath: "apps/web/src/spacing-example.tsx",
});
assert.equal(repeated?.errorCount, 0);
assert.equal(repeated?.output, undefined);
});
test("la biblioteca de terceros queda excluida", async () => {
assert.equal(await eslint.isPathIgnored("packages/shadcn-ui/src/components/button.tsx"), true);
});
test("Biome y ESLint conservan el espaciado al repetir el formateo", async () => {
const format = (text) =>
execFileSync(
"pnpm",
["exec", "biome", "format", "--stdin-file-path=apps/web/src/spacing-example.tsx"],
{ input: text, encoding: "utf8" }
);
const [spaced] = await eslint.lintText(format(source), {
filePath: "apps/web/src/spacing-example.tsx",
});
assert.ok(spaced);
assert.equal(spaced.errorCount, 0);
const output = spaced.output ?? format(source);
const biomeOutput = format(output);
const [repeated] = await eslint.lintText(biomeOutput, {
filePath: "apps/web/src/spacing-example.tsx",
});
assert.equal(repeated?.errorCount, 0);
assert.equal(repeated?.output, undefined);
assert.equal(syntax(output), syntax(biomeOutput));
});