Modernización de la infraestructura de CSS en Herramientas para desarrolladores

Actualización de la arquitectura de DevTools: Modernización de la infraestructura de CSS en DevTools

Esta publicación forma parte de una serie de publicaciones de blog en las que se describen los cambios que estamos realizando en la arquitectura de DevTools y cómo se compila. Explicaremos históricamente cómo funcionaba CSS en Herramientas para desarrolladores y cómo modernizamos nuestro CSS en Herramientas para desarrolladores a fin de prepararnos para (finalmente) la migración a una solución web estándar para cargar CSS en archivos JavaScript.

Estado anterior del CSS en DevTools

DevTools implementó CSS de dos maneras diferentes: una para los archivos CSS que se usan en la parte heredada de DevTools y otra para los componentes web modernos que se usan en DevTools.

La implementación de CSS en DevTools se definió hace muchos años y ahora está desactualizada. DevTools se ha mantenido con el uso del patrón module.json y se ha realizado un gran esfuerzo para quitar estos archivos. El último bloqueador para quitar estos archivos es la sección resources, que se usa para cargar archivos CSS.

Queríamos dedicar tiempo a explorar diferentes soluciones potenciales que, en última instancia, podrían transformarse en secuencias de comandos de módulos de CSS. El objetivo era quitar la deuda técnica causada por el sistema heredado, pero también facilitar el proceso de migración a las secuencias de comandos de módulos CSS.

Los archivos CSS que se encontraban en DevTools se consideraban "heredados", ya que se cargaban con un archivo module.json, que se está quitando. Todos los archivos CSS debían aparecer en resources en un archivo module.json en el mismo directorio que el archivo CSS.

Ejemplo de un archivo module.json restante:

{
  "resources": [
    "serviceWorkersView.css",
    "serviceWorkerUpdateCycleView.css"
  ]
}

Luego, estos archivos CSS propagarían un mapa de objetos global llamado Root.Runtime.cachedResources como una asignación de una ruta de acceso a su contenido. Para agregar estilos a Herramientas para desarrolladores, debes llamar a registerRequiredCSS con la ruta de acceso exacta al archivo que quieres cargar.

Ejemplo de llamada registerRequiredCSS:

constructor() {
  
  this.registerRequiredCSS('ui/legacy/components/quick_open/filteredListWidget.css');
  
}

Esto recuperaría el contenido del archivo CSS y lo insertaría como un elemento <style> en la página mediante la función appendStyle:

Función appendStyle que agrega CSS con un elemento de diseño intercalado:

const content = Root.Runtime.cachedResources.get(cssFile) || '';

if (!content) {
  console.error(cssFile + ' not preloaded. Check module.json');
}

const styleElement = document.createElement('style');
styleElement.textContent = content;
node.appendChild(styleElement);

Cuando presentamos los componentes web modernos (con elementos personalizados), decidimos, en un principio, usar CSS a través de etiquetas <style> intercaladas en los archivos de componentes. Esto planteó sus propios desafíos:

  • Falta de compatibilidad con el resaltado de sintaxis. Los complementos que proporcionan resaltado de sintaxis para CSS intercalado no suelen ser tan buenos como las funciones de resaltado de sintaxis y autocompletado de CSS escritos en archivos .css.
  • Genera una sobrecarga de rendimiento. La CSS integrada también implicaba que debía haber dos pases para el análisis con lint: uno para los archivos CSS y otro para CSS intercalado. Esta era una sobrecarga de rendimiento que podíamos quitar si todo el CSS se escribía en archivos CSS independientes.
  • Desafío en la reducción. El CSS intercalado no se pudo reducir fácilmente, por lo que no se redujo ninguno. El tamaño del archivo de la compilación de lanzamiento de DevTools también aumentó debido al CSS duplicado que introdujeron varias instancias del mismo componente web.

El objetivo de mi proyecto de pasantía era encontrar una solución para la infraestructura de CSS que funcione con la infraestructura heredada y los nuevos componentes web que se usan en DevTools.

Investigar posibles soluciones

El problema se podría dividir en dos partes diferentes:

  • Descubrir cómo el sistema de compilación controla los archivos CSS
  • Descubrir cómo DevTools importa y utiliza los archivos CSS

A continuación, analizamos diferentes posibles soluciones para cada parte.

Cómo importar archivos CSS

El objetivo de importar y usar CSS en los archivos de TypeScript era cumplir con los estándares web lo más posible, aplicar la coherencia en todas las Herramientas para desarrolladores y evitar CSS duplicados en nuestro código HTML. También queríamos poder elegir una solución que permitiera migrar nuestros cambios a nuevos estándares de plataformas web, como las secuencias de comandos de módulos CSS.

Por estos motivos, las instrucciones @import y las etiquetas no parecían ser la opción adecuada para DevTools. No serían uniformes con las importaciones en el resto de DevTools y generarían un destello de contenido sin diseño (FOUC). La migración a las secuencias de comandos del módulo CSS sería más difícil porque las importaciones deberían agregarse de manera explícita y abordarlas de manera diferente a como lo harían con etiquetas <link>.

const output = LitHtml.html`
<style> @import "css/styles.css"; </style>
<button> Hello world </button>`
const output = LitHtml.html`
<link rel="stylesheet" href="styles.css">
<button> Hello World </button>`

Soluciones potenciales con @import o <link>

En su lugar, decidimos buscar una forma de importar el archivo CSS como un objeto CSSStyleSheet para poder agregarlo al Shadow Dom (las Herramientas para desarrolladores usan Shadow DOM durante algunos años) con su propiedad adoptedStyleSheets.

Opciones de Bundler

Necesitábamos una forma de convertir archivos CSS en un objeto CSSStyleSheet para poder manipularlo fácilmente en el archivo TypeScript. Consideramos Rollup y webpack como posibles empaquetadores para realizar esta transformación por nosotros. DevTools ya usa Rollup en su compilación de producción, pero agregar cualquiera de los empaquetadores a la compilación de producción podría tener posibles problemas de rendimiento cuando se trabaja con nuestro sistema de compilación actual. Nuestra integración con el sistema de compilación GN de Chromium dificulta el empaquetado y, por lo tanto, los empaquetadores suelen no integrarse bien con el sistema de compilación actual de Chromium.

En su lugar, exploramos la opción de usar el sistema de compilación GN actual para que realice esta transformación por nosotros.

La nueva infraestructura del uso de CSS en Herramientas para desarrolladores

La nueva solución implica usar adoptedStyleSheets para agregar estilos a un Shadow DOM en particular mientras se usa el sistema de compilación GN para generar objetos CSSStyleSheet que pueden ser adoptados por un document o un ShadowRoot.

// CustomButton.ts

// Import the CSS style sheet contents from a JS file generated from CSS
import customButtonStyles from './customButton.css.js';
import otherStyles from './otherStyles.css.js';

export class CustomButton extends HTMLElement{
  
  connectedCallback(): void {
    // Add the styles to the shadow root scope
    this.shadow.adoptedStyleSheets = [customButtonStyles, otherStyles];
  }
}

El uso de adoptedStyleSheets tiene varios beneficios, como los siguientes:

  • Está en proceso de convertirse en un estándar web moderno.
  • Evita el CSS duplicado
  • Aplica estilos solo a un Shadow DOM, lo que evita cualquier problema causado por nombres de clase duplicados o selectores de ID en archivos CSS.
  • Fácil de migrar a futuros estándares web, como las secuencias de comandos de módulos CSS y las aserciones de importación

La única advertencia para la solución era que las sentencias import requerían que se importara el archivo .css.js. Para permitir que GN genere un archivo CSS durante la compilación, escribimos la secuencia de comandos generate_css_js_files.js. El sistema de compilación ahora procesa cada archivo CSS y lo transforma en un archivo JavaScript que, de forma predeterminada, exporta un objeto CSSStyleSheet. Esto es excelente, ya que podemos importar el archivo CSS y adoptarlo fácilmente. Además, ahora también podemos reducir la compilación de producción fácilmente, lo que ahorra el tamaño del archivo:

const styles = new CSSStyleSheet();
styles.replaceSync(
  // In production, we also minify our CSS styles
  /`${isDebug ? output : cleanCSS.minify(output).styles}
  /*# sourceURL=${fileName} */`/
);

export default styles;

Ejemplo de iconButton.css.js generado a partir de la secuencia de comandos.

Cómo migrar código heredado con reglas de ESLint

Si bien los componentes web se podían migrar de forma manual y fácil, el proceso para migrar los usos heredados de registerRequiredCSS fue más complejo. Las dos funciones principales que registraron estilos heredados fueron registerRequiredCSS y createShadowRootWithCoreStyles. Decidimos que, dado que los pasos para migrar estas llamadas eran bastante mecánicos, podíamos usar reglas de ESLint para aplicar correcciones y migrar automáticamente el código heredado. Herramientas para desarrolladores ya usa una serie de reglas personalizadas específicas para la base de código de Herramientas para desarrolladores. Esto fue útil porque ESLint ya analiza el código en un árbol de sintaxis abstracta (abreviatura de AST) y podríamos consultar los nodos de llamada específicos que eran llamadas para registrar CSS.

El mayor problema que enfrentamos cuando escribimos las reglas de ESLint de migración fue capturar casos extremos. Queríamos asegurarnos de encontrar el equilibrio adecuado entre saber qué casos extremos valía la pena capturar y cuáles debían migrarse de forma manual. También queríamos asegurarnos de poder informar a un usuario cuando el sistema de compilación no generaba automáticamente un archivo .css.js importado, ya que esto evitaba errores en el tiempo de ejecución de archivos no encontrados.

Una desventaja de usar reglas de ESLint para la migración era que no se podía cambiar el archivo de compilación de GN requerido en el sistema. El usuario debía realizar estos cambios de forma manual en cada directorio. Si bien esto requirió más trabajo, fue una buena manera de confirmar que el sistema de compilación genera cada archivo .css.js que se importa.

En general, usar las reglas de ESLint para esta migración fue muy útil, ya que pudimos migrar rápidamente el código heredado a la nueva infraestructura y tener el AST disponible de inmediato significó que también pudimos controlar varios casos extremos en la regla y corregirlos automáticamente de forma confiable con la API de corrector de ESLint.

¿Qué debo hacer?

Hasta el momento, todos los componentes web de Chromium DevTools se migraron para usar la nueva infraestructura de CSS en lugar de usar estilos intercalados. La mayoría de los usos heredados de registerRequiredCSS también se migraron para usar el nuevo sistema. Solo queda quitar tantos archivos module.json como sea posible y, luego, migrar esta infraestructura actual para implementar secuencias de comandos de módulos de CSS en el futuro.

Descarga los canales de vista previa

Considera usar Chrome Canary, Dev o Beta como tu navegador de desarrollo predeterminado. Estos canales de versión preliminar te brindan acceso a las funciones más recientes de DevTools, te permiten probar las APIs de plataformas web de vanguardia y te ayudan a encontrar problemas en tu sitio antes que tus usuarios.

Comunícate con el equipo de Herramientas para desarrolladores de Chrome

Usa las siguientes opciones para hablar sobre las funciones nuevas, las actualizaciones o cualquier otro tema relacionado con DevTools.