Изучаем TypeScript. Введение в TS Compiler API #1

Привет, на связи Antihype JS. Это вторая статья, посвященная TypeScript Compiler API. Сегодня мы разберемся как написать и подключить ваш первый плагин для компилятора. Если вы случайно нашли эту статью, то рекомендуем прочесть первую часть.

Перед написанием кода самого плагина, давайте рассмотрим основные встроенные в TS методы и объекты, с которыми нам предстоит работать.

visitNode

import ts from 'typescript'; ts.visitNode(sourceFile, visitor);

Посещаем узел AST с помощью переданной функции visitor. У нас есть возможность изменить узел, просто вернув новый объект из visitor. Как правило, трансформация запускается начиная с корневого узла - sourceFile.

visitEachChild

import ts from 'typescript'; ts.visitEachChild(node, visitor, context);

Посещает каждый дочерний узел узла, используя функцию visitor. Под капотом используется все тот же метод visitNode.

context

export interface CoreTransformationContext { readonly factory: NodeFactory /** Gets the compiler options supplied to the transformer. */ getCompilerOptions(): CompilerOptions /** Starts a new lexical environment. */ startLexicalEnvironment(): void /** Suspends the current lexical environment, usually after visiting a parameter list. */ suspendLexicalEnvironment(): void /** Resumes a suspended lexical environment, usually before visiting a function body. */ resumeLexicalEnvironment(): void /** Ends a lexical environment, returning any declarations. */ endLexicalEnvironment(): Statement[] | undefined /** Hoists a function declaration to the containing scope. */ hoistFunctionDeclaration(node: FunctionDeclaration): void /** Hoists a variable declaration to the containing scope. */ hoistVariableDeclaration(node: Identifier): void }

Каждый трансформер получает на вход контекст. Например, контекст можно использовать для получения информации о текущей конфигурации компилятора (compilerOptions).

visitor

const transformer: ts.TransformerFactory<ts.SourceFile> = (context: ts.TransformationContext) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.Node => { console.log(node.getText(sourceFile)) return ts.visitEachChild(node, visitor, context) } return ts.visitNode(sourceFile, visitor) } }

Callback функция, которая используется для посещения каждого узла дерева. В ней будет располагаться вся логика трансформации кода.

context.factory.create*

Методы будут полезны для создания новых узлов AST. Например, этот код:

const test = 'test'

будет представлен в виде следующего набора вызовов для создания дерева:

context.factory.createVariableStatement( undefined, context.factory.createVariableDeclarationList( [ context.factory.createVariableDeclaration( context.factory.createIdentifier('test'), undefined, undefined, context.factory.createStringLiteral('test') ), ], ts.NodeFlags.Const ) )

Также эти методы можно использовать для различного кодгена, например генерации типов из Swagger или GraphQL схемы.

context.factory.update*

По умолчанию все ноды дерева иммутабельные. Для модификации существующих узлов необходимо использовать набор методов context.factory.update*.
Для замены значения строки “test” в уже знакомом нам коде:

const test = 'test'

В функции visitor следует использовать метод updateVariableDeclaration:

if ( ts.isVariableDeclaration(node) && node.initializer && ts.isStringLiteral(node.initializer) && node.initializer.text === 'test' ) { return context.factory.updateVariableDeclaration( node, node.name, node.exclamationToken, node.type, context.factory.createStringLiteral('random text') ) }

Наконец-то пишем плагин!

С базовым инструментарием мы закончили, давайте приступим к написанию основы для нашего первого плагина. Писать будем трансформер before типа, то есть работаем напрямую с AST тайпскрипта.

Как обычно, сначала импортнем TypeScript:

import ts from 'typescript'

И для реализации функции transformer воспользуемся встроенным интерфейсом TransformerFactory:

import ts from 'typescript' export const transformer: ts.TransformerFactory<ts.SourceFile> = (context: ts.TransformationContext) => { return (sourceFile) => { // наша логика трансформации будет здесь } }

Для лучшего понимания логики работы плагина, сначала мы решим задачу вывода нашего AST в консоль:

import ts from 'typescript' export const transformer: ts.TransformerFactory<ts.SourceFile> = (context: ts.TransformationContext) => { return (sourceFile) => { const visit = (depth: number) => (node: ts.Node): ts.Node => { // sourceFile обрабатывать не будем, выведется весь текст из файла if (!ts.isSourceFile(node)) { const nodeType = ts.SyntaxKind[node.kind] const nodeText = node.getText(sourceFile) console.log('-'.repeat(depth), nodeType, ':', nodeText) } // рекурсивно запускаем visit для каждого потомка return ts.visitEachChild(node, visit(depth + 1), context) } // начинаем обход дерева с корня - sourceFile return ts.visitNode(sourceFile, visit(0)) } }

Круто, осталось подключить наш плагин в бойлерплейт из первой статьи:

import { transformer } from './transformer' // ... function build(entryPoint: string): void { // ... program.emit(undefined, undefined, undefined, undefined, { before: [transformer] }) }

Запускаем сборку и смотрим вывод консоли, чтобы убедиться в корректности работы плагина:

- FirstStatement : const test: string = 'test' -- VariableDeclarationList : const test: string = 'test' --- VariableDeclaration : test: string = 'test' ---- Identifier : test ---- StringKeyword : string ---- StringLiteral : 'test' - ExpressionStatement : console.log(test) -- CallExpression : console.log(test) --- PropertyAccessExpression : console.log ---- Identifier : console ---- Identifier : log --- Identifier : test

В левой части мы видим тип узла, а после разделителя выводится текст из исходного кода нашей программы.

Теперь я могу посмотреть AST своего исходного кода. А можно более сложный пример?

Хорошо, тогда решим задачу добавления рандомного числа в конец строки “test”:

import ts from 'typescript' export const transformer: ts.TransformerFactory<ts.SourceFile> = (context: ts.TransformationContext) => { return (sourceFile) => { const visit = (node: ts.Node): ts.Node => { if ( // проверяем что узел это декларация переменной ts.isVariableDeclaration(node) && // проверим что у узла есть начальное значение // и это значение является строкой "test" node.initializer && ts.isStringLiteral(node.initializer) && node.initializer.text === 'test' ) { // вернем измененный узел декларации переменной // при этом сохраняя неизменными все свойства // кроме initializer - начального значения // к нему мы добавим рандомное число через разделитель "_" return context.factory.updateVariableDeclaration( node, node.name, node.exclamationToken, node.type, context.factory.createStringLiteral(`${node.initializer.text}_${Math.random()}`) ) } return ts.visitEachChild(node, visit, context) } return ts.visitNode(sourceFile, visit) } }

С помощью встроенных тайпгардов (ts.isVariableDeclaration в нашем случае) и рекурсивного обхода дерева мы можем точечно выбирать нужные узлы и производить их кастомную обработку. Повторюсь, обязательным условием для корректной модификации является возврат нового узла.

Запускаем сборку и проверяем содержимого выходного файла:

// dist/index.js "use strict"; const test = "test_0.7321963815876473"; console.log(test);

Поздравляю, мы справились с нашей задачей, первый плагин отработал корректно!

На сегодня это все. Остаемся на связи, пишите в комментариях пожелания к следующей части. Пример проекта с исходным кодом можно посмотреть на GitHub.

Также подпишитесь на наш канал, там мы пишем и общаемся про frontend и все что с ним связано.

99
10 комментариев

Все просто и понятно 🔥

1
Ответить

Шикарный автор, жду следующую часть!

1
Ответить

Также эти методы можно использовать для различного кодгена, например генерации типов из Swagger или GraphQL схемы

Теперь ждём мануал вот на это:)

1
Ответить

учли! :)

Ответить

Зачем здесь куча кода? Это же не Хабр

Ответить

Потому что на хабре полно карма-дрочеров и негатива. Автор пиши ещё.

1
Ответить