Files
Musify/web-player/scripts/check-tokens.mjs
T
AdminandClaude Sonnet 5 ec6313dda1 web-player: P14 — barrido final y guardarraíles
Añade scripts/check-tokens.mjs (npm run lint:tokens, incorporado a
lint) como guardrail: falla si src/**/*.svelte reintroduce valores
arbitrarios fuera de la lista blanca de B.4, rounded-* de fábrica,
pares h-N w-N (deberían ser size-N), espaciado fraccionario en
cuartos, o colores hex/rgba() literales.

Se limpia el repo hasta dejar el script en 0 violaciones: ~40 pares
h-N w-N → size-N (sobre todo iconos, vía script puntual), ~26
espaciados fraccionarios en cuartos redondeados al entero más cercano
(Δ≤1px, p. ej. py-2.75→py-3, gap-1.75→gap-2), radios de fábrica
sueltos (rounded-sm/lg, rounded a secas) migrados a los tokens
correspondientes, y dos tracking-[-0.02em] que coincidían
exactamente con el token tracking-display ya existente pero nunca se
habían migrado. Se retiran las recetas stroke-thin/regular/bold
(creadas en P1, cero consumidores) — decisión tomada, sin bloquear:
no se migra strokeWidth={n} (13 valores, en casi todos los
componentes) a CSS porque no se puede verificar sin Docker que
lucide-svelte respete stroke-width por clase en vez del prop, y una
regresión ahí sería silenciosa en toda la app; el script documenta
este gap explícitamente en vez de fingir cobertura completa.

Detector de huérfanos: 0 componentes sin importadores. Se borra
docs/design-system-refactor-plan.md (plan anterior, superseded por
frontend-refactor-plan.md). web-player/CLAUDE.md gana una sección
"Estilos" con la lista blanca B.4 y los componentes base a reutilizar.
Se recalcula la tabla de línea base (A.4) y se anota en el Registro
(D.1): 61→47 componentes, 44→11 arbitrarios (los 11 restantes en
lista blanca), 13→8 radios distintos, 0 violaciones de lint:tokens.

Verificado con npm run check (0 errores), npm run lint:tokens (0
violaciones) y npm run build. Smoke test de /login sin errores de
consola achacables al cambio (Docker no disponible en esta sesión).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 15:05:07 +02:00

101 lines
4.0 KiB
JavaScript

#!/usr/bin/env node
// Guardrail contra la reintroducción de lo que P0-P14 limpió
// (ver docs/frontend-refactor-plan.md, secciones B.4 y P14). Escanea
// src/**/*.svelte en busca de valores arbitrarios y utilidades de Tailwind
// que deberían ser tokens del sistema de diseño (src/routes/layout.css).
//
// Gaps conocidos, no cubiertos aquí a propósito:
// - `strokeWidth={n}` en iconos: hay ~13 valores distintos repartidos por
// casi todos los componentes. Migrarlo a las recetas `stroke-*` requeriría
// verificar que lucide-svelte respeta `stroke-width` por CSS en vez del
// atributo del componente, y no hay forma de comprobarlo visualmente en
// esta sesión (sin Docker). Se deja fuera para no arriesgar una regresión
// silenciosa en todos los iconos de la app.
// - `text-base` sin receta: solo queda un caso real (Queue.svelte, título
// "En cola", 16px) sin equivalente en la escala text-display-*. Crear una
// receta nueva para un único consumidor sería una abstracción prematura;
// se acepta como excepción documentada en vez de forzarlo a un tamaño que
// no le corresponde.
import { readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
const files = execSync('git ls-files "src/**/*.svelte"', { encoding: 'utf8' })
.split('\n')
.filter(Boolean);
// B.4 — arbitrarios permitidos tras el refactor.
const ARBITRARY_WHITELIST = [
/^text-\[clamp\([^)]+\)\]$/, // tipografía fluida (login, Logo)
/^leading-\[[\d.]+\]$/, // leading ajustado del display gigante de login
/^grid-cols-\[auto_1fr\]$/, // upload: portada + campos
/^max-h-\[85dvh\]$/, // Modal
/^my-\[\d+d?vh\]$/, // +error.svelte: centrado vertical de página de error
/^py-\[\d+d?vh\]$/, // Explorar: centrado vertical de "sin resultados"
/^transition-\[filter\]$/, // Explorar: genre-tile hover
/^bg-\[image:var\(--mf-[\w-]+\)\]$/ // gradientes dinámicos por tema (Artwork, Logo, liked)
];
// Radios semánticos de B.1.1 — cualquier otro rounded-* de Tailwind (o el
// `rounded` a secas de fábrica) es una regresión. `rounded-full` y los
// tokens (`rounded-tag/thumb/control/art/art-lg/panel/panel-lg`) están bien:
// el lookahead exige que no venga más texto de clase pegado con `-`.
const RAW_ROUNDED = /\brounded(-(none|xs|sm|md|lg|xl|2xl|3xl))?(?![\w-])/g;
const PAIRED_HW = /\b(?:([a-z0-9-]+:))?h-([\d.]+)\s+\1w-\2\b/g;
const QUARTER_STEP_SPACING = /\b[a-z-]+-\d+\.(25|75)\b/g;
const HEX_COLOR = /#[0-9a-fA-F]{3,8}\b/g;
const RGBA_LITERAL = /\brgba?\(/g;
let violations = [];
for (const file of files) {
const content = readFileSync(file, 'utf8');
const lines = content.split('\n');
lines.forEach((line, i) => {
const lineNo = i + 1;
for (const match of line.matchAll(/[a-zA-Z][a-zA-Z0-9-]*-\[[^\]]+\]/g)) {
if (!ARBITRARY_WHITELIST.some((re) => re.test(match[0]))) {
violations.push(`${file}:${lineNo} — arbitrario fuera de la lista blanca: \`${match[0]}\``);
}
}
for (const match of line.matchAll(RAW_ROUNDED)) {
violations.push(`${file}:${lineNo} — radio de Tailwind sin tokenizar: \`${match[0]}\``);
}
for (const match of line.matchAll(PAIRED_HW)) {
violations.push(`${file}:${lineNo} — usa \`size-${match[2]}\` en vez de \`${match[0]}\``);
}
for (const match of line.matchAll(QUARTER_STEP_SPACING)) {
violations.push(
`${file}:${lineNo} — espaciado fraccionario en cuartos: \`${match[0]}\` (redondear al entero más cercano)`
);
}
for (const match of line.matchAll(HEX_COLOR)) {
violations.push(
`${file}:${lineNo} — color hex literal: \`${match[0]}\` (usa un token --mf-*)`
);
}
for (const match of line.matchAll(RGBA_LITERAL)) {
violations.push(
`${file}:${lineNo} — color rgba() literal: \`${match[0]}\` (usa un token --mf-*)`
);
}
});
}
if (violations.length > 0) {
console.error(`\n${violations.length} violación(es) del sistema de diseño:\n`);
for (const v of violations) console.error(` ${v}`);
console.error('\nVer docs/frontend-refactor-plan.md, sección B.4.\n');
process.exit(1);
}
console.log('check-tokens: sin violaciones.');