Dialecto CSS
@pbuilder/sdk/css asigna y elimina declaraciones en archivos .css. Está pensado para archivos
que mezclan valores generados con CSS escrito a mano, como un archivo de tokens de diseño
sincronizado desde una exportación JSON (por ejemplo, variables de Figma). Cada op edita solo la
declaración que indica, y el resto de los bytes del archivo queda como estaba. El handle funciona
como el de cualquier otro dialecto: las ops encadenadas sobre él se combinan en una sola
escritura, y await sigue las reglas de
Cómo funciona un handle de dialecto.
Sincronizar tokens en un archivo de Tailwind 4
Sección titulada «Sincronizar tokens en un archivo de Tailwind 4»@import "tailwindcss";@custom-variant dark (&:where(.dark, .dark *));
@theme { --color-primary: #0b254d; --radius: 4px;}import * as css from "@pbuilder/sdk/css";
export default async () => { await css .find("tokens.css") .setDeclaration({ atRule: "theme" }, "--color-primary", "#1974f9") .setDeclaration({ selector: "html[data-theme='nylon']" }, "--color-primary", "#0b254d");};@import "tailwindcss";@custom-variant dark (&:where(.dark, .dark *));
@theme { --color-primary: #1974f9; --radius: 4px;}
html[data-theme='nylon'] { --color-primary: #0b254d;}El valor de @theme se actualiza en su lugar. El bloque del modo nylon no existía, así que la
op lo creó al final del archivo. El dialecto recibe strings ya resueltos: convertir el JSON de
tokens en nombres de propiedad y valores (alias, modos) es tarea de tu schematic.
find(path) acepta solo archivos .css; .scss no está soportado. Cualquier otra extensión
lanza un error de forma síncrona. Los fragmentos del resto de esta página omiten el envoltorio de
la factory.
Targets
Sección titulada «Targets»Un target indica un bloque de nivel superior:
| Target | Bloque |
|---|---|
{ selector: ":root" } |
la regla de nivel superior :root { } |
{ atRule: "theme" } |
el cuerpo de @theme { } |
{ atRule: "theme", params: "inline" } |
el cuerpo de @theme inline { } |
{ atRule: "media", params: "(prefers-color-scheme: dark)", selector: ":root" } |
la regla :root dentro de ese @media |
- El selector y los params coinciden tal como están escritos, después de recortar los espacios.
html[data-theme='nylon']yhtml[data-theme="nylon"]son targets distintos, y asignar un valor en el segundo crea un bloque nuevo junto al primero. - El nombre de la at-rule se escribe sin
@y coincide sin distinguir mayúsculas. - Omitir
paramscoincide solo con una at-rule sin params.{ atRule: "theme" }no coincide con@theme inline { }: en un archivo que solo tiene@theme inline, crea un bloque@theme { }nuevo al final del archivo. Usa{ atRule: "theme", params: "inline" }. - Una sentencia at-rule sin cuerpo, como
@layer base;o@import, nunca es un target. - Un target que coincide con más de un bloque se rechaza.
Ubicación
Sección titulada «Ubicación»- Un bloque que falta se crea al final del archivo. Siempre queda después de
@import, que debe ir antes que cualquier otra regla, y después de cualquier encabezado escrito a mano. Una regla dentro de una at-rule, como:rootdentro de@media, se crea al final de esa at-rule. - Un bloque nuevo usa el fin de línea del archivo (
\no\r\n) y la indentación de su primera declaración de nivel superior. Si no hay ninguna declaración que copiar, indenta con dos espacios. - Una declaración nueva va al final de su bloque, o junto a una existente con
{ before }o{ after }. - Una declaración existente se actualiza en su lugar y el ancla se ignora, así que una segunda ejecución nunca reordena el bloque.
css.find("tokens.css").setDeclaration({ atRule: "theme" }, "--color-accent", "#ff0", { after: "--color-primary" });// -> @theme {// -> --color-primary: #0b254d;// -> --color-accent: #ff0;// -> --radius: 4px;// -> }Un target con at-rule y selector crea los dos cuando faltan:
css.find("tokens.css").setDeclaration( { atRule: "media", params: "(prefers-color-scheme: dark)", selector: ":root" }, "--color-primary", "#9ec5ff",);// se agrega al final del archivo:// -> @media (prefers-color-scheme: dark) {// -> :root {// -> --color-primary: #9ec5ff;// -> }// -> }Nombres de propiedad y valores
Sección titulada «Nombres de propiedad y valores»- Los nombres de propiedad normales se comparan sin distinguir mayúsculas, así que
COLORactualizacolor. Las propiedades personalizadas sí distinguen mayúsculas, como en CSS:--Brandy--brandson dos declaraciones. - Un valor es CSS crudo, como
#1974f9,var(--blue-500)ocalc(1rem + 2px), y debe ser exactamente un valor de declaración.;y}se permiten dentro de strings, como encontent: ";". !importantva en la opciónimportant, nunca en el valor.{ important: true }lo escribe, y omitir la opción lo quita de una declaración existente.- El dialecto no razona sobre shorthands. Asignar
margin-topen un bloque que tienemargin: 8pxagregamargin-topy dejamargincomo está.
setDeclaration(target, property, value, options?)
Sección titulada «setDeclaration(target, property, value, options?)»Asigna value a property en el bloque del target. Una declaración existente se actualiza en su
lugar. Una nueva va al final del bloque, o junto a options.before u options.after. Un bloque
que falta se crea al final del archivo. options.important: true escribe !important. Asignar
el valor que la declaración ya tiene no escribe nada.
// :root antes: color-scheme: light !important;css.find("tokens.css").setDeclaration({ selector: ":root" }, "color-scheme", "light");// -> :root {// -> color-scheme: light;// -> }removeDeclaration(target, property)
Sección titulada «removeDeclaration(target, property)»Elimina property del bloque del target. Un bloque o una declaración que falta no hace nada, así
que una sincronización puede eliminar tokens que quizá ya no estén. El bloque se mantiene, aunque
quede vacío.
css .find("tokens.css") .removeDeclaration({ atRule: "theme" }, "--radius") .removeDeclaration({ selector: ".missing" }, "--x");// -> @theme {// -> --color-primary: #0b254d;// -> }Rechazos
Sección titulada «Rechazos»Cada rechazo lanza un error antes de que el archivo cambie. El mensaje empieza con
dialect operation failed: e indica la op y el target:
dialect operation failed: setDeclaration(@theme) — `red; color: blue` is not a single declaration value.Una op rechazada deja el archivo sin cambios, y lo mismo pasa con las ops encadenadas antes que ella en el mismo handle. Las ops rechazan:
- un nombre de propiedad que no es un identificador CSS ni una propiedad personalizada
--; - un valor que cerraría la declaración o agregaría otra (
red; color: blue,red}), un valor con!important, un valor vacío y un valor con un comentario; - un target que no es
{ selector },{ atRule, params? }ni ambos, un nombre de at-rule escrito con@, y un selector o unos params que agregarían otro bloque; - un target que coincide con más de un bloque;
- una propiedad declarada más de una vez en el bloque, como el fallback
display: -webkit-box; display: flex;(edítalo con.modify()); - un ancla que no está declarada en el bloque, y pasar
beforeyaftera la vez.
Ediciones personalizadas con .modify()
Sección titulada «Ediciones personalizadas con .modify()».modify(fn) le pasa a fn el Root vivo de PostCSS. El módulo exporta el paquete postcss
como astLibrary, así que no necesitas agregar postcss a tus propias dependencias. PostCSS
conserva cada byte de espacios, comentarios y BOM, así que un archivo que ninguna op tocó se
imprime idéntico byte a byte.
await css.find("app.css").modify((root) => { root.walkRules(".legacy", (rule) => rule.remove());});Leer un valor con getDeclaration
Sección titulada «Leer un valor con getDeclaration»Sobre el handle, getDeclaration(target, property) es una query.
Termina la cadena y resuelve a { value, important }, o a undefined cuando falta el bloque o
la declaración. Ve todas las ops encadenadas antes que ella, y esas ops se confirman igual:
const primary = await css .find("tokens.css") .setDeclaration({ atRule: "theme" }, "--color-primary", "#1974f9") .getDeclaration({ atRule: "theme" }, "--color-primary");// primary: { value: "#1974f9", important: false }El módulo también exporta getDeclaration(root, target, property) como función normal. Recibe
un Root ya parseado y devuelve { value, important }, o undefined cuando falta el bloque o la
declaración. Sigue las mismas reglas de target, nombre, ambigüedad y duplicados que las ops, y
lanza los mismos errores. Llámala dentro de .modify(fn) para decidir qué cambiar. Esta factory
actualiza --color-primary en @theme solo cuando todavía tiene el valor viejo:
import * as css from "@pbuilder/sdk/css";
export default async () => { await css.find("tokens.css").modify((root) => { if (css.getDeclaration(root, { atRule: "theme" }, "--color-primary")?.value === "#0b254d") { // A bare root.walkDecls would also rewrite the html[data-theme='nylon'] block. root.walkAtRules("theme", (theme) => theme.walkDecls("--color-primary", (decl) => { decl.value = "#1974f9"; }), ); } });};@import "tailwindcss";@custom-variant dark (&:where(.dark, .dark *));
@theme { --color-primary: #1974f9; --radius: 4px;}
html[data-theme='nylon'] { --color-primary: #0b254d;}getDeclaration también funciona sobre un Root que parseas tú con astLibrary.parse:
const root = css.astLibrary.parse((await css.find("tokens.css").read()) ?? "");css.getDeclaration(root, { selector: ":root" }, "--radius"); // { value: "4px", important: false }Las ediciones hechas en .modify() saltan las validaciones de las ops. Las
reglas para las ediciones personalizadas
también aplican aquí.
Para ir más lejos
Sección titulada «Para ir más lejos»- Librerías AST: qué librería y qué versión respaldan cada dialecto.
- La guía del dialecto CSS del SDK (en inglés).