Saltar al contenido

enforce-canonical ​

Regla de oxlint que reescribe las clases de Tailwind CSS a la forma canónica que calcula el propio Tailwind, como -m-0 → m-0, con autofix.

De un vistazo ​

AutofixSugerencias en el editorDesign systemOpciones
SíSíObligatorio (entryPoint)entryPoint, reportNonEquivalent

Qué hace esta regla ​

Le pregunta al design system cuál es la forma canónica de cada utility en tu código y reescribe las que no lo son. "Canónico" es lo que devuelve canonicalizeCandidates() de @tailwindcss/node — la misma fuente de verdad que usan prettier-plugin-tailwindcss, oxfmt y las herramientas oficiales de Tailwind. Ejemplos: -m-0 → m-0 (no necesitas el negativo para un cero), start-2 → inset-s-2, flex-grow-[2] → grow-2 (un valor arbitrario cuya forma nombrada emite el mismo CSS), text-[var(--color-text)]/90 → text-(--color-text)/90. El auto-fix corrige el primer hit, las sugerencias cubren el resto en el mismo string. Los renombres de v3 (bg-gradient-to-r → bg-linear-to-r, break-words → wrap-break-word) son propiedad de no-deprecated-classes, no de esta regla — ver Interacciones.

Las clases con nombre resuelven contra un canonicalMap precomputado en memoria (sub-microsegundo). Los valores arbitrarios (p-[2px], bg-(--c)) y las variantes arbitrarias (data-[open]:, min-[40rem]:) pasan por el worker canonicalize-service porque necesitan una consulta viva al DS; los resultados se cachean a nivel proceso por (entryPoint, rem, class). El worker preserva la posición del ! (prefix vs suffix vs ninguno).

DS-dependiente — requiere settings.tailwindcss.entryPoint. Si el design system no puede cargar, la regla emite un único diagnóstico fatal designSystemUnavailable por archivo en vez de pasar en silencio.

Opciones ​

reportNonEquivalent ​

boolean, default false.

Algunas formas canónicas no son el mismo CSS en tu proyecto, y la regla deja esas clases como están. Con esta opción activa, además te avisa de dos tipos de ellas, nunca con autofix:

  • Una variante que tu CSS define distinto del Tailwind de stock. shadcn/ui, por ejemplo, trae data-disabled: como una variante propia construida sobre :where([data-disabled="true"]), …, así que data-[disabled]:opacity-50 — que el Tailwind de stock reescribe a data-disabled:opacity-50 — selecciona otros elementos aquí. El reporte nombra las dos variantes, y cambiar es decisión tuya.
  • Una forma canónica que además fija una variable que leen otras utilidades. Tailwind reescribe [line-height:var(--x)] a leading-(--x), pero leading-(--x) también fija --tw-leading, que cada tamaño text-* lee para su interlineado, así que junto a uno las dos pueden verse distinto. Lo mismo pasa con [border-style:dashed] → border-dashed (--tw-border-style, que lee border-2), [transition-duration:300ms] → duration-300 (--tw-duration, que lee transition) y las formas font-*, tracking-*, ease-*, outline-* y content-none. El reporte nombra la variable y trae una sugerencia que tu editor puede aplicar.

Una diferencia que también tiene el Tailwind de stock (has-[[data-slot=x]]: vs has-data-[slot=x]: emiten selectores distintos en cualquier proyecto) no se reporta, y tampoco un valor que lee el theme (p-[2px] vs p-0.5, mira prefer-scale-token). En apps/v4 de shadcn/ui la opción agrega 23 reportes, todos data-[disabled]:, y ninguno del segundo tipo.

jsonc
{ "tailwindcss/enforce-canonical": ["warn", { "reportNonEquivalent": true }] }

entryPoint ​

string, opcional. Override por regla de settings.tailwindcss.entryPoint. Cuando puedas, configura el entry point en settings.tailwindcss.entryPoint para todo el proyecto.

Ejemplos ​

✗ Incorrecto ​

tsx
// El negativo-de-cero es simplemente cero
<div className="-m-0 -mt-0" />
// → <div className="m-0 mt-0" />

// Shorthand de inset lógico → canónico inset-s-* / inset-e-*
<div className="start-2 end-4" />
// → <div className="inset-s-2 inset-e-4" />

// Un valor arbitrario cuya forma nombrada emite el mismo CSS
<div className="flex-grow-[2]" />
// → <div className="grow-2" />

// Variants e important se preservan
<div className="hover:!flex-grow-[2]" />
// → <div className="hover:!grow-2" />

// Modificadores de opacidad y variantes arbitrarias, con o sin otras variantes
<div className="dark:bg-white/[.08] data-[open]:flex min-[40rem]:flex" />
// → <div className="dark:bg-white/8 data-open:flex sm:flex" />

// options: { "reportNonEquivalent": true }
// No es el mismo CSS: leading-(--x) también fija --tw-leading, que leen los tamaños text-*.
// Se reporta con una sugerencia, nunca se arregla solo
<div className="[line-height:var(--x)]" />
// → <div className="leading-(--x)" />

✓ Correcto ​

tsx
<div className="m-0 mt-0" />

<div className="inset-s-2 inset-e-4" />

<div className="grow-2" />

<div className="hover:!grow-2" />

<div className="dark:bg-white/8 data-open:flex sm:flex" />

<div className="leading-(--x)" />

Interacciones con otras reglas ​

  • no-unnecessary-arbitrary-value: complementaria, no hay doble-fire. Las dos actúan solo cuando el valor arbitrario y su reemplazo nombrado emiten CSS idéntico; se reparten por forma. no-unnecessary-arbitrary-value es dueña de los casos donde el arbitrario mapea directo a una única utility con nombre (e.g. h-[auto] → h-auto). Un valor que solo es numéricamente igual a un paso de la escala (p-[2px] → p-0.5, cuyo texto CSS difiere) no es asunto de ninguna de las dos — eso es prefer-scale-token, solo-reporte.
  • prefer-theme-tokens: el tercer partner del trío arbitrario→nombrado. Atrapa referencias a variables CSS como border-(--border) → border-border donde ni enforce-canonical (el CSS difiere) ni no-unnecessary-arbitrary-value (no hay equivalente bracket compartido) se dispararían.
  • no-deprecated-classes: es la dueña de los renombres de v3, y esta regla los omite. Tailwind también los canonicaliza, así que bg-gradient-to-r → bg-linear-to-r antes se reportaba dos veces con el mismo fix; el mensaje de la otra regla ("deprecada en v4") es el más accionable de los dos. Aquí queda todo lo que es actual-pero-no-canónico: -m-0 → m-0, start-2 → inset-s-2, y las formas con valor arbitrario como flex-grow-[2] → grow-2 (la lista de renombres tiene grafías, no valores). Mantén ambas activas — con solo esta los renombres quedan sin reportar.
  • prefer-scale-token: la mitad solo-reporte de lo que esta regla cedió en #78. Una reescritura cuyo CSS emitido difiere textualmente (p-[10px] → p-2.5, donde el token llega por var(--spacing)) no se autofixea aquí y no lo hará nunca; esa regla lo reporta con una sugerencia. Las dos no pueden disparar juntas: esta solo reescribe pares byte-idénticos, que es exactamente lo que la otra salta.
  • enforce-consistent-important-position: esta regla preserva la posición del ! que escribiste (prefix vs suffix vs ninguno). enforce-consistent-important-position es la única fuente de verdad para imponer una posición en particular.

Cuándo desactivarla ​

  • No quieres ninguna reescritura de clases — por ejemplo, en un archivo legacy que estás manteniendo literal para diffear.
  • Lint runs sensibles a la performance donde el costo de inicializar el worker molesta. El cache hace que las corridas siguientes sean baratas, pero el primer hit paga el startup de @tailwindcss/node. La mayoría de proyectos no lo van a notar.
  • No migraste a Tailwind v4 todavía: las formas canónicas producidas aquí asumen semántica v4.

Released under the MIT License.