Acest document descrie cum se pot crea plugin-uri pentru Babel.
Acest manual este disponibil și în alte limbi, a se vedea README pentru o listă completă.
- Introducere
- Concepte de bază
- API
- Scrierea primului Plugin Babel
- Operații de Transformare
- Vizitare (Visiting)
- Verificare dacă un nod este de un anumit tip
- Verificare dacă un identificator are referință
- Manipulare
- Înlocuirea unui nod
- Înlocuirea unui nod cu mai multe noduri
- Înlocuirea unui nod cu un șir de caractere sursă
- Inserarea unui nod pe același nivel
- Ștergerea unui nod
- Înlocuirea unui părinte
- Ștergerea unui părinte
- Domeniu (Scope)
- Verificare dacă o variabilă locală este legată
- Generarea unui UID
- Mutarea unei declarații de variabilă într-un domeniu părinte
- Redenumirea unei legături și a referințelor sale
- Opțiuni de plugin
- Construirea nodurilor
- Practici preferate
Babel este un compilator generic multi-scop pentru JavaScript. Mai mult decât atât, este o colecție de module care pot fi utilizate pentru diverse tipuri de analiză statică.
Analiza statică este procesul de a analiza cod fără a-l executa. (Analiza de cod, în timp ce se execută este cunoscută sub denumirea de analiză dinamică). Scopul analizei statice variază foarte mult. Poate fi folosită pentru validare (linting), compilare, evidențiere (highlighting), transformare, optimizare, minimizare, și multe altele.
Puteți utiliza Babel pentru a construi diverse tipuri de instrumente care vă pot ajuta să fiți mai productivi și pentru a scrie programe mai bune.
Pentru actualizări, urmăriţi-l pe @thejameskyle pe Twitter.
Babel este un compilator de JavaScript, mai exact un compilator sursă-la-sursă, deseori numit un "transpiler". Asta înseamnă că dacă îi pasezi cod JavaScript, Babel modifică codul, și generează cod nou.
Fiecare dintre acești pași implică crearea sau lucrul cu un Arbore Abstract de Sintaxă sau AST.
Babel folosește un AST modificat din ESTree, cu specificațiile interne aflate aici.
function square(n) {
return n * n;
}
Examinați AST Explorer pentru a înțelege mai bine nodurile AST. Aici este un link, cu exemplul de cod de mai sus.
Același program poate fi reprezentat printr-o listă, ca aceasta:
- FunctionDeclaration:
- id:
- Identifier:
- name: square
- params [1]
- Identifier
- name: n
- body:
- BlockStatement
- body [1]
- ReturnStatement
- argument
- BinaryExpression
- operator: *
- left
- Identifier
- name: n
- right
- Identifier
- name: n
Sau printr-un obiect JavaScript ca acesta:
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
params: [{
type: "Identifier",
name: "n"
}],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}
Veți observa că fiecare nivel AST are o structură similară:
{
type: "FunctionDeclaration",
id: {...},
params: [...],
body: {...}
}
{
type: "Identifier",
name: ...
}
{
type: "BinaryExpression",
operator: ...,
left: {...},
right: {...}
}
Notă: Unele proprietăți au fost eliminate pentru simplitate.
Fiecare dintre acestea sunt cunoscute sub denumirea de Nod. AST-ul poate fi alcătuit dintr-un singur nod, sute sau mii de noduri. Impreună ele sunt capabile să descrie sintaxa unui program care poate fi folosită pentru analiză statică.
Fiecare Nod are această interfață:
interface Node {
type: string;
}
Câmpul type
este un string reprezentând tipul Nodului (ex. "FunctionDeclaration"
, "Identifier"
, sau "BinaryExpression"
). Fiecare tip de Nod definește un set suplimentar de proprietăţi care descriu acel nod.
Există proprietăţi suplimentare pe fiecare Nod, generate de Babel, care descriu poziţia Nodului în codul sursă original.
{
type: ...,
start: 0,
end: 38,
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 3,
column: 1
}
},
...
}
Aceste proprietăţi start
, end
, loc
, apar în fiecare Nod.
Cele trei etape principale ale Babel sunt analiză, transformare, generare.
Etapa de analiză, primeste codul şi produce AST-ul. Există două faze ale analizei în Babel: Analiza lexicală şi Analiza sintactică.
Analiza lexicală primeste un şir de cod şi-l transformă într-un flux de simboluri (tokens).
Vă puteţi gândi la tokens ca o matrice uni-dimensională de piese de sintaxă a limbii.
n * n;
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
...
]
Fiecare type
are un set de proprietăţi care descrie token-ul:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
La fel ca nodurile AST, acestea conțin start
, end
, și loc
.
Analiza sintactică primește un flux de token-uri şi-l transformă într-o reprezentare AST. Folosind informaţiile din token-uri, această fază le va reformata ca un AST care reprezintă structura codului într-un mod care este mai uşor de utilizat.
Etapa de Transformare primește un AST pe care-l traversează, adăugă, actualizează şi sterge noduri. Această etapă este de departe cea mai complexă din Babel sau din orice alt compilator. Acesta este locul în care plugin-urile acționează de fapt, aşadar va fi subiectul majorității capitolelor din acest manual. Nu vom intra prea adânc în detalii pentru moment.
Etapa de generare de cod primește AST-ul final şi-l transformă înapoi într-un şir de cod, creând şi source maps.
Generarea de cod este destul de simplă: se traversează AST-ul și se construiește un şir de caractere care reprezintă codul transformat.
Atunci când doriţi să transformați un AST trebuie să-l traversați recursiv.
Să zicem că avem tipul FunctionDeclaration
. El are câteva proprietăți: id
, params
și body
. Fiecare dintre ele au noduri imbricate.
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
params: [{
type: "Identifier",
name: "n"
}],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}
Vom începe cu FunctionDeclaration
şi ştim proprietăţile sale interne, astfel încât vom vizita fiecare proprietate şi copiii săi, în ordine.
Apoi vom continua cu id
, care este un Identificator
. Identificatorii
nu au proprietăţi copil, așadar putem merge mai departe.
Urmează params
, care este o matrice de noduri, așadar le vom vizita pe fiecare în parte. În acest caz este un singur nod care este de asemenea un Identificator
aşadar putem merge mai departe.
Apoi ajungem la body
, care este un BlockStatement
cu o proprietate body
, care este o serie de noduri, aşa că le vom vizita pe fiecare dintre ele.
Singurul element de aici este un nod ReturnStatement
, care are un argument
, vom merge la argument
unde găsim un BinaryExpression
.
BinaryExpression
conține un operator
, un left
, şi un right
. "Operator" nu este un nod, doar o valoare, așadar o ignorăm, şi în schimb vizităm doar left
şi right
.
Acest proces de traversare se întâmplă de-a lungul etapei de transformare Babel.
Atunci când vorbim despre "a merge" la un nod, ne referim de fapt la a-l vizita. Motivul pentru care vom folosi acest termen este pentru că există acest concept de vizitator.
Vizitatorii sunt un model folosit în traversarea AST, utilizat în diverse limbaje. În termeni simpli, aceștia sunt obiecte cu metode definite pentru a accepta anumite tipuri de nod dintr-un AST. Această definiție poate fi puțin abstractă, așadar să luăm un exemplu.
const MyVisitor = {
Identifier() {
console.log("Called!");
}
};
Notă:
Identifier() { ... }
este o prescurtare pentruIdentifier: {enter() { ... }}
.
Aceasta este un vizitator simplu care atunci când este utilizat în timpul traversării va apela metoda Identifier()
pentru fiecare Identificator
din arbore.
Așadar, cu acest cod, metoda Identifier()
va fi apelată de patru ori cu fiecare Identificator
(inclusiv square
).
function square(n) {
return n * n;
}
Called!
Called!
Called!
Called!
Toate aceste apeluri se petrec la intrarea în nod. Cu toate acestea, există, de asemenea, posibilitatea de a apela o metodă vizitator la ieşire.
Imaginaţi-vă că avem această structură de arbore:
- FunctionDeclaration
- Identifier (id)
- Identifier (params[0])
- BlockStatement (body)
- ReturnStatement (body)
- BinaryExpression (argument)
- Identifier (left)
- Identifier (right)
În timpul parcurgerii fiecărei ramuri, vom ajunge în cele din urmă într-o înfundătură, unde trebuie să traversăm arborele în sens invers pentru a ajunge la nodul următor. Mergând în jos prin arbore intrăm în fiecare nod, iar când parcurgem în sens invers ieșim din fiecare nod.
Haideţi să parcurgem acest proces de traversare pentru arborele de mai sus.
- Intrare
FunctionDeclaration
- Intrare
Identifier (id)
- Înfundătură
- Ieșire
Identifier (id)
- Intrare
Identifier (params[0])
- Înfundătură
- Ieșire
Identifier (params[0])
- Intrare
BlockStatement (body)
- Intrare
ReturnStatement (body)
- Intrare
BinaryExpression (argument)
- Intrare
Identifier (left)
- Înfundătură
- Ieșire
Identifier (left)
- Intrare
Identifier (right)
- Înfundătură
- Ieșire
Identifier (right)
- Ieșire
BinaryExpression (argument)
- Intrare
- Ieșire
ReturnStatement (body)
- Ieșire
BlockStatement (body)
- Intrare
- Ieșire
FunctionDeclaration
Așadar, când creaţi un vizitator aveţi două ocazii de a vizita un nod.
const MyVisitor = {
Identifier: {
enter() {
console.log("Entered!");
},
exit() {
console.log("Exited!");
}
}
};
AST o are în general multe Noduri, dar cum se relaționează ele unul la altul? Am putea avea un singur obiect mutabil gigant, care să-l manipulăm şi să avem acces deplin la el, sau putem simplifica acest lucru cu Trasee (Paths).
Un Traseu (Path) este o reprezentare de obiect a legăturii între două noduri.
De exemplu, dacă luăm următorul nod şi copilul său:
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
...
}
Şi reprezentăm copilul Identifier
ca un Traseu, ar arăta ceva de genul acesta:
{
"parent": {
"type": "FunctionDeclaration",
"id": {...},
....
},
"node": {
"type": "Identifier",
"name": "square"
}
}
De asemenea, conține metadate suplimentare despre traseu:
{
"parent": {...},
"node": {...},
"hub": {...},
"contexts": [],
"data": {},
"shouldSkip": false,
"shouldStop": false,
"removed": false,
"state": null,
"opts": null,
"skipKeys": null,
"parentPath": null,
"context": null,
"container": null,
"listKey": null,
"inList": false,
"parentKey": null,
"key": null,
"scope": null,
"type": null,
"typeAnnotation": null
}
Precum şi foarte multe metode legate de adăugarea, actualizarea, mutarea, şi ștergerea de noduri, dar vom ajunge la ele mai târziu.
Într-un anumit sens, traseele sunt o reprezentare reactivă a poziţiei unui nod în arbore şi multe alte informatii despre nod. Ori de câte ori apelați o metodă care modifică arborele, această informaţie este actualizată. Babel gestionează toate acestea pentru a face lucrul cu noduri cât mai ușor posibil.
Când aveţi un vizitator care are o metodă Identifier()
, de fapt se vizitează traseul, nu nodul. În acest fel se lucrează cu reprezentarea reactivă a nodului, nu cu nodul în sine.
const MyVisitor = {
Identifier(path) {
console.log("Visiting: " + path.node.name);
}
};
a + b + c;
Visiting: a
Visiting: b
Visiting: c
Starea este duşmanul transformării AST-ului. Starea îți va crea mari probleme şi ipotezele tale despre stare vor fi aproape întotdeauna greşite, din cauza unei sintaxe care nu ai luat-o în considerare.
Să considerăm următorul cod:
function square(n) {
return n * n;
}
Să scriem un vizitator rapid, care va redenumi n
în x
.
let paramName;
const MyVisitor = {
FunctionDeclaration(path) {
const param = path.node.params[0];
paramName = param.name;
param.name = "x";
},
Identifier(path) {
if (path.node.name === paramName) {
path.node.name = "x";
}
}
};
Acest lucru ar putea funcționa pentru codul de mai sus, dar îl putem strica uşor dacă facem acest lucru:
function square(n) {
return n * n;
}
n;
O modalitate mai bună de a rezolva această problema este folosind recursivitate. Așadar, haideți să facem ca într-un film de Christopher Nolan şi să punem un vizitator în interiorul unui vizitator.
const updateParamNameVisitor = {
Identifier(path) {
if (path.node.name === this.paramName) {
path.node.name = "x";
}
}
};
const MyVisitor = {
FunctionDeclaration(path) {
const param = path.node.params[0];
const paramName = param.name;
param.name = "x";
path.traverse(updateParamNameVisitor, { paramName });
}
};
Desigur, acesta este un exemplu teoretic, însă demonstrează cum să eliminăm starea globală din vizitatori.
În continuare vom introduce conceptul de domeniu. JavaScript utilizează domeniu lexical, care este o structură de arbore, în care fiecare bloc crează un nou domeniu.
// global scope
function scopeOne() {
// scope 1
function scopeTwo() {
// scope 2
}
}
Ori de câte ori creaţi o referinţă în JavaScript, fie că este o variabilă, funcție, clasă, parametru, import, etichetî, etc., aceasta aparţine actualului domeniu.
var global = "I am in the global scope";
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
var two = "I am in the scope created by `scopeTwo()`";
}
}
Codul dintr-un domeniu mai adânc poate utiliza o referință dintr-un domeniu superior.
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
}
}
Un domeniu mai adânc ar putea crea, de asemenea, o referință cu același nume fără a o modifica.
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
}
}
Când scriem o transformare, vrem să ținem cont de domeniu. Trebuie să ne asigurăm că nu stricăm cod existent în timp ce modificăm diverse părți din el.
Probabil vom dori să adăugăm noi referinţe şi trebuie sa ne asigurăm că acestea nu intră în coliziune cu cele existente. Sau poate vrem doar să găsim unde se referențiază o anumită variabilă. Vrem să fim capabili să urmărim aceste referinţe într-un anumit domeniu.
Un domeniu poate fi reprezentat în felul următor:
{
path: path,
block: path.node,
parentBlock: path.parent,
parent: parentScope,
bindings: [...]
}
Crearea unui domeniu nou implică pasarea unui traseu şi a unui domeniu părinte. Apoi, în timpul procesului de traversare se colectează toate referințele ("legături") din acel domeniu.
Odată ce am făcut acest lucru, există tot felul de metode ce le putem utiliza pe domenii. Însă le vom examina mai târziu.
Toate referinţele aparţin unui anumit domeniu; această relaţie este cunoscută sub denumirea de legătură.
function scopeOnce() {
var ref = "This is a binding";
ref; // This is a reference to a binding
function scopeTwo() {
ref; // This is a reference to a binding from a lower scope
}
}
O legătură arată astfel:
{
identifier: node,
scope: scope,
path: path,
kind: 'var',
referenced: true,
references: 3,
referencePaths: [path, path, path],
constant: false,
constantViolations: [path]
}
Cu aceste informaţii putem găsi toate referințele la o legătură, putem vedea ce tip de legătură este (parametru, declaraţie etc.), putem căuta cărui domeniu îi aparține, sau putem să-i copiem identificatorul. Putem chiar să aflăm dacă este constantă şi, dacă nu, putem afla ce trasee o determină să fie variabilă, nu constantă.
Fiind capabili să spunem dacă o legătură este constantă este utilă pentru multe scopuri, insă cel mai mare este minimizarea codului.
function scopeOne() {
var ref1 = "This is a constant binding";
becauseNothingEverChangesTheValueOf(ref1);
function scopeTwo() {
var ref2 = "This is *not* a constant binding";
ref2 = "Because this changes the value";
}
}
Babel este de fapt o colecţie de module. În această secţiune vom trece prin cele mai importante, explicând la ce ajută şi cum se utilizează.
Notă: Acest document nu este un înlocuitor pentru documentaţia detaliată a API-ului, care va fi disponibilă în altă parte în scurt timp.
Babylon este analizorul din Babel. A început ca o bifurcație din Acorn, este rapid, simplu de utilizat, are o arhitectură bazată pe plugin-uri pentru caracteristici neconvenţionale (precum şi viitoarele standarde).
În primul rând, să-l instalăm.
$ npm install --save babylon
Să începem pur şi simplu prin parsarea unui şir de cod:
import * as babylon from "babylon";
const code = `function square(n) {
return n * n;
}`;
babylon.parse(code);
// Node {
// type: "File",
// start: 0,
// end: 38,
// loc: SourceLocation {...},
// program: Node {...},
// comments: [],
// tokens: [...]
// }
Putem, de asemenea, sa pasăm opţiuni metodei parse()
astfel:
babylon.parse(code, {
sourceType: "module", // default: "script"
plugins: ["jsx"] // default: []
});
sourceType
poate fi "module"
sau "script"
, care este modul în care Babylon ar trebui să-l analizeze. "module"
va analiza în mod strict (strict mode) şi permite declaraţii de module, "script"
nu va permite acest lucru si nu va analiza implicit in mod strict.
Notă:
sourceType
va lua valoarea implicită"script"
si va arunca eroare atunci când găsește valoareaimport
sauexport
. PasațisourceType: "module"
pentru a scăpa de aceste erori.
Din moment ce Babylon este construit cu o arhitectură bazată pe plugin-uri, există, de asemenea, o opţiune plugins
care permite activarea plugin-urilor interne. Reţineţi că Babylon nu a deschis încă API-ul pentru plugin-uri externe, deşi este posibil sa facă acest lucru în viitor.
Pentru a vedea o listă completă de plugin-uri, examinați Babylon README.
Modulul de Traversare Babel conține starea generală a arborelui, şi este responsabil pentru înlocuirea, ștergerea şi adăugarea de noduri.
Instalaţi-l prin rularea:
$ npm install --save babel-traverse
Putem să-l folosim alături de Babylon să traversăm şi să actualizăm noduri:
import * as babylon from "babylon";
import traverse from "babel-traverse";
const code = `function square(n) {
return n * n;
}`;
const ast = babylon.parse(code);
traverse(ast, {
enter(path) {
if (
path.node.type === "Identifier" &&
path.node.name === "n"
) {
path.node.name = "x";
}
}
});
Babel Types este o librărie de utilitare, similară cu Lodash, pentru nodurile AST. Conține metode pentru construirea, validarea şi conversia nodurilor AST. Este util pentru curățarea logicii AST cu metode utilitare bine gândite.
Îl puteţi instala prin rularea:
$ npm install --save babel-types
Apoi să-l utilizaţi:
import traverse from "babel-traverse";
import * as t from "babel-types";
traverse(ast, {
enter(path) {
if (t.isIdentifier(path.node, { name: "n" })) {
path.node.name = "x";
}
}
});
Babel Types conține definiţii pentru fiecare tip de nod, și informaţii cu privire la ce proprietăţile aparţin cui, ce valori sunt valide, cum se construiește un nod, cum ar trebui traversat nodul şi pseudonime ale nodului.
O definiţie a unui tip de nod arată astfel:
defineType("BinaryExpression", {
builder: ["operator", "left", "right"],
fields: {
operator: {
validate: assertValueType("string")
},
left: {
validate: assertNodeType("Expression")
},
right: {
validate: assertNodeType("Expression")
}
},
visitor: ["left", "right"],
aliases: ["Binary", "Expression"]
});
Veţi observa mai sus că definiţia pentru BinaryExpression
are un câmp builder
.
builder: ["operator", "left", "right"]
Acest lucru se datorează faptului că fiecare tip de nod primește o metodă constructor, care, atunci când este utilizată arată în felul următor:
t.binaryExpression("*", t.identifier("a"), t.identifier("b"));
Care creează un AST ca acesta:
{
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "a"
},
right: {
type: "Identifier",
name: "b"
}
}
Iar atunci când este tipărit arată astfel:
a * b
Constructorii, de asemenea, vor valida nodurile pe care le crează şi aruncă erori descriptive dacă sunt folosiți necorespunzător. Ceea ce ne conduce la următorul tip de metodă.
Definiția pentru BinaryExpression
include informații privind cămpurile (fields
) nodului şi cum să le validăm.
fields: {
operator: {
validate: assertValueType("string")
},
left: {
validate: assertNodeType("Expression")
},
right: {
validate: assertNodeType("Expression")
}
}
Acest lucru este folosit pentru a crea două tipuri de metode de validare. Prima dintre acestea este isX
.
t.isBinaryExpression(maybeBinaryExpressionNode);
Aceasta testează pentru a se asigura că nodul este o expresie binară, dar puteţi pasa, de asemenea, un al doilea parametru pentru a se asigura că nodul conţine anumite proprietăţi şi valori.
t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });
Există, de asemenea, mai multe, ehem, versiuni dogmatice ale acestor metode, care vor arunca erori în loc sa returneze adevărat (true
) sau fals (false
).
t.assertBinaryExpression(maybeBinaryExpressionNode);
t.assertBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });
// Error: Expected type "BinaryExpression" with option { "operator": "*" }
[WIP] în lucru
Babel Generator este generatorul de cod pentru Babel. Primește un AST şi îl transformă în cod cu sourcemaps.
Executaţi următoarea comandă pentru a-l instala:
$ npm install --save babel-generator
Apoi folosiți-l
import * as babylon from "babylon";
import generate from "babel-generator";
const code = `function square(n) {
return n * n;
}`;
const ast = babylon.parse(code);
generate(ast, null, code);
// {
// code: "...",
// map: "..."
// }
Puteţi pasa, de asemenea, opţiuni metodei generate()
.
generate(ast, {
retainLines: false,
compact: "auto",
concise: false,
quotes: "double",
// ...
}, code);
Babel Template este un alt modul micuț dar incredibil de util. Vă permite să scrieți şiruri de cod cu substituenţi care îi puteţi folosi în loc să construiți manual un AST uriaș.
$ npm install --save babel-template
import template from "babel-template";
import generate from "babel-generator";
import * as t from "babel-types";
const buildRequire = template(`
var IMPORT_NAME = require(SOURCE);
`);
const ast = buildRequire({
IMPORT_NAME: t.identifier("myModule"),
SOURCE: t.stringLiteral("my-module")
});
console.log(generate(ast).code);
var myModule = require("my-module");
Acum că sunteţi familiarizați cu toate elementele de bază din Babel, haideţi să le utilizăm împreună cu API-ul pentru plugin-uri.
Începe cu o funcţie
care primește obiectul babel
curent.
export default function(babel) {
// plugin contents
}
Deoarece îl veţi folosi foarte des, probabil doriți să pasați doar babel.types
astfel:
export default function({ types: t }) {
// plugin contents
}
Apoi, veţi returna un obiect cu o proprietate visitor
care este principalul vizitator pentru plugin.
export default function({ types: t }) {
return {
visitor: {
// visitor contents
}
};
};
Să scriem un plug-in rapid pentru a scoate în evidenţă modul în care funcţionează. Acesta este codul nostru sursă:
foo === bar;
Sau în forma AST:
{
type: "BinaryExpression",
operator: "===",
left: {
type: "Identifier",
name: "foo"
},
right: {
type: "Identifier",
name: "bar"
}
}
Vom începe prin adăugarea unei metode vizitator BinaryExpression
.
export default function({ types: t }) {
return {
visitor: {
BinaryExpression(path) {
// ...
}
}
};
}
Apoi să filtrăm doar token-urile BinaryExpression
care folosesc operatorul ===
.
visitor: {
BinaryExpression(path) {
if (path.node.operator !== "===") {
return;
}
// ...
}
}
Acum să înlocuim proprietatea left
cu un nou identificator:
BinaryExpression(path) {
if (path.node.operator !== "===") {
return;
}
path.node.left = t.identifier("sebmck");
// ...
}
În cazul în care vom rula acest plugin, ar rezulta:
sebmck === bar;
Acum să înlocuim si proprietatea right
.
BinaryExpression(path) {
if (path.node.operator !== "===") {
return;
}
path.node.left = t.identifier("sebmck");
path.node.right = t.identifier("dork");
}
Ceea ce conduce la rezultatul nostru final:
sebmck === dork;
Super mișto! Primul nostru plugin pentru Babel.
Dacă doriţi să verificaţi de ce tip este un anumit nod, modul preferat de a face acest lucru este:
BinaryExpression(path) {
if (t.isIdentifier(path.node.left)) {
// ...
}
}
De asemenea, puteţi face o verificare superficială pentru proprietăţile acelui nod:
BinaryExpression(path) {
if (t.isIdentifier(path.node.left, { name: "n" })) {
// ...
}
}
Aceasta este echivalentă cu:
BinaryExpression(path) {
if (
path.node.left != null &&
path.node.left.type === "Identifier" &&
path.node.left.name === "n"
) {
// ...
}
}
Identifier(path) {
if (path.isReferencedIdentifier()) {
// ...
}
}
Alternativ:
Identifier(path) {
if (t.isReferenced(path.node, path.parent)) {
// ...
}
}
BinaryExpression(path) {
path.replaceWith(
t.binaryExpression("**", path.node.left, t.numberLiteral(2))
);
}
function square(n) {
- return n * n;
+ return n ** 2;
}
ReturnStatement(path) {
path.replaceWithMultiple([
t.expressionStatement(t.stringLiteral("Is this the real life?")),
t.expressionStatement(t.stringLiteral("Is this just fantasy?")),
t.expressionStatement(t.stringLiteral("(Enjoy singing the rest of the song in your head)")),
]);
}
function square(n) {
- return n * n;
+ "Is this the real life?";
+ "Is this just fantasy?";
+ "(Enjoy singing the rest of the song in your head)";
}
Notă: Când se înlocuieşte o expresie cu mai multe noduri, acestea trebuie să fie declaraţii. Acest lucru este necesar deoarece Babel utilizează euristică pe scară largă la înlocuirea nodurilor, ceea ce înseamnă că puteţi face unele transformări destul de complexe, care altfel ar fi extrem de detaliate.
FunctionDeclaration(path) {
path.replaceWithSourceString(`function add(a, b) {
return a + b;
}`);
}
- function square(n) {
- return n * n;
+ function add(a, b) {
+ return a + b;
}
Notă: Nu este recomandat să utilizaţi acest API dacă nu aveți de a face cu șiruri de caractere sursă dinamice, altfel este mult mai eficient pentru a analiza codul în afara vizitatorului.
FunctionDeclaration(path) {
path.insertBefore(t.expressionStatement(t.stringLiteral("Because I'm easy come, easy go.")));
path.insertAfter(t.expressionStatement(t.stringLiteral("A little high, little low.")));
}
+ "Because I'm easy come, easy go.";
function square(n) {
return n * n;
}
+ "A little high, little low.";
Notă: Acesta ar trebui să fie întotdeauna o declaraţie sau o serie de declaraţii. Aceasta utilizează aceleaşi euristici menţionate în Înlocuirea unui nod cu mai multe noduri.
FunctionDeclaration(path) {
path.remove();
}
- function square(n) {
- return n * n;
- }
BinaryExpression(path) {
path.parentPath.replaceWith(
t.expressionStatement(t.stringLiteral("Anyway the wind blows, doesn't really matter to me, to me."))
);
}
function square(n) {
- return n * n;
+ "Anyway the wind blows, doesn't really matter to me, to me.";
}
BinaryExpression(path) {
path.parentPath.remove();
}
function square(n) {
- return n * n;
}
FunctionDeclaration(path) {
if (path.scope.hasBinding("n")) {
// ...
}
}
Aceasta va parcurge arborele şi va căuta acea legatură anume.
Puteţi verifica și dacă un domeniu are o anumită legătură proprie (own):
FunctionDeclaration(path) {
if (path.scope.hasOwnBinding("n")) {
// ...
}
}
Următorul cod va genera un identificator care nu se ciocnește cu nicio variabilă definită local.
FunctionDeclaration(path) {
path.scope.generateUidIdentifier("uid");
// Node { type: "Identifier", name: "_uid" }
path.scope.generateUidIdentifier("uid");
// Node { type: "Identifier", name: "_uid2" }
}
Uneori, poate doriţi să mutați un VariableDeclaration
, pentru a-i putea asocia o valoare.
FunctionDeclaration(path) {
const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id);
path.remove();
path.scope.parent.push({ id, init: path.node });
}
- function square(n) {
+ var _square = function square(n) {
return n * n;
- }
+ };
FunctionDeclaration(path) {
path.scope.rename("n", "x");
}
- function square(n) {
- return n * n;
+ function square(x) {
+ return x * x;
}
Alternativ, puteţi redenumi o legătură cu un identificator unic generat:
FunctionDeclaration(path) {
path.scope.rename("n");
}
- function square(n) {
- return n * n;
+ function square(_n) {
+ return _n * _n;
}
Dacă doriţi să lăsați utilizatorii să particularizeze comportamentul plugin-ul vostru Babel, puteţi accepta opţiuni de plugin specifice, pe care utilizatorii le pot specifica în felul următor:
{
plugins: [
["my-plugin", {
"option1": true,
"option2": false
}]
]
}
Aceste opţiuni sunt pasate apoi vizitatorilor plugin-ului prin obiectul state
:
export default function({ types: t }) {
return {
visitor: {
FunctionDeclaration(path, state) {
console.log(state.opts);
// { option1: true, option2: false }
}
}
}
}
Aceste opţiuni sunt specifice plugin-ului şi nu puteţi accesa opţiuni din alte plugin-uri.
Când scrieţi transformări veţi dori adesea să construiți unele noduri pentru a le insera în AST. Aşa cum am menţionat anterior, puteţi face acest lucru folosind metodele constructor (builder) din pachetul babel-types
.
Numele metodei pentru un constructor este pur şi simplu numele tipului de nod pe care doriţi să-l construiți cu excepţia că prima literă trebuie sa fie mică. De exemplu dacă doriți să construiți MemberExpression
ar trebui să utilizaţi t.memberExpression(...)
.
Argumentele acestor constructori sunt stabilite prin definiţia nodului. În momentul de față se lucrează pentru a genera documentaţie uşor de citit pentru definiţii, dar pentru moment toate pot fi găsite aici.
O definiţie de nod arată în felul următor:
defineType("MemberExpression", {
builder: ["object", "property", "computed"],
visitor: ["object", "property"],
aliases: ["Expression", "LVal"],
fields: {
object: {
validate: assertNodeType("Expression")
},
property: {
validate(node, key, val) {
let expectedType = node.computed ? "Expression" : "Identifier";
assertNodeType(expectedType)(node, key, val);
}
},
computed: {
default: false
}
}
});
Aici puteţi vedea toate informaţiile despre acest tip de nod, inclusiv modul de construcție, traversare şi validare.
Uitându-ne la proprietatea builder
, putem vedea 3 argumente care vor fi necesare pentru a apela metoda constructor (t.memberExpression
).
builder: ["object", "property", "computed"],
Reţineţi că, uneori, există mai multe proprietăţi care le puteți particulariza, decât cele conținute în seria constructorului (
builder
). Acest lucru se întâmplă pentru a evita prea multe argumente pe constructor. În aceste cazuri, trebuie să setaţi proprietăţile manual. Un exemplu esteClassMethod
.
Puteţi vedea validarea pentru argumentele constructorului cu obiectul fields
.
fields: {
object: {
validate: assertNodeType("Expression")
},
property: {
validate(node, key, val) {
let expectedType = node.computed ? "Expression" : "Identifier";
assertNodeType(expectedType)(node, key, val);
}
},
computed: {
default: false
}
}
Puteţi vedea că object
trebuie să fie Expression
, property
trebuie să fie Expression
sau Identifier
în funcţie dacă expresia de membru este calculată (computed
) sau nu şi computed
este pur şi simplu un boolean care implicit este false
.
Aşadar putem construi un MemberExpression
în felul următor:
t.memberExpression(
t.identifier('object'),
t.identifier('property')
// `computed` is optional
);
Ceea ce va rezulta în:
object.property
Cu toate acestea, am spus că object
să fie Expression
, așadar de ce Identifier
este valid?
Ei bine, dacă ne uităm la definiţia pentru Identifier
putem vedea că are o proprietate aliases
care declară că este, de asemenea, o expresie.
aliases: ["Expression", "LVal"],
Așadar, din moment ce MemberExpression
este de tip Expression
, l-am putea seta ca un object
pentru alt MemberExpression
:
t.memberExpression(
t.memberExpression(
t.identifier('member'),
t.identifier('expression')
),
t.identifier('property')
)
Ceea ce va rezulta în:
member.expression.property
Este foarte puţin probabil că veți memora vreodată semnăturile metodei constructor pentru fiecare tip de nod. Așadar, ar trebui să vă rezervați ceva timp să înţelegeți cum sunt generate acestea din definiţiile nodului.
Puteţi găsi toate definiţiile aici şi le puteţi vedea documentate aici
Voi lucra la această secţiune în următoarele săptămâni.
Traversarea AST este scumpă, şi este uşor să traversați accidental AST mai mult decât este necesar. Acest lucru ar putea însemna mii daca nu zeci de mii de operaţiuni suplimentare.
Babel optimizează acest lucru cât mai mult posibil, prin îmbinarea vizitatorilor împreună, dacă este posibil, pentru a face totul într-o singură traversare.
Când scrieţi vizitatori, poate fi tentant să apelați path.traverse
în mai multe locuri unde sunt necesare în mod logic.
path.traverse({
Identifier(path) {
// ...
}
});
path.traverse({
BinaryExpression(path) {
// ...
}
});
Cu toate acestea, este mult mai bine să scrieți toate acestea ca un vizitator unic care este rulat doar o singură dată. Altfel veți traversa acelaşi arbore mai multe ori pentru niciun motiv.
path.traverse({
Identifier(path) {
// ...
},
BinaryExpression(path) {
// ...
}
});
De asemenea, poate fi tentant să apelați path.traverse
atunci când căutați un anumit tip de nod.
const visitorOne = {
Identifier(path) {
// ...
}
};
const MyVisitor = {
FunctionDeclaration(path) {
path.get('params').traverse(visitorOne);
}
};
Așadar, în cazul în care căutați ceva specific, este o şansă bună să gasiți nodurile respective printr-o căutare manuală, fără a efectua vreo traversare costisitoare.
const MyVisitor = {
FunctionDeclaration(path) {
path.node.params.forEach(function() {
// ...
});
}
};
Atunci când aveți vizitatori imbricați, ar putea face mai mult sens să-i scrieți imbricat și în codul dumneavoastră.
const MyVisitor = {
FunctionDeclaration(path) {
path.traverse({
Identifier(path) {
// ...
}
});
}
};
Însă acest lucru creează un nou obiect vizitator de fiecare dată când FunctionDeclaration()
este apelată, iar Babel trebuie să o spargă şi să o valideze de fiecare dată. Acest lucru poate fi costisitor, așadar este mai bine să declarați vizitatorul în afară.
const visitorOne = {
Identifier(path) {
// ...
}
};
const MyVisitor = {
FunctionDeclaration(path) {
path.traverse(visitorOne);
}
};
Dacă aveţi nevoie de stare în cadrul vizitatorilor imbricați, astfel:
const MyVisitor = {
FunctionDeclaration(path) {
var exampleState = path.node.params[0].name;
path.traverse({
Identifier(path) {
if (path.node.name === exampleState) {
// ...
}
}
});
}
};
Puteţi să-l pasați ca stare metodei traverse()
şi să aveți acces la ea pe obiectul this
al vizitatorului.
const visitorOne = {
Identifier(path) {
if (path.node.name === this.exampleState) {
// ...
}
}
};
const MyVisitor = {
FunctionDeclaration(path) {
var exampleState = path.node.params[0].name;
path.traverse(visitorOne, { exampleState });
}
};
Uneori când ne gândim la o transformare, am putea uita că structura poate fi imbricată.
De exemplu, imaginaţi-vă că dorim să căutăm constructor
ClassMethod
din Foo
ClassDeclaration
.
class Foo {
constructor() {
// ...
}
}
const constructorVisitor = {
ClassMethod(path) {
if (path.node.name === 'constructor') {
// ...
}
}
}
const MyVisitor = {
ClassDeclaration(path) {
if (path.node.id.name === 'Foo') {
path.traverse(constructorVisitor);
}
}
}
Putem ușor ignora faptul că clasele pot fi imbricate şi folosind traversarea mai sus ne vom lovi de un constructor
imbricat, precum:
class Foo {
constructor() {
class Bar {
constructor() {
// ...
}
}
}
}
Pentru actualizări, urmăriţi-l pe @thejameskyle pe Twitter.