← Blog

Renderizar React en console.log

Text.js dibuja una app de React en la consola de DevTools con una llamada a console.log con estilos por fotograma.

Adam

Text.js renderiza apps de React en la consola de DevTools de Chrome. Salvo un campo de entrada oculto, la página no tiene interfaz de usuario. Cada fotograma de la app es una sola llamada a console.log.

La consola como pantalla

console.log acepta una cadena de formato. Cada %c aplica el CSS del siguiente argumento al texto que le sigue, hasta el siguiente %c:

console.log("%c▼ Text.js%c ready", "color:#ededed;font-weight:700", "color:#45a557");

Estos estilos permiten definir colores, fondos, negrita, cursiva y subrayado, y la fuente de la consola es monoespaciada (Menlo en macOS). Con una fuente monoespaciada y colores que pueden cambiar en cada secuencia de caracteres, la consola funciona como una cuadrícula de celdas de caracteres con estilo. En ella se pueden dibujar bordes, paneles, campos de entrada y gráficos.

Un fotograma es una cuadrícula de celdas

El renderizador guarda cada fotograma en un CellBuffer. Los caracteres se almacenan en un array tipado, los colores de primer plano y de fondo en otros dos, y los atributos como la negrita y el subrayado en un cuarto. Los arrays se asignan una sola vez para un tamaño determinado y se reutilizan en cada fotograma.

Para registrar un fotograma, el controlador recorre cada fila y agrupa en secuencias las celdas vecinas que comparten un estilo. Cada secuencia se convierte en un span %c, y las filas se unen con \n. Nuestro panel de prueba de 100×32, con 3200 celdas, se codifica en unos 200 spans.

Una llamada por fotograma

Registrar cada fila como un mensaje independiente no funciona. Cada mensaje tiene su propio borde y enlace al código fuente en DevTools, y DevTools agrupa los mensajes consecutivos idénticos tras un contador de repeticiones, por lo que dos filas idénticas de una tabla se fusionarían en una. Un fotograma registrado como un solo mensaje se mantiene en una sola pieza.

Primero hay que escapar dos elementos del texto. Un % literal se escribe como %%, para que un texto como 100% no pueda consumir un argumento de estilo. DevTools también convierte las URL en enlaces y las repinta con sus propios colores, por lo que se añade un word joiner (U+2060), que no tiene anchura, a :// y www.:

export function escapeConsoleText(text: string, allowLink = false): string {  let s = text;  if (s.includes("%")) s = s.replaceAll("%", "%%");  if (!allowLink) {    if (s.includes(LINK_SCHEME)) s = s.replaceAll(LINK_SCHEME, LINK_SCHEME_SAFE);    if (s.includes(WWW) || s.includes("WWW.")) s = s.replace(/www\./gi, (m) => `${m.slice(0, 3)}\u2060.`);  }  return s;}

Los fotogramas sin cambios no registran nada

Se calcula un hash de cada fila del búfer. Si ningún hash ha cambiado desde el último fotograma, el controlador retorna antes de construir una cadena y no toca la consola. Una app que espera una entrada no hace ninguna llamada.

Cuando una fila cambia, el controlador borra la consola y registra el nuevo fotograma:

const { frame, dirtyRows } = this.encode(buffer, options);if (frame === null) return SKIPPED;if (this.clearEachFrame) target.clear();target.log(frame.format, ...frame.args);

Solo se vuelven a codificar las filas cuyo hash ha cambiado. Las demás reutilizan los spans del fotograma anterior.

Entrada

La consola no recibe pulsaciones de teclas, así que la página las recoge. Un <textarea> oculto recibe las pulsaciones de teclas, el contenido pegado y la composición IME, y los convierte en eventos de teclado para la app. Al hacer clic en la página, este campo recibe el foco.

Próximos pasos

Los prototipos 0.0.x de @textjs/core en npm incluyen todo en un solo paquete. Text.js 0.1 traslada el renderizador descrito aquí a @textjs/core, el reconciliador de React a @textjs/react y añade un App Router.