← Blog

Afficher React dans console.log

Text.js dessine une application React dans la console DevTools avec un appel console.log stylé par frame.

Adam

Text.js affiche des applications React dans la console DevTools de Chrome. À part un champ de saisie masqué, la page n’a pas d’interface. Chaque frame de l’application correspond à un seul appel à console.log.

La console comme écran

console.log accepte une chaîne de format. Chaque %c applique le CSS de l’argument suivant au texte qui le suit, jusqu’au prochain %c :

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

Ces styles peuvent définir les couleurs, les arrière-plans, le gras, l’italique et le soulignement, et la police de la console est à chasse fixe (Menlo sur macOS). Avec une police à chasse fixe et des couleurs qui peuvent changer à chaque séquence de caractères, la console fonctionne comme une grille de cellules de caractères stylisées. On peut y dessiner des bordures, des panneaux, des champs de saisie et des graphiques.

Une frame comme grille de cellules

Le moteur de rendu conserve chaque frame dans un CellBuffer. Les caractères occupent un tableau typé, les couleurs de premier plan et d’arrière-plan deux autres, et les attributs comme le gras et le soulignement un quatrième. Les tableaux sont alloués une fois pour une taille donnée et réutilisés à chaque frame.

Pour écrire une frame dans la console, le pilote parcourt chaque ligne et regroupe en séquences les cellules voisines qui partagent un style. Chaque séquence devient un span %c, et les lignes sont jointes avec \n. Notre tableau de bord de test de 100×32, soit 3200 cellules, est encodé en environ 200 spans.

Un appel par frame

Écrire chaque ligne dans un message distinct ne fonctionne pas. Chaque message reçoit sa propre bordure et son propre lien vers la source dans DevTools, et DevTools regroupe les messages consécutifs identiques derrière un compteur de répétitions, si bien que deux lignes identiques d’un tableau fusionneraient en une seule. Une frame écrite dans un seul message reste d’un seul tenant.

Deux éléments du texte doivent d’abord être échappés. Un % littéral s’écrit %%, pour qu’un texte comme 100% ne puisse pas consommer un argument de style. DevTools transforme aussi les URL en liens et les redessine avec ses propres couleurs, donc :// et www. reçoivent un word joiner (U+2060), dont la largeur est nulle :

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;}

Aucune écriture pour les frames inactives

Chaque ligne du tampon est hachée. Si aucun hachage n’a changé depuis la dernière frame, le pilote retourne avant de construire une chaîne et ne touche jamais à la console. Une application qui attend une saisie ne fait aucun appel.

Lorsqu’une ligne a changé, le pilote efface la console et y écrit la nouvelle frame :

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

Seules les lignes dont le hachage a changé sont réencodées. Les autres réutilisent les spans de la frame précédente.

Saisie

La console ne reçoit pas les frappes au clavier, c’est donc la page qui les recueille. Un <textarea> masqué reçoit les frappes, les collages et la composition IME, puis les transforme en événements clavier pour l’application. Un clic sur la page lui donne le focus.

La suite

Les prototypes @textjs/core 0.0.x sur npm regroupent tout dans un seul paquet. Text.js 0.1 déplace le moteur de rendu décrit ici dans @textjs/core, le réconciliateur React dans @textjs/react, et ajoute un App Router.