Erstellen Sie mit Babel benutzerdefinierte JavaScript-Syntaxkonstrukte. Teil 1

Heute veröffentlichen wir den ersten Teil der Übersetzung des Materials, der der Erstellung eigener Syntaxkonstrukte für JavaScript mit Babel gewidmet ist.



Rückblick


Schauen wir uns zunächst an, was wir erreichen werden, wenn wir am Ende dieses Materials angelangt sind:

//  '@@'   `foo`   function @@ foo(a, b, c) {   return a + b + c; } console.log(foo(1, 2)(3)); // 6 

Wir werden die @@ implementieren, die Currying- Funktionen ermöglicht. Diese Syntax ähnelt der zum Erstellen von Generatorfunktionen verwendeten . In unserem Fall wird jedoch anstelle des Zeichens * eine Folge von @@ Zeichen zwischen dem function und dem function @@ . Wenn Sie Funktionen deklarieren, können Sie daher eine Konstruktion der Formularfunktion function @@ name(arg1, arg2) .

Im obigen Beispiel können Sie bei der Arbeit mit der Funktion foo deren Teilanwendung verwenden . Wenn Sie die Funktion foo indem Sie so viele Parameter übergeben, die geringer sind als die Anzahl der benötigten Argumente, wird eine neue Funktion zurückgegeben, die die verbleibenden Argumente annehmen kann:

 foo(1, 2, 3); // 6 const bar = foo(1, 2); // (n) => 1 + 2 + n bar(3); // 6 

Ich habe die Reihenfolge der @@ Zeichen gewählt, da das @ -Symbol nicht in Variablennamen verwendet werden kann. Dies bedeutet, dass ein Konstrukt der Formularfunktion function@@foo(){} auch syntaktisch korrekt ist. Außerdem wird der Operator @ für Dekorationsfunktionen verwendet , und ich wollte etwas völlig Neues verwenden. Aus diesem Grund habe ich mich für die @@ .

Um unser Ziel zu erreichen, müssen wir folgende Aktionen ausführen:

  • Erstellen Sie eine Abzweigung des Babel-Parsers.
  • Erstellen Sie Ihr eigenes Babel-Plugin für die Code-Transformation.

Sieht nach etwas Unmöglichem aus?
Tatsächlich gibt es hier nichts Schreckliches. Wir werden alles gemeinsam im Detail analysieren. Ich hoffe, wenn Sie dies lesen, werden Sie die Feinheiten von Babel meisterhaft beherrschen.

Eine Gabel erstellen Babel


Gehen Sie zum Babel- Repository auf GitHub und klicken Sie auf die Schaltfläche Fork , die sich oben links auf der Seite befindet.


Erstellen einer Gabel von Babel ( Bild in voller Größe )

Übrigens, wenn Sie gerade zum ersten Mal die Gabelung des beliebten Open Source-Projekts erstellt haben - herzlichen Glückwunsch!

Klonen Sie nun die Babel-Gabel auf Ihren Computer und bereiten Sie sie für die Arbeit vor .

 $ git clone https://github.com/tanhauhau/babel.git # set up $ cd babel $ make bootstrap $ make build 

Lassen Sie mich nun kurz auf die Organisation des Babel-Repositorys eingehen.

Babel verwendet ein Monorepository. Alle Pakete (z. B. @babel/core , @babel/parser , @babel/plugin-transform-react-jsx usw.) befinden sich im Ordner packages/ . Es sieht so aus:

 - doc - packages  - babel-core  - babel-parser  - babel-plugin-transform-react-jsx  - ... - Gulpfile.js - Makefile - ... 

Ich stelle fest, dass Babel ein Makefile verwendet, um Aufgaben zu automatisieren. Beim make build eines Projekts mit dem make build wird Gulp als Task-Manager verwendet.

Code-Konvertierung in einen AST-Kurzkurs


Wenn Sie mit Konzepten wie „Parser“ und „Abstract Syntax Tree“ (AST) nicht vertraut sind, empfehle ich Ihnen dringend, sich dieses Material anzusehen, bevor Sie weiterlesen.

Wenn Sie sehr kurz darüber sprechen, was beim Parsen (Parsen) des Codes passiert, erhalten Sie Folgendes:

  • Der als Zeichenfolge dargestellte Code ( string ) sieht aus wie eine lange Liste von Zeichen: f, u, n, c, t, i, o, n, , @, @, f, ...
  • Zu Beginn führt Babel eine Code-Tokenisierung durch. In diesem Schritt scannt Babel den Code und erstellt Token. Zum Beispiel so etwas wie function, @@, foo, (, a, ...
  • Dann werden die Token zum Parsen durch den Parser geleitet. Hier erstellt Babel basierend auf der Spezifikation der JavaScript-Sprache einen abstrakten Syntaxbaum.

Hier ist eine großartige Ressource für diejenigen, die mehr über Compiler erfahren möchten.

Wenn Sie denken, dass der „Compiler“ etwas sehr Komplexes und Unverständliches ist, dann wissen Sie, dass in Wirklichkeit nicht alles so mysteriös ist. Beim Kompilieren wird einfach der Code analysiert und auf seiner Grundlage ein neuer Code erstellt, den wir XXX nennen werden. Der XXX-Code kann durch Maschinencode dargestellt werden (vielleicht taucht Maschinencode zuerst in den Köpfen der meisten von uns auf, wenn wir an den Compiler denken). Dies kann JavaScript-Code sein, der mit älteren Browsern kompatibel ist. Tatsächlich ist eine der Hauptfunktionen von Babel die Kompilierung von modernem JS-Code zu Code, der für veraltete Browser verständlich ist.

Entwickeln Sie Ihren eigenen Parser für Babel


Wir werden im Ordner packages/babel-parser/ :

 - src/  - tokenizer/  - parser/  - plugins/    - jsx/    - typescript/    - flow/    - ... - test/ 

Wir haben bereits über Tokenisierung und Analyse gesprochen. Sie finden den Code, der diese Prozesse implementiert, in Ordnern mit den entsprechenden Namen. Die plugins/ Ordner enthalten Plugins (Plug-Ins), die die Funktionen des Basisparsers erweitern und dem System Unterstützung für zusätzliche Syntaxen hinzufügen. Genau so wird beispielsweise flow Unterstützung von jsx und flow implementiert.

Lösen wir unser Problem mithilfe der Entwicklungstechnologie durch Testen (Test Driven Development, TDD). Meiner Meinung nach ist es am einfachsten, zuerst einen Test zu schreiben und dann nach und nach am System diesen Test fehlerfrei auszuführen. Dieser Ansatz eignet sich besonders gut, wenn Sie in einer unbekannten Codebasis arbeiten. Mit TDD können Sie leicht nachvollziehen, wo Sie Änderungen am Code vornehmen müssen, um die beabsichtigte Funktionalität zu implementieren.

 packages/babel-parser/test/curry-function.js import { parse } from '../lib'; function getParser(code) {  return () => parse(code, { sourceType: 'module' }); } describe('curry function syntax', function() {  it('should parse', function() {    expect(getParser(`function @@ foo() {}`)()).toMatchSnapshot();  }); }); 

Sie können den Test für babel-parser folgendermaßen TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only : TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only . Auf diese Weise können Sie die Fehler anzeigen:

 SyntaxError: Unexpected token (1:9) at Parser.raise (packages/babel-parser/src/parser/location.js:39:63) at Parser.raise [as unexpected] (packages/babel-parser/src/parser/util.js:133:16) at Parser.unexpected [as parseIdentifierName] (packages/babel-parser/src/parser/expression.js:2090:18) at Parser.parseIdentifierName [as parseIdentifier] (packages/babel-parser/src/parser/expression.js:2052:23) at Parser.parseIdentifier (packages/babel-parser/src/parser/statement.js:1096:52) 

Wenn Sie feststellen, dass das Anzeigen aller Tests zu lange dauert, können Sie, um den gewünschten Test auszuführen, jest direkt aufrufen:

 BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/curry-function.js 

Unser Parser entdeckte 2 @ Token, scheinbar völlig unschuldig, wo sie nicht sein sollten.

Woher wusste ich das? Die Antwort auf diese Frage hilft uns, die Verwendung des Codeüberwachungsmodus zu finden, der mit dem make watch gestartet wird.

Das Anzeigen des Aufrufstapels führt uns zu packages / babel-parser / src / this.unexpected() / expression.js , wo die Ausnahme this.unexpected() ausgelöst wird.

Fügen Sie dieser Datei einige Protokollierungsbefehle hinzu:

 packages/babel-parser/src/parser/expression.js parseIdentifierName(pos: number, liberal?: boolean): string {  if (this.match(tt.name)) {    // ...  } else {    console.log(this.state.type); //      console.log(this.lookahead().type); //      throw this.unexpected();  } } 

Wie Sie sehen können, sind beide Token @ :

 TokenType {  label: '@',  // ... } 

Wie habe ich herausgefunden, dass die Konstruktionen this.state.type und this.lookahead().type mir die aktuellen und nächsten Token geben?
Ich werde darüber in dem Abschnitt dieses Materials this.eat , der den Funktionen this.eat , this.match und this.next .

Bevor wir fortfahren, fassen wir zusammen:

  • Wir haben einen Test für babel-parser .
  • Wir haben den Test nur mit make test-only .
  • Wir haben den Code-Überwachungsmodus mit make watch .
  • Wir haben den Status des Parsers this.state.type und Informationen zum Typ des aktuellen Tokens ( this.state.type ) in der Konsole this.state.type .

Und jetzt werden wir sicherstellen, dass 2 @ -Zeichen nicht als separate Token wahrgenommen werden, sondern als neues @ @@ , das wir für Curry-Funktionen verwendet haben.

Neues Token: "@@"


Schauen wir uns zunächst an, wo die Arten von Token bestimmt werden. Dies ist die Datei packages / babel-parser / src / tokenizer / types.js .

Hier finden Sie eine Liste der Token. Fügen Sie hier die Definition des neuen atat Tokens hinzu:

 packages/babel-parser/src/tokenizer/types.js export const types: { [name: string]: TokenType } = {  // ...  at: new TokenType('@'),  atat: new TokenType('@@'), }; 

Suchen wir nun nach der Stelle im Code, an der beim Tokenisierungsprozess Token erstellt werden. Wenn Sie die Reihenfolge der tt.at Zeichen in babel-parser/src/tokenizer tt.at babel-parser/src/tokenizer , gelangen Sie zur folgenden Datei: packages / babel-parser / src / tokenizer / index.js . Im babel-parser werden Tokentypen als tt importiert.

Wenn nun nach dem aktuellen @ -Symbol ein weiteres @ , erstellen tt.atat anstelle des tt.at Tokens ein neues Token tt.atat :

 packages/babel-parser/src/tokenizer/index.js getTokenFromCode(code: number): void {  switch (code) {    // ...    case charCodes.atSign:      //    -  `@`      if (this.input.charCodeAt(this.state.pos + 1) === charCodes.atSign) {        //  `tt.atat`  `tt.at`        this.finishOp(tt.atat, 2);      } else {        this.finishOp(tt.at, 1);      }      return;    // ...  } } 

Wenn Sie den Test erneut ausführen, werden Sie feststellen, dass sich die Informationen zu den aktuellen und nächsten Token geändert haben:

 //   TokenType {  label: '@@',  // ... } //   TokenType {  label: 'name',  // ... } 

Es sieht schon ziemlich gut aus. Wir werden die Arbeit fortsetzen.

Neuer Parser


Bevor Sie fortfahren, schauen Sie sich an, wie Generatorfunktionen in AST dargestellt werden.


AST für Generatorfunktion ( Bild in voller Größe )

Wie Sie sehen können, gibt das Attribut generator: true der Entität FunctionDeclaration an, dass es sich um eine Generatorfunktion handelt.

Wir können einen ähnlichen Ansatz verfolgen, um eine Funktion zu beschreiben, die das Curry unterstützt. Wir können nämlich das Attribut curry: true zu FunctionDeclaration hinzufügen.


AST für die Curry-Funktion ( Bild in voller Größe )

Eigentlich haben wir jetzt einen Plan. Beschäftigen wir uns mit seiner Implementierung.

Wenn Sie im Code nach dem Wort FunctionDeclaration suchen, können Sie zur Funktion parseFunction , die in packages / babel-parser / src / parser / statement.js deklariert ist. Hier finden Sie die Zeile, in der das generator ist. Fügen Sie dem Code eine weitere Zeile hinzu:

 packages/babel-parser/src/parser/statement.js export default class StatementParser extends ExpressionParser {  // ...  parseFunction<T: N.NormalFunction>(    node: T,    statement?: number = FUNC_NO_FLAGS,    isAsync?: boolean = false  ): T {    // ...    node.generator = this.eat(tt.star);    node.curry = this.eat(tt.atat);  } } 

Wenn wir den Test erneut durchführen, erwartet uns eine angenehme Überraschung. Der Code wurde erfolgreich getestet!

 PASS packages/babel-parser/test/curry-function.js  curry function syntaxshould parse (12ms) 

Und das ist alles? Was haben wir getan, um den Test auf wundersame Weise zu bestehen?

Um dies herauszufinden, lassen Sie uns darüber sprechen, wie das Parsen funktioniert. Ich hoffe, Sie werden im Verlauf dieses Gesprächs verstehen, wie die Zeile node.curry = this.eat(tt.atat); .

Fortsetzung folgt…

Liebe Leser! Benutzt du babel


Source: https://habr.com/ru/post/de470876/


All Articles