Zurück zum Blog
engineering||7 Min. Lesezeit

Ein PR an einem Parser entsperrte Prerendering in Brisa

AR
Aral Roca

Ersteller von Kitmul

Brisa Framework; das JavaScript-Framework, dessen Build-Pipeline jede Quelldatei über Meriyah parst
Brisa Framework; das JavaScript-Framework, dessen Build-Pipeline jede Quelldatei über Meriyah parst

Ich habe ein JavaScript-Framework namens Brisa gebaut. Die Art von Framework, das jede Quelldatei Ihrer Anwendung parsen muss; Imports analysieren, Server- vs. Client-Komponenten erkennen, Macros injizieren, JSX transformieren. All das passiert auf AST-Ebene.

Vor Brisa habe ich bereits next-translate gepflegt, eine i18n-Bibliothek für Next.js. Für das Plugin, das Locale-Loader automatisch in Seiten injiziert, habe ich die TypeScript Compiler API verwendet. Es funktionierte. Aber es war schmerzhaft langsam; ts.createProgram() für jede Seitendatei zur Build-Zeit, vollständige Type-Checker-Instanziierung, Lib-Aufloesung. Wir müssten noResolve: true und noLib: true hinzufuegen, nur um es ertraeglich zu machen. Der Parser machte zehnmal mehr Arbeit als noetig, weil alles, was wir wollten, der AST war, nicht die Typen.

Als ich anfing, Brisa zu bauen, wusste ich, dass ich etwas Schnelleres brauchte. Etwas, das mir einen ESTree-konformen AST ohne den Overhead eines vollständigen Compilers liefert. So fand ich Meriyah.

Warum ich Meriyah gewaehlt habe

Meriyah ist vollständig in JavaScript geschrieben. Keine nativen Bindings. Kein WASM-Ladeschritt. Keine Kompilierung. Nur parseScript(code, { jsx: true, module: true, next: true }) und Sie erhalten einen ESTree-AST in Mikrosekunden.

Für Brisas Build-Pipeline summiert sich dieser Geschwindigkeitsunterschied. Jede Quelldatei in einem Brisa-Projekt durchläuft Meriyah. Der Parser läuft in AST().parseCodeToAST(), das zuerst über Buns Transpiler transpiliert und dann das Ergebnis an Meriyah weitergibt.

Aber hier würde es interessant. Brisa hat ein Feature namens renderOn, mit dem Komponenten zur Build-Zeit vorgerendert werden können. Man schreibt dies in seiner Seite:

<SomeComponent renderOn="build" foo="bar" />

Zur Build-Zeit erkennt die AST-Transformation renderOn="build", ersetzt das JSX durch einen __prerender__macro()-Aufruf und injiziert diesen Import am Anfang der Datei:

import { __prerender__macro } from 'brisa/macros' with { type: 'macro' };

Dieses with { type: 'macro' } ist ein Import Attribute, das Buns Bundler anweist, den Import zur Kompilierzeit aufzulösen. Die Komponente wird während des Builds gerendert, und das Ergebnis wird als statisches HTML injiziert. Der Benutzer schreibt renderOn="build", aber intern konstruiert das Framework ImportDeclaration- und ImportAttribute-AST-Knoten von Hand und regeneriert den Code.

Das Problem: Meriyah unterstuetzte keine Import Attributes, als ich anfing, es zu benutzen. Also habe ich einen PR beigetragen, um das Feature hinzuzufuegen. Der PR würde akzeptiert, und Brisas gesamte Prerender-Pipeline könnte von Anfang bis Ende funktionieren.

Von "der Parser versteht meine Syntax nicht" zu "ich repariere den Parser selbst" zu gelangen, ist die Art von Sache, die nur passiert, wenn man tiefgehend versteht, wie ASTs funktionieren.

Die Inspiration

AST Explorer existiert, und es ist grossartig. Ich benutze es regelmaessig. Es ist das Referenz-Tool zum Erkunden von ASTs. Ich wollte etwas Aehnliches als Teil von Kitmul bauen; meine eigene Version eines AST-Visualizers mit Parser-Auswahl, interaktiver Baumansicht und Unterstuetzung für die Parser, die ich täglich verwende.

Der AST Visualizer macht genau das. JavaScript einfuegen, Parser wählen (Acorn, Meriyah oder SWC), und einen interaktiven Baum oder rohes JSON erhalten. Alles läuft lokal im Browser.

Die Parser-Wahl ist wichtig, weil jeder einen leicht anderen AST erzeugt:

  • Acorn folgt der ESTree-Spezifikation strikt. Es ist der Parser, den ESLint intern verwendet.
  • Meriyah folgt ebenfalls ESTree, fuegt aber JSX-Support und neueste Features über das next: true-Flag hinzu. Es ist der Parser, den ich für Brisa gewaehlt habe, weil er schnell, leicht und in purem JS geschrieben ist.
  • SWC ist ein Rust-basierter Compiler, der via WASM im Browser läuft. Sein AST verwendet eine andere Struktur; Module statt Program, span-Objekte statt start/end-Positionen.

Drei Dinge, die der Baum lehrt und die Dokumentation nicht

next-translate Bundle-Größenvergleich; die i18n-Bibliothek für Next.js, bei der ich erstmals mit AST-Parsing über die TypeScript Compiler API arbeitete
next-translate Bundle-Größenvergleich; die i18n-Bibliothek für Next.js, bei der ich erstmals mit AST-Parsing über die TypeScript Compiler API arbeitete

1. Ausdruecke vs. Anweisungen werden sichtbar.

x = 5;

Der AST zeigt ein ExpressionStatement, das einen AssignmentExpression umschliesst. Diese Unterscheidung erklärt, warum if (x = 5) gueltiges JavaScript ist.

2. Operatorrangfolge wird strukturell.

Parsen Sie 2 + 3 * 4 und die Multiplikation ist innerhalb des rechten Operanden der Addition verschachtelt. Der tiefere Knoten wird zuerst ausgewertet.

3. Import Attributes zeigen, wie renderOn="build" funktioniert.

Parsen Sie dies mit Meriyah:

import { __prerender__macro } from 'brisa/macros' with { type: 'macro' };

Der ImportDeclaration-Knoten erhaelt ein attributes-Array mit ImportAttribute-Knoten. Jedes Attribut hat einen key und einen value, beides Literal-Knoten. Dies ist der Import, den Brisas Build-Pipeline injiziert, wenn sie renderOn="build" auf einer Komponente findet. Das with { type: 'macro' } weist Bun an, die Funktion zur Kompilierzeit aufzulösen. Ohne den Baum zu sehen, würden Sie nie erraten, dass with { type: 'macro' } zu einem verschachtelten Array von Attribut-Objekten wird.

Reale Anwendungsfaelle beim Framework-Bau

Build-Pipelines. In Brisa wird jede Quelldatei zu einem AST geparst, für Imports analysiert, transformiert (Macro-Injektion, Server/Client-Trennung, i18n-Verarbeitung) und als Code regeneriert.

Prerender-Macro-Injektion über renderOn="build". Wenn Brisa <Foo renderOn="build" /> findet, konstruiert die AST-Transformation ImportAttribute-Knoten von Hand, um import {__prerender__macro} from 'brisa/macros' with { type: "macro" } zu injizieren. Es gibt eine Eigenheit: Meriyah verwendet value bei Literal-Knoten, wo astring name erwartet. Das ist ein tatsächlicher Kommentar im Brisa-Quellcode: // This astring is looking for "name", but meriyah "value". So etwas entdeckt man nur, wenn man Baeume studiert.

i18n-Loader-Injektion. In next-translate-plugin verwendet der Webpack-Loader ts.createProgram() zum Parsen jeder Seite. Der TypeScript-AST verwendet SyntaxKind-Enums statt string-basierter Typen.

Import-Pfad-Aufloesung. Brisa löst relative Imports zur Build-Zeit in absolute Pfade auf. Die Transformation durchläuft ImportDeclaration-Knoten und ersetzt source.value.

Parser-Vergleich

Funktion Acorn Meriyah SWC
Sprache JavaScript JavaScript Rust (WASM im Browser)
Spezifikation ESTree ESTree SWC AST
JSX-Support Nein Ja Ja
Import Attributes Nein Ja Ja
Geschwindigkeit Schnell Sehr schnell Schnell (nach WASM-Ladung)
Bundle-Größe ~120KB ~320KB ~14MB (WASM)
Verwendet von ESLint Brisa Next.js, Turbopack

Der AST Visualizer zeigt die Parser-Auswahl zwischen Acorn, Meriyah und SWC mit Baum- und JSON-Ansichtsmodi
Der AST Visualizer zeigt die Parser-Auswahl zwischen Acorn, Meriyah und SWC mit Baum- und JSON-Ansichtsmodi

Fuenf Code-Snippets zum Erkunden

Fügen Sie diese in den AST Visualizer ein und probieren Sie jeden Parser:

1. Arrow Function mit implizitem Return:

const add = (a, b) => a + b;

2. Import Attributes (verwenden Sie Meriyah oder SWC):

import { __prerender__macro } from 'brisa/macros' with { type: 'macro' };

3. Optional Chaining:

const value = obj?.nested?.deep?.property;

4. Async/await:

async function fetchData() {
  const response = await fetch('/api/data');
  return response.json();
}

5. Destrukturierung mit Standardwerten:

const { a = 1, b: { c = 2 } = {} } = config;

Datenschutz

Alle drei Parser laufen vollständig in Ihrem Browser. Kein Code wird an einen Server übertragen. Die Sammlung Visualisierer und Logik-Tools umfasst Graph-Visualisierer und Regex-Tools, die gut zur AST-Arbeit passen. Der Pomodoro Timer mit integrierter Fokus-Musik ist überraschend effektiv für Lernsessions.

Die eigentliche Erkenntnis

Ich bin von der TypeScript Compiler API in next-translate zur Contribution von Parser-Features an Meriyah für Brisa gegangen. Der Wendepunkt war nicht mehr Dokumentation lesen. Es war, genug ASTs zu sehen, bis die Knotentypen zur zweiten Natur würden.

Der AST Visualizer wird Ihnen keine Compiler-Theorie beibringen. Er wird Ihnen zeigen, was der Parser sieht, wenn er Ihren Code liest. Für Framework-Internals, Build-Tools, Codemods und ESLint-Regeln ist das das Einzige, was zaehlt.


Der AST Visualizer ist kostenlos, privat und läuft vollständig in Ihrem Browser. Keine Anmeldung, keine Installation, keine Daten verlassen Ihr Gerät. Teil der Visualisierer und Logik-Tools Sammlung auf Kitmul.

Diesen Artikel teilen

Newsletter

Erhalte Produktivitätstipps und Neue Tools Zuerst

Schließe dich Machern und Entwicklern an, die Datenschutz schätzen. Jede Ausgabe: neue Tools, Produktivitäts-Hacks und Updates — kein Spam.

Prioritätszugang zu neuen Tools
Jederzeit abbestellen, ohne Rückfragen