Во время обработки исходного кода Parcel может не только анализировать файлы как текст, но и представлять их в виде AST (Abstract Syntax Tree, абстрактного синтаксического дерева). Такой подход позволяет выполнять сложные преобразования кода, сохраняя корректность синтаксиса и структуру программы.
AST представляет исходный код как набор узлов, связанных между собой иерархическими отношениями. Например, инструкция:
const sum = a + b;
может быть представлена в виде дерева:
VariableDeclaration
└── VariableDeclarator
├── Identifier (sum)
└── BinaryEx * pression (+)
├── Identifier (a)
└── Identifier (b)
Плагины Parcel получают возможность:
Работа с AST обычно выполняется внутри Transformer-плагинов.
Типичная последовательность выглядит следующим образом:
Упрощённая схема:
Source Code
↓
Parser
↓
AST
↓
Transformer
↓
Modified AST
↓
Code Generator
↓
Final Source
Базовая структура трансформера выглядит так:
import {Transformer} from '@parcel/plugin';
export default new Transformer({
async transform({asset}) {
return [asset];
}
});
Для работы с AST необходимо получить дерево из объекта Asset.
Обычно используется метод:
const ast = await asset.getAST();
Полный пример:
import {Transformer} from '@parcel/plugin';
export default new Transformer({
async transform({asset}) {
const ast = await asset.getAST();
console.log(ast);
return [asset];
}
});
Если парсер для данного типа файлов уже был выполнен, Parcel вернёт готовое дерево.
Возвращаемый объект содержит метаданные и корневой узел дерева.
Пример:
{
type: 'babel',
version: '7.0.0',
program: {
type: 'Program',
body: [...]
}
}
Основные поля:
| Поле | Назначение |
|---|---|
| type | Тип AST |
| version | Версия формата |
| program | Корневой узел дерева |
Корневой узел чаще всего имеет тип:
Program
Он содержит массив инструкций:
ast.program.body
Например:
Program
├── ImportDeclaration
├── VariableDeclaration
├── FunctionDeclaration
└── ExportDefaultDeclaration
Для JavaScript Parcel обычно использует структуру AST, совместимую с Babel.
Поэтому доступны все привычные типы узлов:
Identifier
Literal
StringLiteral
NumericLiteral
BinaryExpression
CallExpression
MemberExpression
FunctionDeclaration
ArrowFunctionExpression
ImportDeclaration
ExportNamedDeclaration
Пример узла:
{
type: 'Identifier',
name: 'user'
}
После получения AST необходимо пройти по его узлам.
Для этого обычно используется пакет Babel Traverse.
Установка:
npm install @babel/traverse
Импорт:
import traverse from '@babel/traverse';
Пример обхода:
traverse(ast.program, {
Identifier(path) {
console.log(path.node.name);
}
});
Для каждого идентификатора будет выполнен обработчик.
Во время обхода используется объект Path.
Он содержит информацию о текущем узле и его положении внутри дерева.
Пример:
Identifier(path) {
console.log(path.node);
}
Получение текущего узла:
path.node
Получение родителя:
path.parent
Проверка типа:
path.isIdentifier()
Замена:
path.replaceWith(...)
Удаление:
path.remove()
Предположим, необходимо найти все вызовы функции:
log();
AST-обход:
traverse(ast.program, {
CallEx * pression(path) {
const callee = path.node.callee;
if (
callee.type === 'Identifier' &&
callee.name === 'log'
) {
console.log('Найден вызов');
}
}
});
Исходный код:
log();
foo();
log();
Результат:
Найден вызов
Найден вызов
AST позволяет безопасно переименовывать идентификаторы.
Исходный код:
const user = 'Alex';
console.log(user);
Трансформация:
traverse(ast.program, {
Identifier(path) {
if (path.node.name === 'user') {
path.node.name = 'currentUser';
}
}
});
После генерации:
const currentUser = 'Alex';
console.log(currentUser);
Для создания элементов дерева удобно использовать пакет Babel Types.
Установка:
npm install @babel/types
Импорт:
import * as t from '@babel/types';
Создание строкового литерала:
const node = t.stringLiteral('hello');
Создание идентификатора:
const id = t.identifier('username');
Создание вызова функции:
const call = t.callEx * pression(
t.identifier('log'),
[t.stringLiteral('start')]
);
Полученный AST-узел полностью совместим с Babel и Parcel.
Допустим, необходимо автоматически вставить логирование.
Создание узла:
const statement = t.expressionStatement(
t.callEx * pression(
t.memberEx * pression(
t.identifier('console'),
t.identifier('log')
),
[t.stringLiteral('Application started')]
)
);
Добавление в начало программы:
ast.program.body.unshift(statement);
Результат:
console.log('Application started');
const app = createApp();
Исходный код:
debug();
run();
Поиск вызова:
traverse(ast.program, {
CallEx * pression(path) {
if (
path.node.callee.type === 'Identifier' &&
path.node.callee.name === 'debug'
) {
path.remove();
}
}
});
Результат:
run();
Удаление через AST безопаснее текстовых замен, поскольку затрагиваются только реальные конструкции языка.
Исходный код:
oldFunction();
Замена:
traverse(ast.program, {
CallEx * pression(path) {
if (
path.node.callee.name === 'oldFunction'
) {
path.replaceWith(
t.callEx * pression(
t.identifier('newFunction'),
[]
)
);
}
}
});
Результат:
newFunction();
AST позволяет анализировать импорты.
Исходный код:
import React from 'react';
import lodash from 'lodash';
Поиск:
traverse(ast.program, {
ImportDeclaration(path) {
console.log(path.node.source.value);
}
});
Результат:
react
lodash
Создание нового импорта:
const importNode = t.importDeclaration(
[
t.importDefaultSpecifier(
t.identifier('React')
)
],
t.stringLiteral('react')
);
Вставка:
ast.program.body.unshift(importNode);
Результат:
import React from 'react';
...
Поиск экспортируемых сущностей:
traverse(ast.program, {
ExportNamedDeclaration(path) {
console.log(path.node);
}
});
Пример:
export const version = '1.0';
Будет найден узел:
ExportNamedDeclaration
Часто AST используется для автоматизации повторяющихся операций:
Пример удаления всех debug-вызовов:
traverse(ast.program, {
CallEx * pression(path) {
const callee = path.node.callee;
if (
callee.type === 'Identifier' &&
callee.name === 'debug'
) {
path.remove();
}
}
});
После модификации дерево необходимо вернуть обратно в Asset.
Для этого используется:
asset.setAST(ast);
Пример:
export default new Transformer({
async transform({asset}) {
const ast = await asset.getAST();
// модификация дерева
asset.setAST(ast);
return [asset];
}
});
Parcel автоматически выполнит последующую генерацию кода.
Иногда исходное дерево отсутствует и необходимо сформировать его самостоятельно.
Пример:
const ast = {
type: 'babel',
version: '7.0.0',
program: {
type: 'Program',
body: []
}
};
После этого AST можно наполнять узлами:
ast.program.body.push(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('version'),
t.stringLiteral('1.0.0')
)
])
);
Обработка дерева может стать узким местом сборки, особенно в крупных проектах.
Практические рекомендации:
Неэффективный вариант:
traverse(ast.program, {...});
traverse(ast.program, {...});
traverse(ast.program, {...});
Более эффективный подход:
traverse(ast.program, {
Identifier() {},
CallEx * pression() {},
ImportDeclaration() {}
});
import {Transformer} from '@parcel/plugin';
import traverse from '@babel/traverse';
import * as t from '@babel/types';
export default new Transformer({
async transform({asset}) {
const ast = await asset.getAST();
traverse(ast.program, {
CallEx * pression(path) {
const callee = path.node.callee;
if (
callee.type === 'Identifier' &&
callee.name === 'debug'
) {
path.remove();
}
}
});
ast.program.body.unshift(
t.expressionStatement(
t.callEx * pression(
t.memberEx * pression(
t.identifier('console'),
t.identifier('log')
),
[t.stringLiteral('Build processed')]
)
)
);
asset.setAST(ast);
return [asset];
}
});
Такой плагин выполняет две задачи одновременно:
debug().Подобный подход демонстрирует основной принцип работы с AST внутри Parcel: получение дерева, обход узлов, внесение изменений и сохранение результата для последующих этапов сборки.