Привет, на связи Antihype JS. Это вторая статья, посвященная TypeScript Compiler API. Сегодня мы разберемся как написать и подключить ваш первый плагин для компилятора. Если вы случайно нашли эту статью, то рекомендуем прочесть первую часть.Перед написанием кода самого плагина, давайте рассмотрим основные встроенные в TS методы и объекты, с которыми нам предстоит работать.visitNodeimport ts from 'typescript'; ts.visitNode(sourceFile, visitor);Посещаем узел AST с помощью переданной функции visitor. У нас есть возможность изменить узел, просто вернув новый объект из visitor. Как правило, трансформация запускается начиная с корневого узла - sourceFile.visitEachChildimport ts from 'typescript'; ts.visitEachChild(node, visitor, context);Посещает каждый дочерний узел узла, используя функцию visitor. Под капотом используется все тот же метод visitNode.contextexport 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).visitorconst 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 и все что с ним связано.
Все просто и понятно 🔥
Шикарный автор, жду следующую часть!
Также эти методы можно использовать для различного кодгена, например генерации типов из Swagger или GraphQL схемы
Теперь ждём мануал вот на это:)
учли! :)
Зачем здесь куча кода? Это же не Хабр
Потому что на хабре полно карма-дрочеров и негатива. Автор пиши ещё.