├── .editorconfig ├── .gitignore ├── .prettierrc ├── README.md ├── example-transformers ├── add-import-declaration │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── build-single.sh ├── build.sh ├── create-unique-name │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── find-parent │ ├── source.ts │ ├── transformed │ │ ├── import.js │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── follow-imports │ ├── import.ts │ ├── source.ts │ ├── transformed │ │ ├── import.js │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── follow-node-modules-imports │ ├── node_modules │ │ └── js-pkg │ │ │ ├── index.js │ │ │ └── package.json │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── hoist-function-declaration │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── hoist-variable-declaration │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── log-every-node │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── match-identifier-by-symbol │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── my-first-transformer │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── pragma-check │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── remove-node │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── replace-node │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── return-multiple-node │ ├── source.ts │ ├── transformed │ │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── tsconfig.json └── update-node │ ├── source.ts │ ├── transformed │ └── source.js │ ├── transformer.ts │ └── tsconfig.json ├── package.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxBracketSameLine": true, 3 | "bracketSpacing": true, 4 | "semi": true, 5 | "tabWidth": 2, 6 | "printWidth": 100, 7 | "singleQuote": true, 8 | "trailingComma": "es5", 9 | "useTabs": false 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Transformer Handbook 2 | 3 | This document covers how to write a [TypeScript](https://typescriptlang.org/) [Transformer](https://basarat.gitbook.io/typescript/overview/ast). 4 | 5 | # Table of contents 6 | 7 | 8 | 9 | - [Introduction](#introduction) 10 | - [Running examples](#running-examples) 11 | - [The basics](#the-basics) 12 | - [What is a abstract syntax tree (AST)](#what-is-a-abstract-syntax-tree-ast) 13 | - [Stages](#stages) 14 | - [A Program according to TypeScript](#a-program-according-to-typescript) 15 | - [Parser](#parser) 16 | - [Scanner](#scanner) 17 | - [Binder](#binder) 18 | - [Transforms](#transforms) 19 | - [Emitting](#emitting) 20 | - [Traversal](#traversal) 21 | - [`visitNode()`](#visitnode) 22 | - [`visitEachChild()`](#visiteachchild) 23 | - [`visitor`](#visitor) 24 | - [`context`](#context) 25 | - [Scopes](#scopes) 26 | - [Bindings](#bindings) 27 | - [Transformer API](#transformer-api) 28 | - [Visiting](#visiting) 29 | - [Nodes](#nodes) 30 | - [`context`](#context-1) 31 | - [`program`](#program) 32 | - [`typeChecker`](#typechecker) 33 | - [Writing your first transformer](#writing-your-first-transformer) 34 | - [Types of transformers](#types-of-transformers) 35 | - [Factory](#factory) 36 | - [Config](#config) 37 | - [Program](#program) 38 | - [Consuming transformers](#consuming-transformers) 39 | - [`ttypescript`](#ttypescript) 40 | - [`webpack`](#webpack) 41 | - [`parcel`](#parcel) 42 | - [Transformation operations](#transformation-operations) 43 | - [Visiting](#visiting-1) 44 | - [Checking a node is a certain type](#checking-a-node-is-a-certain-type) 45 | - [Check if two identifiers refer to the same symbol](#check-if-two-identifiers-refer-to-the-same-symbol) 46 | - [Find a specific parent](#find-a-specific-parent) 47 | - [Stopping traversal](#stopping-traversal) 48 | - [Manipulation](#manipulation) 49 | - [Updating a node](#updating-a-node) 50 | - [Replacing a node](#replacing-a-node) 51 | - [Replacing a node with multiple nodes](#replacing-a-node-with-multiple-nodes) 52 | - [Inserting a sibling node](#inserting-a-sibling-node) 53 | - [Removing a node](#removing-a-node) 54 | - [Adding new import declarations](#adding-new-import-declarations) 55 | - [Scope](#scope) 56 | - [Pushing a variable declaration to the top of its scope](#pushing-a-variable-declaration-to-the-top-of-its-scope) 57 | - [Pushing a variable declaration to a parent scope](#pushing-a-variable-declaration-to-a-parent-scope) 58 | - [Checking if a local variable is referenced](#checking-if-a-local-variable-is-referenced) 59 | - [Defining a unique variable](#defining-a-unique-variable) 60 | - [Rename a binding and its references](#rename-a-binding-and-its-references) 61 | - [Finding](#finding) 62 | - [Get line number and column](#get-line-number-and-column) 63 | - [Advanced](#advanced) 64 | - [Evaluating expressions](#evaluating-expressions) 65 | - [Following module imports](#following-module-imports) 66 | - [Following node module imports](#following-node-module-imports) 67 | - [Transforming jsx](#transforming-jsx) 68 | - [Determining the file pragma](#determining-the-file-pragma) 69 | - [Resetting the file pragma](#resetting-the-file-pragma) 70 | - [Tips & tricks](#tips--tricks) 71 | - [Composing transformers](#composing-transformers) 72 | - [Throwing a syntax error to ease the developer experience](#throwing-a-syntax-error-to-ease-the-developer-experience) 73 | - [Testing](#testing) 74 | - [`ts-transformer-testing-library`](#ts-transformer-testing-library) 75 | - [Known bugs](#known-bugs) 76 | - [EmitResolver cannot handle `JsxOpeningLikeElement` and `JsxOpeningFragment` that didn't originate from the parse tree](#emitresolver-cannot-handle-jsxopeninglikeelement-and-jsxopeningfragment-that-didnt-originate-from-the-parse-tree) 77 | - [`getMutableClone(node)` blows up when used with `ts-loader`](#getmutableclonenode-blows-up-when-used-with-ts-loader) 78 | 79 | 80 | 81 | # Introduction 82 | 83 | TypeScript is a typed superset of Javascript that compiles to plain Javascript. 84 | TypeScript supports the ability for consumers to _transform_ code from one form to another, 85 | similar to how [Babel](https://babeljs.io/) does it with _plugins_. 86 | 87 | > Follow me [@itsmadou](https://twitter.com/itsmadou) for updates and general discourse 88 | 89 | ## Running examples 90 | 91 | There are multiple examples ready for you to use through this handbook. 92 | When you want to take the dive make sure to: 93 | 94 | 1. clone the repo 95 | 2. install deps with `yarn` 96 | 3. build the example you want `yarn build example_name` 97 | 98 | # The basics 99 | 100 | A transformer when boiled down is essentially a function that takes and returns some piece of code, 101 | for example: 102 | 103 | ```js 104 | const Transformer = code => code; 105 | ``` 106 | 107 | The difference though is that instead of `code` being of type `string` - 108 | it is actually in the form of an abstract syntax tree (AST), 109 | described below. 110 | With it we can do powerful things like updating, 111 | replacing, 112 | adding, 113 | & deleting `node`s. 114 | 115 | ## What is a abstract syntax tree (AST) 116 | 117 | Abstract Syntax Trees, 118 | or ASTs, 119 | are a data structure that describes the code that has been parsed. 120 | When working with ASTs in TypeScript I'd strongly recommend using an AST explorer - 121 | such as [ts-ast-viewer.com](https://ts-ast-viewer.com). 122 | 123 | Using such a tool we can see that the following code: 124 | 125 | ```js 126 | function hello() { 127 | console.log('world'); 128 | } 129 | ``` 130 | 131 | In its AST representation looks like this: 132 | 133 | ``` 134 | -> SourceFile 135 | -> FunctionDeclaration 136 | - Identifier 137 | -> Block 138 | -> ExpressionStatement 139 | -> CallExpression 140 | -> PropertyAccessExpression 141 | - Identifier 142 | - Identifier 143 | - StringLiteral 144 | - EndOfFileToken 145 | ``` 146 | 147 | For a more detailed look check out the [AST yourself](https://ts-ast-viewer.com/#code/GYVwdgxgLglg9mABACwKYBt10QCgJSIDeAUImYhAgM5zqoB0WA5jgOQDucATugCat4A3MQC+QA)! 148 | You can also see the code can be used to generate the same AST in the bottom left panel, 149 | and the selected node metadata in the right panel. 150 | Super useful! 151 | 152 | When looking at the metadata you'll notice they all have a similar structure (some properties have been omitted): 153 | 154 | ```js 155 | { 156 | kind: 307, // (SyntaxKind.SourceFile) 157 | pos: 0, 158 | end: 47, 159 | statements: [{...}], 160 | } 161 | ``` 162 | 163 | ```js 164 | { 165 | kind: 262, // (SyntaxKind.FunctionDeclaration) 166 | pos: 0, 167 | end: 47, 168 | name: {...}, 169 | body: {...}, 170 | } 171 | ``` 172 | 173 | ```js 174 | { 175 | kind: 244, // (SyntaxKind.ExpressionStatement) 176 | pos: 19, 177 | end: 45, 178 | expression: {...} 179 | } 180 | ``` 181 | 182 | > `SyntaxKind` is a TypeScript enum which describes the kind of node. 183 | > For [more information have a read of Basarat's AST tip](https://basarat.gitbook.io/typescript/overview/ast/ast-tip-syntaxkind). 184 | 185 | And so on. 186 | Each of these describe a `Node`. 187 | ASTs can be made from one to many - 188 | and together they describe the syntax of a program that can be used for static analysis. 189 | 190 | Every node has a `kind` property which describes what kind of node it is, 191 | as well as `pos` and `end` which describe where in the source they are. 192 | We will talk about how to narrow the node to a specific type of node later in the handbook. 193 | 194 | ## Stages 195 | 196 | Very similar to Babel - 197 | TypeScript however has five stages, 198 | **parser**, 199 | _binder_, 200 | _checker_, 201 | **transform**, 202 | **emitting**. 203 | 204 | Two steps are exclusive to TypeScript, 205 | _binder_ and _checker_. 206 | We are going to gloss over _checker_ as it relates to TypeScripts type checking specifics. 207 | 208 | > For a more in-depth understanding of the TypeScript compiler internals have a read of [Basarat's handbook](https://basarat.gitbook.io/typescript/). 209 | 210 | ### A Program according to TypeScript 211 | 212 | Before we continue we need to quickly clarify exactly _what_ a `Program` is according to TypeScript. 213 | A `Program` is a collection of one or more entrypoint source files which consume one or more modules. 214 | The _entire_ collection is then used during each of the stages. 215 | 216 | This is in contrast to how Babel processes files - 217 | where Babel does file in file out, 218 | TypeScript does _project_ in, 219 | project out. 220 | This is why enums don't work when parsing TypeScript with Babel for example, 221 | it just doesn't have all the information available. 222 | 223 | ### Parser 224 | 225 | The TypeScript parser actually has two parts, 226 | the `scanner`, 227 | and then the `parser`. 228 | This step will convert source code into an AST. 229 | 230 | ``` 231 | SourceCode ~~ scanner ~~> Token Stream ~~ parser ~~> AST 232 | ``` 233 | 234 | The parser takes source code and tries to convert it into an in-memory AST representation which you can work with in the compiler. Also: see [Parser](https://basarat.gitbooks.io/typescript/content/docs/compiler/parser.html). 235 | 236 | ### Scanner 237 | 238 | The scanner is used by the parser to convert a string into tokens in a linear fashion, 239 | then it's up to a parser to tree-ify them. 240 | Also: see [Scanner](https://basarat.gitbooks.io/typescript/docs/compiler/scanner.html). 241 | 242 | ### Binder 243 | 244 | Creates a symbol map and uses the AST to provide the type system which is important to link references and to be able to know the nodes of imports and exports. 245 | Also: see [Binder](https://basarat.gitbooks.io/typescript/docs/compiler/binder.html). 246 | 247 | ### Transforms 248 | 249 | This is the step we're all here for. 250 | It allows us, 251 | the developer, 252 | to change the code in any way we see fit. 253 | Performance optimizations, 254 | compile time behavior, 255 | really anything we can imagine. 256 | 257 | There are three stages of `transform` we care about: 258 | 259 | - `before` - which run transformers before the TypeScript ones (code has not been compiled) 260 | - `after` - which run transformers _after_ the TypeScript ones (code has been compiled) 261 | - `afterDeclarations` - which run transformers _after_ the **declaration** step (you can transform type defs here) 262 | 263 | Generally the 90% case will see us always writing transformers for the `before` stage, 264 | but if you need to do some post-compilation transformation, 265 | or modify types, 266 | you'll end up wanting to use `after` and `afterDeclarations`. 267 | 268 | > **Tip** - Type checking _should_ not happen after transforming. 269 | > If it does it's more than likely a bug - 270 | > file an issue! 271 | 272 | ### Emitting 273 | 274 | This stage happens last and is responsible for _emitting_ the final code somewhere. 275 | Generally this is usually to the file system - 276 | but it could also be in memory. 277 | 278 | ## Traversal 279 | 280 | When wanting to modify the AST in any way you need to traverse the tree - 281 | recursively. 282 | In more concrete terms we want to _visit each node_, 283 | and then return either the same, 284 | an updated, 285 | or a completely new node. 286 | 287 | If we take the previous example AST in JSON format (with some values omitted): 288 | 289 | ```js 290 | { 291 | kind: 307, // (SyntaxKind.SourceFile) 292 | statements: [{ 293 | kind: 262, // (SyntaxKind.FunctionDeclaration) 294 | name: { 295 | kind: 80 // (SyntaxKind.Identifier) 296 | escapedText: "hello" 297 | }, 298 | body: { 299 | kind: 241, // (SyntaxKind.Block) 300 | statements: [{ 301 | kind: 244, // (SyntaxKind.ExpressionStatement) 302 | expression: { 303 | kind: 213, // (SyntaxKind.CallExpression) 304 | expression: { 305 | kind: 211, // (SyntaxKind.PropertyAccessExpression) 306 | name: { 307 | kind: 80 // (SyntaxKind.Identifier) 308 | escapedText: "log", 309 | }, 310 | expression: { 311 | kind: 80, // (SyntaxKind.Identifier) 312 | escapedText: "console", 313 | } 314 | } 315 | }, 316 | arguments: [{ 317 | kind: 11, // (SyntaxKind.StringLiteral) 318 | text: "world", 319 | }] 320 | }] 321 | } 322 | }] 323 | } 324 | ``` 325 | 326 | If we were to traverse it we would start at the `SourceFile` and then work through each node. 327 | You might think you could meticulously traverse it yourself, 328 | like `source.statements[0].name` etc, 329 | but you'll find it won't scale and is prone to breaking very easily - 330 | so use it wisely. 331 | 332 | Ideally for the 90% case you'll want to use the built in methods to traverse the AST. 333 | TypeScript gives us two primary methods for doing this: 334 | 335 | ### `visitNode()` 336 | 337 | Generally you'll only pass this the initial `SourceFile` node. 338 | We'll go into what the `visitor` function is soon. 339 | 340 | ```ts 341 | import * as ts from 'typescript'; 342 | 343 | ts.visitNode(sourceFile, visitor, test); 344 | ``` 345 | 346 | ### `visitEachChild()` 347 | 348 | This is a special function that uses `visitNode` internally. 349 | It will handle traversing down to the inner most node - 350 | and it knows how to do it without you having the think about it. 351 | We'll go into what the `context` object is soon. 352 | 353 | ```ts 354 | import * as ts from 'typescript'; 355 | 356 | ts.visitEachChild(node, visitor, context); 357 | ``` 358 | 359 | ### `visitor` 360 | 361 | The [`visitor` pattern](https://en.wikipedia.org/wiki/Visitor_pattern) is something you'll be using in every Transformer you write, 362 | luckily for us TypeScript handles it so we need to only supply a callback function. 363 | The simplest function we could write might look something like this: 364 | 365 | ```ts 366 | import * as ts from 'typescript'; 367 | 368 | const transformer = sourceFile => { 369 | const visitor = (node: ts.Node): ts.Node => { 370 | console.log(node.kind, `\t# ts.SyntaxKind.${ts.SyntaxKind[node.kind]}`); 371 | return ts.visitEachChild(node, visitor, context); 372 | }; 373 | 374 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 375 | }; 376 | ``` 377 | 378 | > **Note** - You'll see that we're _returning_ each node. 379 | > This is required! 380 | > If we didn't you'd see some funky errors. 381 | 382 | If we applied this to the code example used before we would see this logged in our console (comments added afterwords): 383 | 384 | ```sh 385 | 307 # ts.SyntaxKind.SourceFile 386 | 262 # ts.SyntaxKind.FunctionDeclaration 387 | 80 # ts.SyntaxKind.Identifier 388 | 241 # ts.SyntaxKind.Block 389 | 244 # ts.SyntaxKind.ExpressionStatement 390 | 213 # ts.SyntaxKind.CallExpression 391 | 211 # ts.SyntaxKind.PropertyAccessExpression 392 | 80 # ts.SyntaxKind.Identifier 393 | 80 # ts.SyntaxKind.Identifier 394 | 11 # ts.SyntaxKind.StringLiteral 395 | ``` 396 | 397 | > **Tip** - You can see the source for this at [/example-transformers/log-every-node](/example-transformers/log-every-node) - if wanting to run locally you can run it via `yarn build log-every-node`. 398 | 399 | It goes as deep as possible entering each node, 400 | exiting when it bottoms out, 401 | and then entering other child nodes that it comes to. 402 | 403 | ### `context` 404 | 405 | Every transformer will receive the transformation `context`. 406 | This context is used both for `visitEachChild`, 407 | as well as doing some useful things like getting a hold of what the current TypeScript configuration is. 408 | We'll see our first look at a simple TypeScript transformer soon. 409 | 410 | ## Scopes 411 | 412 | > Most of this content is taken directly from the [Babel Handbook](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#scopes) as the same principles apply. 413 | 414 | Next let's introduce the concept of a [scope](). 415 | Javascript has lexical scoping ([closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures)), 416 | which is a tree structure where blocks create new scope. 417 | 418 | ```js 419 | // global scope 420 | 421 | function scopeOne() { 422 | // scope 1 423 | 424 | function scopeTwo() { 425 | // scope 2 426 | } 427 | } 428 | ``` 429 | 430 | Whenever you create a reference in Javascript, 431 | whether that be by a variable, 432 | function, 433 | class, 434 | param, 435 | import, 436 | label, 437 | etc., 438 | it belongs to the current scope. 439 | 440 | ```js 441 | var global = 'I am in the global scope'; 442 | 443 | function scopeOne() { 444 | var one = 'I am in the scope created by `scopeOne()`'; 445 | 446 | function scopeTwo() { 447 | var two = 'I am in the scope created by `scopeTwo()`'; 448 | } 449 | } 450 | ``` 451 | 452 | Code within a deeper scope may use a reference from a higher scope. 453 | 454 | ```js 455 | function scopeOne() { 456 | var one = 'I am in the scope created by `scopeOne()`'; 457 | 458 | function scopeTwo() { 459 | one = 'I am updating the reference in `scopeOne` inside `scopeTwo`'; 460 | } 461 | } 462 | ``` 463 | 464 | A lower scope might also create a reference of the same name without modifying it. 465 | 466 | ```js 467 | function scopeOne() { 468 | var one = 'I am in the scope created by `scopeOne()`'; 469 | 470 | function scopeTwo() { 471 | var one = 'I am creating a new `one` but leaving reference in `scopeOne()` alone.'; 472 | } 473 | } 474 | ``` 475 | 476 | When writing a transform we want to be wary of scope. 477 | We need to make sure we don't break existing code while modifying different parts of it. 478 | 479 | We may want to add new references and make sure they don't collide with existing ones. 480 | Or maybe we just want to find where a variable is referenced. 481 | We want to be able to track these references within a given scope. 482 | 483 | ### Bindings 484 | 485 | References all belong to a particular scope; 486 | this relationship is known as a binding. 487 | 488 | ```js 489 | function scopeOnce() { 490 | var ref = 'This is a binding'; 491 | 492 | ref; // This is a reference to a binding 493 | 494 | function scopeTwo() { 495 | ref; // This is a reference to a binding from a lower scope 496 | } 497 | } 498 | ``` 499 | 500 | # Transformer API 501 | 502 | When writing your transformer you'll want to write it using TypeScript. 503 | You'll be using the [`typescript`](https://www.npmjs.com/package/typescript) package to do most of the heavy lifting. 504 | It is used for everything, 505 | unlike Babel which has separate small packages. 506 | 507 | First, 508 | let's install it. 509 | 510 | ```sh 511 | npm i typescript --save 512 | ``` 513 | 514 | And then let's import it: 515 | 516 | ```ts 517 | import * as ts from 'typescript'; 518 | ``` 519 | 520 | > **Tip** - I _strongly recommend_ using intellisense in VSCode to interrogate the API, 521 | > it's super useful! 522 | 523 | ## Visiting 524 | 525 | These methods are useful for visiting nodes - 526 | we've briefly gone over a few of them above. 527 | 528 | - `ts.visitNode(node, visitor, test)` - useful for visiting the root node, generally the `SourceFile` 529 | - `ts.visitEachChild(node, visitor, context)` - useful for visiting each child of a node 530 | - `ts.isXyz(node)` - useful for narrowing the type of a `node`, an example of this is `ts.isVariableDeclaration(node)` 531 | 532 | ## Nodes 533 | 534 | These methods are useful for modifying a `node` in some form. 535 | 536 | - `ts.factory.createXyz(...)` - useful for creating a new node (to then return), an example of this is `ts.factory.createIdentifier('world')` 537 | - `ts.factory.updateXyz(node, ...)` - useful for updating a node (to then return), an example of this is `ts.factory.updateVariableDeclaration()` 538 | - `ts.factory.updateSourceFile(sourceFile, ...)` - useful for updating a source file to then return 539 | - `ts.setOriginalNode(newNode, originalNode)` - useful for setting a nodes original node 540 | - `ts.setXyz(...)` - sets things 541 | - `ts.addXyz(...)` - adds things 542 | 543 | ## `context` 544 | 545 | Covered above, 546 | this is supplied to every transformer and has some handy methods available (this is not an exhaustive list, 547 | just the stuff we care about): 548 | 549 | - `getCompilerOptions()` - Gets the compiler options supplied to the transformer 550 | - `hoistFunctionDeclaration(node)` - Hoists a function declaration to the top of the containing scope 551 | - `hoistVariableDeclaration(node)` - Hoists a variable declaration to the tope of the containing scope 552 | 553 | ## `program` 554 | 555 | This is a special property that is available when writing a Program transformer. 556 | We will cover this kind of transformer in [Types of transformers](#types-of-transformers). 557 | It contains metadata about the _entire program_, 558 | such as (this is not an exhaustive list, 559 | just the stuff we care about): 560 | 561 | - `getRootFileNames()` - get an array of file names in the project 562 | - `getSourceFiles()` - gets all `SourceFile`s in the project 563 | - `getCompilerOptions()` - compiler options from the `tsconfig.json`, command line, or other (can also get it from `context`) 564 | - `getSourceFile(fileName: string)` - gets a `SourceFile` using its `fileName` 565 | - `getSourceFileByPath(path: Path)` - gets a `SourceFile` using its `path` 566 | - `getCurrentDirectory()` - gets the current directory string 567 | - `getTypeChecker()` - gets ahold of the type checker, useful when doing things with [Symbols](https://basarat.gitbooks.io/typescript/content/docs/compiler/binder.html) 568 | 569 | ## `typeChecker` 570 | 571 | This is the result of calling `program.getTypeChecker()`. 572 | It has a lot of interesting things on in that we'll be interested in when writing transformers. 573 | 574 | - `getSymbolAtLocation(node)` - useful for getting the symbol of a node 575 | - `getExportsOfModule(symbol)` - will return the exports of a module symbol 576 | 577 | # Writing your first transformer 578 | 579 | It's the part we've all be waiting for! 580 | Let's write out first transformer. 581 | 582 | First let's import `typescript`. 583 | 584 | ```ts 585 | import * as ts from 'typescript'; 586 | ``` 587 | 588 | It's going to contain everything that we could use when writing a transformer. 589 | 590 | Next let's create a default export that is going to be our transformer, 591 | our initial transformer we be a transformer factory (because this gives us access to `context`) - 592 | we'll go into the other kinds of transformers later. 593 | 594 | ```ts 595 | import * as ts from 'typescript'; 596 | 597 | const transformer: ts.TransformerFactory = context => { 598 | return sourceFile => { 599 | // transformation code here 600 | }; 601 | }; 602 | 603 | export default transformer; 604 | ``` 605 | 606 | Because we're using TypeScript to write out transformer - 607 | we get all the type safety and more importantly intellisense! 608 | If you're up to here you'll notice TypeScript complaining that we aren't returning a `SourceFile` - 609 | let's fix that. 610 | 611 | ```diff 612 | import * as ts from "typescript"; 613 | 614 | const transformer: ts.TransformerFactory = context => { 615 | return sourceFile => { 616 | // transformation code here 617 | + return sourceFile; 618 | }; 619 | }; 620 | 621 | export default transformer; 622 | ``` 623 | 624 | Sweet we fixed the type error! 625 | 626 | For our first transformer we'll take a hint from the [Babel Handbook](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#writing-your-first-babel-plugin) and rename some identifiers. 627 | 628 | Here's our source code: 629 | 630 | ```ts 631 | babel === plugins; 632 | ``` 633 | 634 | Let's write a visitor function, 635 | remember that a visitor function should take a `node` of a particular type (here a `SourceFile`), 636 | and then return a `node` of the same type. Note that the `test` parameter of `visitNode` can be used 637 | to ensure that nodes of a particular type are returned. 638 | 639 | ```diff 640 | import * as ts from 'typescript'; 641 | 642 | const transformer: ts.TransformerFactory = context => { 643 | return sourceFile => { 644 | + const visitor = (node: ts.Node): ts.Node => { 645 | + return node; 646 | + }; 647 | + 648 | + return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 649 | - 650 | - return sourceFile; 651 | }; 652 | }; 653 | 654 | export default transformer; 655 | ``` 656 | 657 | Okay that will visit the `SourceFile`... 658 | and then just immediately return it. 659 | That's a bit useless - 660 | let's make sure we visit every node! 661 | 662 | ```diff 663 | import * as ts from 'typescript'; 664 | 665 | const transformer: ts.TransformerFactory = context => { 666 | return sourceFile => { 667 | const visitor = (node: ts.Node): ts.Node => { 668 | - return node; 669 | + return ts.visitEachChild(node, visitor, context); 670 | }; 671 | 672 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 673 | }; 674 | }; 675 | 676 | export default transformer; 677 | ``` 678 | 679 | Now let's find identifiers so we can rename them: 680 | 681 | ```diff 682 | import * as ts from 'typescript'; 683 | 684 | const transformer: ts.TransformerFactory = context => { 685 | return sourceFile => { 686 | const visitor = (node: ts.Node): ts.Node => { 687 | + if (ts.isIdentifier(node)) { 688 | + // transform here 689 | + } 690 | 691 | return ts.visitEachChild(node, visitor, context); 692 | }; 693 | 694 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 695 | }; 696 | }; 697 | 698 | export default transformer; 699 | ``` 700 | 701 | And then let's target the specific identifiers we're interested in: 702 | 703 | ```diff 704 | import * as ts from 'typescript'; 705 | 706 | const transformer: ts.TransformerFactory = context => { 707 | return sourceFile => { 708 | const visitor = (node: ts.Node): ts.Node => { 709 | if (ts.isIdentifier(node)) { 710 | + switch (node.escapedText) { 711 | + case 'babel': 712 | + // rename babel 713 | + 714 | + case 'plugins': 715 | + // rename plugins 716 | + } 717 | } 718 | 719 | return ts.visitEachChild(node, visitor, context); 720 | }; 721 | 722 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 723 | }; 724 | }; 725 | 726 | export default transformer; 727 | ``` 728 | 729 | And then let's return new nodes that have been renamed! 730 | 731 | ```diff 732 | import * as ts from 'typescript'; 733 | 734 | const transformer: ts.TransformerFactory = context => { 735 | return sourceFile => { 736 | const visitor = (node: ts.Node): ts.Node => { 737 | if (ts.isIdentifier(node)) { 738 | switch (node.escapedText) { 739 | case 'babel': 740 | + return ts.factory.createIdentifier('typescript'); 741 | 742 | case 'plugins': 743 | + return ts.factory.createIdentifier('transformers'); 744 | } 745 | } 746 | 747 | return ts.visitEachChild(node, visitor, context); 748 | }; 749 | 750 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 751 | }; 752 | }; 753 | 754 | export default transformer; 755 | ``` 756 | 757 | Sweet! 758 | When ran over our source code we get this output: 759 | 760 | ```ts 761 | typescript === transformers; 762 | ``` 763 | 764 | > **Tip** - You can see the source for this at [/example-transformers/my-first-transformer](/example-transformers/my-first-transformer) - if wanting to run locally you can run it via `yarn build my-first-transformer`. 765 | 766 | # Types of transformers 767 | 768 | All transformers end up returning the `TransformerFactory` type signature. 769 | These types of transformers are taken from [`ttypescript`](https://github.com/cevek/ttypescript). 770 | 771 | ## Factory 772 | 773 | Also known as `raw`, 774 | this is the same as the one used in writing your first transformer. 775 | 776 | ```ts 777 | // ts.TransformerFactory 778 | (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile; 779 | ``` 780 | 781 | ## Config 782 | 783 | When your transformer needs config that can be controlled by consumers. 784 | 785 | ```ts 786 | (config?: YourPluginConfigInterface) => ts.TransformerFactory; 787 | ``` 788 | 789 | ## Program 790 | 791 | When needing access to the `program` object this is the signature you should use, 792 | it should return a `TransformerFactory`. 793 | It also has configuration available as the second object, 794 | supplied by consumers. 795 | 796 | ```ts 797 | (program: ts.Program, config?: YourPluginConfigInterface) => ts.TransformerFactory; 798 | ``` 799 | 800 | # Consuming transformers 801 | 802 | Amusingly TypeScript has no official support for consuming transformers via `tsconfig.json`. 803 | There is a [GitHub issue](https://github.com/microsoft/TypeScript/issues/14419) dedicated to talking about introducing something for it. 804 | Regardless you can consume transformers it's just a little round-about. 805 | 806 | ## [`ts-patch`](https://github.com/nonara/ts-patch) 807 | 808 | > **This is the recommended approach**! 809 | > Hopefully in the future this can be officially supported in `typescript`. 810 | 811 | Essentially a wrapper over the top of the `tsc` CLI - 812 | this gives first class support to transformers via the `tsconfig.json`. 813 | It has `typescript` listed as a peer dependency so the theory is it isn't too brittle. 814 | 815 | Install: 816 | 817 | ```sh 818 | npm i ts-patch -D 819 | ``` 820 | 821 | Add your transformer into the compiler options: 822 | 823 | ```json 824 | { 825 | "compilerOptions": { 826 | "plugins": [{ "transform": "my-first-transformer" }] 827 | } 828 | } 829 | ``` 830 | 831 | Run `tspc`: 832 | 833 | ```sh 834 | tspc 835 | ``` 836 | 837 | `ts-patch` supports `tsc` CLI, 838 | Webpack, 839 | Rollup, 840 | Jest, 841 | & VSCode. 842 | Everything we would want to use TBH. 843 | 844 | # Transformation operations 845 | 846 | ## Visiting 847 | 848 | ### Checking a node is a certain type 849 | 850 | There is a wide variety of helper methods that can assert what type a node is. 851 | When they return true they will _narrow_ the type of the `node`, 852 | potentially giving you extra properties & methods based on the type. 853 | 854 | > **Tip** - Abuse intellisense to interrogate the `ts` import for methods you can use, 855 | > as well as [TypeScript AST Viewer](https://ts-ast-viewer.com/) to know what type a node is. 856 | 857 | ```ts 858 | import * as ts from 'typescript'; 859 | 860 | const visitor = (node: ts.Node): ts.Node => { 861 | if (ts.isJsxAttribute(node.parent)) { 862 | // node.parent is a jsx attribute 863 | // ... 864 | } 865 | }; 866 | ``` 867 | 868 | ### Check if two identifiers refer to the same symbol 869 | 870 | Identifiers are created by the parser and are always unique. 871 | Say, if you create a variable `foo` and use it in another line, it will create 2 separate identifiers with the same text `foo`. 872 | 873 | Then, the linker runs through these identifiers and connects the identifiers referring to the same variable with a common symbol (while considering scope and shadowing). Think of symbols as what we intuitively think as variables. 874 | 875 | So, to check if two identifiers refer to the same symbol - just get the symbols related to the identifier and check if they are the same (by reference). 876 | 877 | **Short example** - 878 | 879 | ```ts 880 | const symbol1 = typeChecker.getSymbolAtLocation(node1); 881 | const symbol2 = typeChecker.getSymbolAtLocation(node2); 882 | 883 | symbol1 === symbol2; // check by reference 884 | ``` 885 | 886 | **Full example** - 887 | 888 | This will log all repeating symbols. 889 | 890 | ```ts 891 | import * as ts from 'typescript'; 892 | 893 | const transformerProgram = (program: ts.Program) => { 894 | const typeChecker = program.getTypeChecker(); 895 | 896 | // Create array of found symbols 897 | const foundSymbols = new Array(); 898 | 899 | const transformerFactory: ts.TransformerFactory = context => { 900 | return sourceFile => { 901 | const visitor = (node: ts.Node): ts.Node => { 902 | if (ts.isIdentifier(node)) { 903 | const relatedSymbol = typeChecker.getSymbolAtLocation(node); 904 | 905 | // Check if array already contains same symbol - check by reference 906 | if (foundSymbols.includes(relatedSymbol)) { 907 | const foundIndex = foundSymbols.indexOf(relatedSymbol); 908 | console.log( 909 | `Found existing symbol at position = ${foundIndex} and name = "${relatedSymbol.name}"` 910 | ); 911 | } else { 912 | // If not found, Add it to array 913 | foundSymbols.push(relatedSymbol); 914 | 915 | console.log( 916 | `Found new symbol with name = "${ 917 | relatedSymbol.name 918 | }". Added at position = ${foundSymbols.length - 1}` 919 | ); 920 | } 921 | 922 | return node; 923 | } 924 | 925 | return ts.visitEachChild(node, visitor, context); 926 | }; 927 | 928 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 929 | }; 930 | }; 931 | 932 | return transformerFactory; 933 | }; 934 | 935 | export default transformerProgram; 936 | ``` 937 | 938 | > **Tip** - You can see the source for this at [/example-transformers/match-identifier-by-symbol](/example-transformers/match-identifier-by-symbol) - if wanting to run locally you can run it via `yarn build match-identifier-by-symbol`. 939 | 940 | ### Find a specific parent 941 | 942 | While there doesn't exist an out of the box method you can basically roll your own. 943 | Given a node: 944 | 945 | ```ts 946 | const findParent = (node: ts.Node, predicate: (node: ts.Node) => boolean) => { 947 | if (!node.parent) { 948 | return undefined; 949 | } 950 | 951 | if (predicate(node.parent)) { 952 | return node.parent; 953 | } 954 | 955 | return findParent(node.parent, predicate); 956 | }; 957 | 958 | const visitor = (node: ts.Node): ts.Node => { 959 | if (ts.isStringLiteral(node)) { 960 | const parent = findParent(node, ts.isFunctionDeclaration); 961 | if (parent) { 962 | console.log('string literal has a function declaration parent'); 963 | } 964 | return node; 965 | } 966 | }; 967 | ``` 968 | 969 | Will log to console `string literal has a function declaration parent` with the following source: 970 | 971 | ```ts 972 | function hello() { 973 | if (true) { 974 | 'world'; 975 | } 976 | } 977 | ``` 978 | 979 | - Be careful when traversing after replacing a node with another - `parent` may not be set. 980 | If you need to traverse after transforming make sure to set `parent` on the node yourself. 981 | 982 | > **Tip** - You can see the source for this at [/example-transformers/find-parent](/example-transformers/find-parent) - if wanting to run locally you can run it via `yarn build find-parent`. 983 | 984 | ### Stopping traversal 985 | 986 | In the visitor function you can return early instead of continuing down children, 987 | so for example if we hit a node and we know we don't need to go any further: 988 | 989 | ```ts 990 | const visitor = (node: ts.Node): ts.Node => { 991 | if (ts.isArrowFunction(node)) { 992 | // return early 993 | return node; 994 | } 995 | }; 996 | ``` 997 | 998 | ## Manipulation 999 | 1000 | ### Updating a node 1001 | 1002 | ```ts 1003 | if (ts.isVariableDeclaration(node)) { 1004 | return ts.updateVariableDeclaration( 1005 | node, 1006 | node.name, 1007 | undefined, 1008 | node.type, 1009 | ts.createStringLiteral('world') 1010 | ); 1011 | } 1012 | ``` 1013 | 1014 | ```diff 1015 | -const hello = true; 1016 | +const hello = "updated-world"; 1017 | ``` 1018 | 1019 | > **Tip** - You can see the source for this at [/example-transformers/update-node](/example-transformers/update-node) - if wanting to run locally you can run it via `yarn build update-node`. 1020 | 1021 | ### Replacing a node 1022 | 1023 | Maybe instead of updating a node we want to completely change it. 1024 | We can do that by just returning... a completely new node! 1025 | 1026 | ```ts 1027 | if (ts.isFunctionDeclaration(node)) { 1028 | // Will replace any function it finds with an arrow function. 1029 | return ts.factory.createVariableDeclarationList( 1030 | [ 1031 | ts.factory.createVariableDeclaration( 1032 | ts.factory.createIdentifier(node.name.escapedText), 1033 | undefined, 1034 | ts.factory.createArrowFunction( 1035 | undefined, 1036 | undefined, 1037 | [], 1038 | undefined, 1039 | ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 1040 | ts.factory.createBlock([], false) 1041 | ) 1042 | ), 1043 | ], 1044 | ts.NodeFlags.Const 1045 | ); 1046 | } 1047 | ``` 1048 | 1049 | ```diff 1050 | -function helloWorld() {} 1051 | +const helloWorld = () => {}; 1052 | ``` 1053 | 1054 | > **Tip** - You can see the source for this at [/example-transformers/replace-node](/example-transformers/replace-node) - if wanting to run locally you can run it via `yarn build replace-node`. 1055 | 1056 | ### Replacing a node with multiple nodes 1057 | 1058 | Interestingly, a visitor function can also return an array of nodes instead of just one node. 1059 | That means, even though it gets one node as input, it can return multiple nodes which replaces that input node. 1060 | 1061 | ```ts 1062 | type Visitor = 1063 | (node: TIn) => VisitResult; 1064 | type VisitResult = T | readonly Node[]; 1065 | ``` 1066 | 1067 | Let's just replace every expression statement with two copies of the same statement (duplicating it) - 1068 | 1069 | ```ts 1070 | const transformer: ts.TransformerFactory = context => { 1071 | return sourceFile => { 1072 | const visitor = (node: ts.Node): ts.VisitResult => { 1073 | // If it is a expression statement, 1074 | if (ts.isExpressionStatement(node)) { 1075 | // Return it twice. 1076 | // Effectively duplicating the statement 1077 | return [node, node]; 1078 | } 1079 | 1080 | return ts.visitEachChild(node, visitor, context); 1081 | }; 1082 | 1083 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 1084 | }; 1085 | }; 1086 | ``` 1087 | 1088 | So, 1089 | 1090 | ```ts 1091 | let a = 1; 1092 | a = 2; 1093 | ``` 1094 | 1095 | becomes 1096 | 1097 | ```js 1098 | let a = 1; 1099 | a = 2; 1100 | a = 2; 1101 | ``` 1102 | 1103 | > **Tip** - You can see the source for this at [/example-transformers/return-multiple-node](/example-transformers/return-multiple-node) - if wanting to run locally you can run it via `yarn build return-multiple-node`. 1104 | 1105 | The declaration statement (first line) is ignored as it's not a `ExpressionStatement`. 1106 | 1107 | _Note_ - Make sure that what you are trying to do actually makes sense in the AST. For ex., returning two expressions instead of one is often just invalid. 1108 | 1109 | Say there is an assignment expression (BinaryExpression with with EqualToken operator), `a = b = 2`. Now returning two nodes instead of `b = 2` expression is invalid (because right hand side can not be multiple nodes). So, TS will throw an error - `Debug Failure. False expression: Too many nodes written to output.` 1110 | 1111 | ### Inserting a sibling node 1112 | 1113 | This is effectively same as the [previous section](#replacing-a-node-with-multiple-nodes). Just return a array of nodes including itself and other sibling nodes. 1114 | 1115 | ### Removing a node 1116 | 1117 | What if you don't want a specific node anymore? 1118 | Return an `undefined`! 1119 | 1120 | ```ts 1121 | if (ts.isImportDeclaration(node)) { 1122 | // Will remove all import declarations 1123 | return undefined; 1124 | } 1125 | ``` 1126 | 1127 | ```diff 1128 | import lodash from 'lodash'; 1129 | -import lodash from 'lodash'; 1130 | ``` 1131 | 1132 | > **Tip** - You can see the source for this at [/example-transformers/remove-node](/example-transformers/remove-node) - if wanting to run locally you can run it via `yarn build remove-node`. 1133 | 1134 | ### Adding new import declarations 1135 | 1136 | Sometimes your transformation will need some runtime part, 1137 | for that you can add your own import declaration. 1138 | 1139 | ```ts 1140 | ts.factory.updateSourceFile(sourceFile, [ 1141 | ts.factory.createImportDeclaration( 1142 | /* modifiers */ undefined, 1143 | ts.factory.createImportClause( 1144 | false, 1145 | ts.factory.createIdentifier('DefaultImport'), 1146 | ts.factory.createNamedImports([ 1147 | ts.factory.createImportSpecifier( 1148 | false, 1149 | undefined, 1150 | ts.factory.createIdentifier('namedImport') 1151 | ), 1152 | ]) 1153 | ), 1154 | ts.factory.createStringLiteral('package') 1155 | ), 1156 | // Ensures the rest of the source files statements are still defined. 1157 | ...sourceFile.statements, 1158 | ]); 1159 | ``` 1160 | 1161 | ```diff 1162 | +import DefaultImport, { namedImport } from "package"; 1163 | ``` 1164 | 1165 | > **Tip** - You can see the source for this at [/example-transformers/add-import-declaration](/example-transformers/add-import-declaration) - if wanting to run locally you can run it via `yarn build add-import-declaration`. 1166 | 1167 | ## Scope 1168 | 1169 | ### Pushing a variable declaration to the top of its scope 1170 | 1171 | Sometimes you may want to push a `VariableDeclaration` so you can assign to it. 1172 | Remember that this only hoists the variable - 1173 | the assignment will still be where it was in the source. 1174 | 1175 | ```ts 1176 | if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) { 1177 | context.hoistVariableDeclaration(node.name); 1178 | return node; 1179 | } 1180 | ``` 1181 | 1182 | ```diff 1183 | function functionOne() { 1184 | + var innerOne; 1185 | + var innerTwo; 1186 | const innerOne = true; 1187 | const innerTwo = true; 1188 | } 1189 | ``` 1190 | 1191 | > **Tip** - You can see the source for this at [/example-transformers/hoist-variable-declaration](/example-transformers/hoist-variable-declaration) - if wanting to run locally you can run it via `yarn build hoist-variable-declaration`. 1192 | 1193 | You can also do this with function declarations: 1194 | 1195 | ```ts 1196 | if (ts.isFunctionDeclaration(node)) { 1197 | context.hoistFunctionDeclaration(node); 1198 | return node; 1199 | } 1200 | ``` 1201 | 1202 | ```diff 1203 | +function functionOne() { 1204 | + console.log('hello, world!'); 1205 | +} 1206 | if (true) { 1207 | function functionOne() { 1208 | console.log('hello, world!'); 1209 | } 1210 | } 1211 | ``` 1212 | 1213 | > **Tip** - You can see the source for this at [/example-transformers/hoist-function-declaration](/example-transformers/hoist-function-declaration) - if wanting to run locally you can run it via `yarn build hoist-function-declaration`. 1214 | 1215 | ### Pushing a variable declaration to a parent scope 1216 | 1217 | > **TODO** - Is this possible? 1218 | 1219 | ### Checking if a local variable is referenced 1220 | 1221 | > **TODO** - Is this possible? 1222 | 1223 | ### Defining a unique variable 1224 | 1225 | Sometimes you want to add a new variable that has a unique name within its scope, 1226 | luckily it's possible without needing to go through any hoops. 1227 | 1228 | ```ts 1229 | if (ts.isVariableDeclarationList(node)) { 1230 | return ts.factory.updateVariableDeclarationList(node, [ 1231 | ...node.declarations, 1232 | ts.factory.createVariableDeclaration( 1233 | ts.factory.createUniqueName('hello'), 1234 | undefined /* exclamation token */, 1235 | undefined /* type */, 1236 | ts.factory.createStringLiteral('world') 1237 | ), 1238 | ]); 1239 | } 1240 | 1241 | return ts.visitEachChild(node, visitor, context); 1242 | ``` 1243 | 1244 | ```diff 1245 | -const hello = 'world'; 1246 | +const hello = 'world', hello_1 = "world"; 1247 | ``` 1248 | 1249 | > **Tip** - You can see the source for this at [/example-transformers/create-unique-name](/example-transformers/create-unique-name) - if wanting to run locally you can run it via `yarn build create-unique-name`. 1250 | 1251 | ### Rename a binding and its references 1252 | 1253 | > **TODO** - Is this possible in a concise way? 1254 | 1255 | ## Finding 1256 | 1257 | ### Get line number and column 1258 | 1259 | ``` 1260 | sourceFile.getLineAndCharacterOfPosition(node.getStart()); 1261 | ``` 1262 | 1263 | ## Advanced 1264 | 1265 | ### Evaluating expressions 1266 | 1267 | > **TODO** - Is this possible? 1268 | 1269 | ### Following module imports 1270 | 1271 | It's possible! 1272 | 1273 | ```ts 1274 | // We need to use a Program transformer to get ahold of the program object. 1275 | const transformerProgram = (program: ts.Program) => { 1276 | const transformerFactory: ts.TransformerFactory = context => { 1277 | return sourceFile => { 1278 | const visitor = (node: ts.Node): ts.Node => { 1279 | if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { 1280 | const typeChecker = program.getTypeChecker(); 1281 | const importSymbol = typeChecker.getSymbolAtLocation(node.moduleSpecifier)!; 1282 | const exportSymbols = typeChecker.getExportsOfModule(importSymbol); 1283 | 1284 | exportSymbols.forEach(symbol => 1285 | console.log( 1286 | `found "${ 1287 | symbol.escapedName 1288 | }" export with value "${symbol.valueDeclaration!.getText()}"` 1289 | ) 1290 | ); 1291 | 1292 | return node; 1293 | } 1294 | 1295 | return ts.visitEachChild(node, visitor, context); 1296 | }; 1297 | 1298 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 1299 | }; 1300 | }; 1301 | 1302 | return transformerFactory; 1303 | }; 1304 | ``` 1305 | 1306 | Which will log this to the console: 1307 | 1308 | ``` 1309 | found "hello" export with value "hello = 'world'" 1310 | found "default" export with value "export default 'hello';" 1311 | ``` 1312 | 1313 | You can also traverse the imported node as well using `ts.visitChild` and the like. 1314 | 1315 | > **Tip** - You can see the source for this at [/example-transformers/follow-imports](/example-transformers/follow-imports) - if wanting to run locally you can run it via `yarn build follow-imports`. 1316 | 1317 | ### Following node module imports 1318 | 1319 | Like following TypeScript imports for the code that you own, 1320 | sometimes we may want to also interrogate the code inside a module we're importing. 1321 | 1322 | Using the same code above except running on a `node_modules` import we get this logged to the console: 1323 | 1324 | ``` 1325 | found "mixin" export with value: 1326 | export declare function mixin(): { 1327 | color: string; 1328 | };" 1329 | found "constMixin" export with value: 1330 | export declare function constMixin(): { 1331 | color: 'blue'; 1332 | };" 1333 | ``` 1334 | 1335 | Hmm what - we're getting the type def AST instead of source code... 1336 | Lame! 1337 | 1338 | So it turns out it's a little harder for us to get this working (at least out of the box). 1339 | It turns out we have two options : 1340 | 1341 | 1. Turn on `allowJs` in the tsconfig and the **delete the type def**... 1342 | which will give us the source AST... 1343 | but we now won't have type defs... 1344 | So this isn't desirable. 1345 | 2. Create another TS program and do the dirty work ourselves 1346 | 1347 | **Spoiler:** _We're going with option 2_. 1348 | It's more resilient and will work when type checking is turned off - 1349 | which is also how we'll follow TypeScript imports in that scenario! 1350 | 1351 | ```ts 1352 | const visitor = (node: ts.Node): ts.Node => { 1353 | if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { 1354 | // Find the import location in the file system using require.resolve 1355 | const pkgEntry = require.resolve(`${node.moduleSpecifier.text}`); 1356 | 1357 | // Create another program 1358 | const innerProgram = ts.createProgram([pkgEntry], { 1359 | // Important to set this to true! 1360 | allowJs: true, 1361 | }); 1362 | 1363 | console.log(innerProgram.getSourceFile(pkgEntry)?.getText()); 1364 | 1365 | return node; 1366 | } 1367 | 1368 | return ts.visitEachChild(node, visitor, context); 1369 | }; 1370 | ``` 1371 | 1372 | Which will log this to the console: 1373 | 1374 | ``` 1375 | export function mixin() { 1376 | return { color: 'red' }; 1377 | } 1378 | 1379 | export function constMixin() { 1380 | return { color: 'blue' } 1381 | } 1382 | ``` 1383 | 1384 | Awesome! 1385 | The cool thing about this btw is that since we've made a _program_ we will get all of _its_ imports followed for free! 1386 | However it'll have the same problem as above if they have type defs - 1387 | so watch out if you need to jump through multiple imports - 1388 | you'll probably have to do something more clever. 1389 | 1390 | > **Tip** - You can see the source for this at [/example-transformers/follow-node-modules-imports](/example-transformers/follow-node-modules-imports) - if wanting to run locally you can run it via `yarn build follow-node-modules-imports`. 1391 | 1392 | ### Transforming jsx 1393 | 1394 | TypeScript can also transform [JSX](https://reactjs.org/docs/introducing-jsx.html) - 1395 | there are a handful of helper methods to get started. 1396 | All previous methods of visiting and manipulation apply. 1397 | 1398 | - `ts.isJsxXyz(node)` 1399 | - `ts.factory.updateJsxXyz(node, ...)` 1400 | - `ts.factory.createJsxXyz(...)` 1401 | 1402 | Interrogate the typescript import for more details. 1403 | The primary point is you need to create valid JSX - 1404 | however if you ensure the types are valid in your transformer it's very hard to get it wrong. 1405 | 1406 | ### Determining the file pragma 1407 | 1408 | Useful when wanting to know what the file pragma is so you can do something in your transform. 1409 | Say for example we wanted to know if a custom `jsx` pragma is being used: 1410 | 1411 | ```ts 1412 | const transformer = sourceFile => { 1413 | const jsxPragma = (sourceFile as any).pragmas.get('jsx'); // see below regarding the cast to `any` 1414 | if (jsxPragma) { 1415 | console.log(`a jsx pragma was found using the factory "${jsxPragma.arguments.factory}"`); 1416 | } 1417 | 1418 | return sourceFile; 1419 | }; 1420 | ``` 1421 | 1422 | The source file below would cause `'a jsx pragma was found using the factory "jsx"'` to be logged to console. 1423 | 1424 | ```ts 1425 | /** @jsx jsx */ 1426 | ``` 1427 | 1428 | > **Tip** - You can see the source for this at [/example-transformers/pragma-check](/example-transformers/pragma-check) - if wanting to run locally you can run it via `yarn build pragma-check`. 1429 | 1430 | Currently as of 29/12/2019 `pragmas` is not on the typings for `sourceFile` - 1431 | so you'll have to cast it to `any` to gain access to it. 1432 | 1433 | ### Resetting the file pragma 1434 | 1435 | Sometimes during transformation you might want to change the pragma _back_ to the default (in our case React). 1436 | I've found success with the following code: 1437 | 1438 | ```ts 1439 | const transformer = sourceFile => { 1440 | sourceFile.pragmas.clear(); 1441 | delete sourceFile.localJsxFactory; 1442 | }; 1443 | ``` 1444 | 1445 | # Tips & tricks 1446 | 1447 | ## Composing transformers 1448 | 1449 | If you're like me sometimes you want to split your big transformer up into small more maintainable pieces. 1450 | Well luckily with a bit of coding elbow grease we can achieve this: 1451 | 1452 | ```ts 1453 | const transformers = [...]; 1454 | 1455 | function transformer( 1456 | program: ts.Program, 1457 | ): ts.TransformerFactory { 1458 | return context => { 1459 | const initializedTransformers = transformers.map(transformer => transformer(program)(context)); 1460 | 1461 | return sourceFile => { 1462 | return initializedTransformers.reduce((source, transformer) => { 1463 | return transformer(source); 1464 | }, sourceFile); 1465 | }; 1466 | }; 1467 | } 1468 | ``` 1469 | 1470 | ## Throwing a syntax error to ease the developer experience 1471 | 1472 | > **TODO** - Is this possible like it is in Babel? 1473 | > Or we use a [language service plugin](https://github.com/Microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin)? 1474 | 1475 | # Testing 1476 | 1477 | Generally with transformers the the usefulness of unit tests is quite limited. 1478 | I recommend writing integration tests to allow your tests to be super useful and resilient. 1479 | This boils down to: 1480 | 1481 | - **Write integration tests** over unit tests 1482 | - Avoid snapshot tests - only do it if it makes sense - **the larger the snapshot the less useful it is** 1483 | - Try to pick apart specific behavior for every test you write - and only **assert one thing per test** 1484 | 1485 | If you want you can use the [TypeScript compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#a-simple-transform-function) to setup your transformer for testing, 1486 | but I'd recommend using a library instead. 1487 | 1488 | ## [`ts-transformer-testing-library`](https://github.com/marionebl/ts-transformer-testing-library) 1489 | 1490 | This library makes testing transformers easy. 1491 | It is made to be used in conjunction with a test runner such as [`jest`](https://github.com/facebook/jest). 1492 | It simplifies the setup of your transformer, 1493 | but still allows you to write your tests as you would for any other piece of software. 1494 | 1495 | Here's an example test using it: 1496 | 1497 | ```ts 1498 | import { Transformer } from 'ts-transformer-testing-library'; 1499 | import transformerFactory from '../index'; 1500 | import pkg from '../../../../package.json'; 1501 | 1502 | const transformer = new Transformer() 1503 | .addTransformer(transformerFactory) 1504 | .addMock({ name: pkg.name, content: `export const jsx: any = () => null` }) 1505 | .addMock({ 1506 | name: 'react', 1507 | content: `export default {} as any; export const useState = {} as any;`, 1508 | }) 1509 | .setFilePath('/index.tsx'); 1510 | 1511 | it('should add react default import if it only has named imports', () => { 1512 | const actual = transformer.transform(` 1513 | /** @jsx jsx */ 1514 | import { useState } from 'react'; 1515 | import { jsx } from '${pkg.name}'; 1516 | 1517 |
hello world
1518 | `); 1519 | 1520 | // We are also using `jest-extended` here to add extra matchers to the jest object. 1521 | expect(actual).toIncludeRepeated('import React, { useState } from "react"', 1); 1522 | }); 1523 | ``` 1524 | 1525 | # Known bugs 1526 | 1527 | ## EmitResolver cannot handle `JsxOpeningLikeElement` and `JsxOpeningFragment` that didn't originate from the parse tree 1528 | 1529 | If you replace a node with a new jsx element like this: 1530 | 1531 | ```tsx 1532 | const visitor = node => { 1533 | return ts.factory.createJsxFragment( 1534 | ts.factory.createJsxOpeningFragment(), 1535 | [], 1536 | ts.factory.createJsxJsxClosingFragment() 1537 | ); 1538 | }; 1539 | ``` 1540 | 1541 | It will blow up if there are any surrounding `const` or `let` variables. 1542 | A work around is to ensure the opening/closing elements are passed into `ts.setOriginalNode`: 1543 | 1544 | ```diff 1545 | ts.createJsxFragment( 1546 | - ts.createJsxOpeningFragment(), 1547 | + ts.setOriginalNode(ts.factory.createJsxOpeningFragment(), node), 1548 | [], 1549 | - ts.createJsxJsxClosingFragment() 1550 | + ts.setOriginalNode(ts.factory.createJsxJsxClosingFragment(), node) 1551 | ); 1552 | ``` 1553 | 1554 | See https://github.com/microsoft/TypeScript/issues/35686 for more information. 1555 | -------------------------------------------------------------------------------- /example-transformers/add-import-declaration/source.ts: -------------------------------------------------------------------------------- 1 | console.log('hello, world!'); 2 | -------------------------------------------------------------------------------- /example-transformers/add-import-declaration/transformed/source.js: -------------------------------------------------------------------------------- 1 | import DefaultImport, { namedImport } from "package"; 2 | console.log('hello, world!'); 3 | -------------------------------------------------------------------------------- /example-transformers/add-import-declaration/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = () => { 4 | return sourceFile => { 5 | return ts.factory.updateSourceFile(sourceFile, [ 6 | ts.factory.createImportDeclaration( 7 | /* modifiers */ undefined, 8 | ts.factory.createImportClause( 9 | false, 10 | ts.factory.createIdentifier('DefaultImport'), 11 | ts.factory.createNamedImports([ 12 | ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier('namedImport')), 13 | ]) 14 | ), 15 | ts.factory.createStringLiteral('package') 16 | ), 17 | // Ensures the rest of the source files statements are still defined. 18 | ...sourceFile.statements, 19 | ]); 20 | }; 21 | }; 22 | 23 | export default transformer; 24 | -------------------------------------------------------------------------------- /example-transformers/add-import-declaration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/build-single.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd ./example-transformers/$1 && tspc 4 | -------------------------------------------------------------------------------- /example-transformers/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd example-transformers && ls -d */ | xargs -I {} bash -c "cd '{}' && tspc" 4 | -------------------------------------------------------------------------------- /example-transformers/create-unique-name/source.ts: -------------------------------------------------------------------------------- 1 | const hello = 'world'; 2 | -------------------------------------------------------------------------------- /example-transformers/create-unique-name/transformed/source.js: -------------------------------------------------------------------------------- 1 | const hello = 'world', hello_1 = "world"; 2 | -------------------------------------------------------------------------------- /example-transformers/create-unique-name/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = context => { 4 | return sourceFile => { 5 | const visitor = (node: ts.Node): ts.Node => { 6 | if (ts.isVariableDeclarationList(node)) { 7 | return ts.factory.updateVariableDeclarationList(node, [ 8 | ...node.declarations, 9 | ts.factory.createVariableDeclaration( 10 | ts.factory.createUniqueName('hello'), 11 | undefined /* exclamation token */, 12 | undefined /* type */, 13 | ts.factory.createStringLiteral('world') 14 | ), 15 | ]); 16 | } 17 | 18 | return ts.visitEachChild(node, visitor, context); 19 | }; 20 | 21 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 22 | }; 23 | }; 24 | 25 | export default transformer; 26 | -------------------------------------------------------------------------------- /example-transformers/create-unique-name/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/find-parent/source.ts: -------------------------------------------------------------------------------- 1 | function hello() { 2 | if (true) { 3 | 'world'; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example-transformers/find-parent/transformed/import.js: -------------------------------------------------------------------------------- 1 | function hello() { 2 | if (true) { 3 | 'world'; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example-transformers/find-parent/transformed/source.js: -------------------------------------------------------------------------------- 1 | function hello() { 2 | if (true) { 3 | 'world'; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example-transformers/find-parent/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const findParent = (node: ts.Node, predicate: (node: ts.Node) => boolean) => { 4 | if (!node.parent) { 5 | return undefined; 6 | } 7 | 8 | if (predicate(node.parent)) { 9 | return node.parent; 10 | } 11 | 12 | return findParent(node.parent, predicate); 13 | }; 14 | 15 | const transformerFactory: ts.TransformerFactory = context => { 16 | return sourceFile => { 17 | const visitor = (node: ts.Node): ts.Node => { 18 | if (ts.isStringLiteral(node)) { 19 | const parent = findParent(node, ts.isFunctionDeclaration); 20 | if (parent) { 21 | console.log('string literal has a function declaration parent'); 22 | } 23 | 24 | return node; 25 | } 26 | 27 | return ts.visitEachChild(node, visitor, context); 28 | }; 29 | 30 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 31 | }; 32 | }; 33 | 34 | export default transformerFactory; 35 | -------------------------------------------------------------------------------- /example-transformers/find-parent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/follow-imports/import.ts: -------------------------------------------------------------------------------- 1 | export const hello = 'world'; 2 | 3 | export default 'hello'; 4 | -------------------------------------------------------------------------------- /example-transformers/follow-imports/source.ts: -------------------------------------------------------------------------------- 1 | import { hello } from './import'; 2 | 3 | console.log(hello); 4 | -------------------------------------------------------------------------------- /example-transformers/follow-imports/transformed/import.js: -------------------------------------------------------------------------------- 1 | export const hello = 'world'; 2 | export default 'hello'; 3 | -------------------------------------------------------------------------------- /example-transformers/follow-imports/transformed/source.js: -------------------------------------------------------------------------------- 1 | import { hello } from './import'; 2 | console.log(hello); 3 | -------------------------------------------------------------------------------- /example-transformers/follow-imports/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformerProgram = (program: ts.Program) => { 4 | const transformerFactory: ts.TransformerFactory = context => { 5 | return sourceFile => { 6 | const visitor = (node: ts.Node): ts.Node => { 7 | if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { 8 | const typeChecker = program.getTypeChecker(); 9 | const importSymbol = typeChecker.getSymbolAtLocation(node.moduleSpecifier)!; 10 | const exportSymbols = typeChecker.getExportsOfModule(importSymbol); 11 | 12 | exportSymbols.forEach(symbol => 13 | console.log( 14 | `found "${ 15 | symbol.escapedName 16 | }" export with value "${symbol.valueDeclaration!.getText()}"` 17 | ) 18 | ); 19 | 20 | return node; 21 | } 22 | 23 | return ts.visitEachChild(node, visitor, context); 24 | }; 25 | 26 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 27 | }; 28 | }; 29 | 30 | return transformerFactory; 31 | }; 32 | 33 | export default transformerProgram; 34 | -------------------------------------------------------------------------------- /example-transformers/follow-imports/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/follow-node-modules-imports/node_modules/js-pkg/index.js: -------------------------------------------------------------------------------- 1 | export function mixin() { 2 | return { color: 'red' }; 3 | } 4 | 5 | export function constMixin() { 6 | return { color: 'blue' } 7 | } 8 | -------------------------------------------------------------------------------- /example-transformers/follow-node-modules-imports/node_modules/js-pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-pkg", 3 | "main": "./index.js" 4 | } 5 | -------------------------------------------------------------------------------- /example-transformers/follow-node-modules-imports/source.ts: -------------------------------------------------------------------------------- 1 | import { mixin } from 'js-pkg'; 2 | 3 | console.log(mixin); 4 | -------------------------------------------------------------------------------- /example-transformers/follow-node-modules-imports/transformed/source.js: -------------------------------------------------------------------------------- 1 | import { mixin } from 'js-pkg'; 2 | console.log(mixin); 3 | -------------------------------------------------------------------------------- /example-transformers/follow-node-modules-imports/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformerProgram = () => { 4 | const transformerFactory: ts.TransformerFactory = context => { 5 | return sourceFile => { 6 | const visitor = (node: ts.Node): ts.Node => { 7 | if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { 8 | // Find the import location in the file system using require.resolve 9 | const pkgEntry = require.resolve(`${node.moduleSpecifier.text}`); 10 | 11 | // Create another program 12 | const innerProgram = ts.createProgram([pkgEntry], { 13 | // Important to set this to true! 14 | allowJs: true, 15 | }); 16 | 17 | console.log(innerProgram.getSourceFile(pkgEntry)?.getText()); 18 | 19 | return node; 20 | } 21 | 22 | return ts.visitEachChild(node, visitor, context); 23 | }; 24 | 25 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 26 | }; 27 | }; 28 | 29 | return transformerFactory; 30 | }; 31 | 32 | export default transformerProgram; 33 | -------------------------------------------------------------------------------- /example-transformers/follow-node-modules-imports/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": false, 5 | "outDir": "transformed", 6 | "plugins": [{ "transform": "./transformer.ts" }] 7 | }, 8 | "include": ["./source.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /example-transformers/hoist-function-declaration/source.ts: -------------------------------------------------------------------------------- 1 | if (true) { 2 | function functionOne() { 3 | console.log('hello, world!'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example-transformers/hoist-function-declaration/transformed/source.js: -------------------------------------------------------------------------------- 1 | function functionOne() { 2 | console.log('hello, world!'); 3 | } 4 | if (true) { 5 | function functionOne() { 6 | console.log('hello, world!'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/hoist-function-declaration/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = context => { 4 | return sourceFile => { 5 | const visitor = (node: ts.Node): ts.Node => { 6 | if (ts.isFunctionDeclaration(node)) { 7 | context.hoistFunctionDeclaration(node); 8 | return node; 9 | } 10 | 11 | return ts.visitEachChild(node, visitor, context); 12 | }; 13 | 14 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 15 | }; 16 | }; 17 | 18 | export default transformer; 19 | -------------------------------------------------------------------------------- /example-transformers/hoist-function-declaration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/hoist-variable-declaration/source.ts: -------------------------------------------------------------------------------- 1 | function functionOne() { 2 | const innerOne = true; 3 | const innerTwo = true; 4 | } 5 | -------------------------------------------------------------------------------- /example-transformers/hoist-variable-declaration/transformed/source.js: -------------------------------------------------------------------------------- 1 | function functionOne() { 2 | var innerOne, innerTwo; 3 | const innerOne = true; 4 | const innerTwo = true; 5 | } 6 | -------------------------------------------------------------------------------- /example-transformers/hoist-variable-declaration/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = context => { 4 | return sourceFile => { 5 | const visitor = (node: ts.Node): ts.Node => { 6 | if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) { 7 | context.hoistVariableDeclaration(node.name); 8 | return node; 9 | } 10 | 11 | return ts.visitEachChild(node, visitor, context); 12 | }; 13 | 14 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 15 | }; 16 | }; 17 | 18 | export default transformer; 19 | -------------------------------------------------------------------------------- /example-transformers/hoist-variable-declaration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/log-every-node/source.ts: -------------------------------------------------------------------------------- 1 | function hello() { 2 | console.log('world'); 3 | } 4 | -------------------------------------------------------------------------------- /example-transformers/log-every-node/transformed/source.js: -------------------------------------------------------------------------------- 1 | function hello() { 2 | console.log('world'); 3 | } 4 | -------------------------------------------------------------------------------- /example-transformers/log-every-node/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = context => { 4 | return sourceFile => { 5 | const visitor = (node: ts.Node): ts.Node => { 6 | console.log(node.kind, `\t# ts.SyntaxKind.${ts.SyntaxKind[node.kind]}`); 7 | return ts.visitEachChild(node, visitor, context); 8 | }; 9 | 10 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 11 | }; 12 | }; 13 | 14 | export default transformer; 15 | -------------------------------------------------------------------------------- /example-transformers/log-every-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/match-identifier-by-symbol/source.ts: -------------------------------------------------------------------------------- 1 | let foo = 1; 2 | foo = 2; 3 | { 4 | const foo = 'abcd'; 5 | } 6 | foo = 3; 7 | -------------------------------------------------------------------------------- /example-transformers/match-identifier-by-symbol/transformed/source.js: -------------------------------------------------------------------------------- 1 | let foo = 1; 2 | foo = 2; 3 | { 4 | const foo = 'abcd'; 5 | } 6 | foo = 3; 7 | -------------------------------------------------------------------------------- /example-transformers/match-identifier-by-symbol/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformerProgram = (program: ts.Program) => { 4 | const typeChecker = program.getTypeChecker(); 5 | 6 | // Create array of found symbols 7 | const foundSymbols = new Array(); 8 | 9 | const transformerFactory: ts.TransformerFactory = context => { 10 | return sourceFile => { 11 | const visitor = (node: ts.Node): ts.Node => { 12 | if (ts.isIdentifier(node)) { 13 | const relatedSymbol = typeChecker.getSymbolAtLocation(node)!; 14 | 15 | // Check if array already contains same symbol - check by reference 16 | if (foundSymbols.includes(relatedSymbol)) { 17 | const foundIndex = foundSymbols.indexOf(relatedSymbol); 18 | console.log( 19 | `Found existing symbol at position = ${foundIndex} and name = "${relatedSymbol.name}"` 20 | ); 21 | } else { 22 | // If not found, Add it to array 23 | foundSymbols.push(relatedSymbol); 24 | 25 | console.log( 26 | `Found new symbol with name = "${ 27 | relatedSymbol.name 28 | }". Added at position = ${foundSymbols.length - 1}` 29 | ); 30 | } 31 | 32 | return node; 33 | } 34 | 35 | return ts.visitEachChild(node, visitor, context); 36 | }; 37 | 38 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 39 | }; 40 | }; 41 | 42 | return transformerFactory; 43 | }; 44 | 45 | export default transformerProgram; 46 | -------------------------------------------------------------------------------- /example-transformers/match-identifier-by-symbol/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/my-first-transformer/source.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | babel === plugins; 3 | -------------------------------------------------------------------------------- /example-transformers/my-first-transformer/transformed/source.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | typescript === transformers; 3 | -------------------------------------------------------------------------------- /example-transformers/my-first-transformer/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = context => { 4 | return sourceFile => { 5 | const visitor = (node: ts.Node): ts.Node => { 6 | if (ts.isIdentifier(node)) { 7 | switch (node.escapedText) { 8 | case 'babel': 9 | return ts.factory.createIdentifier('typescript'); 10 | 11 | case 'plugins': 12 | return ts.factory.createIdentifier('transformers'); 13 | } 14 | } 15 | 16 | return ts.visitEachChild(node, visitor, context); 17 | }; 18 | 19 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 20 | }; 21 | }; 22 | 23 | export default transformer; 24 | -------------------------------------------------------------------------------- /example-transformers/my-first-transformer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/pragma-check/source.ts: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | -------------------------------------------------------------------------------- /example-transformers/pragma-check/transformed/source.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | -------------------------------------------------------------------------------- /example-transformers/pragma-check/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = context => { 4 | return sourceFile => { 5 | const jsxPragma = (sourceFile as any).pragmas.get('jsx'); 6 | if (jsxPragma) { 7 | console.log(`a jsx pragma was found using the factory "${jsxPragma.arguments.factory}"`); 8 | } 9 | 10 | return sourceFile; 11 | }; 12 | }; 13 | 14 | export default transformer; 15 | -------------------------------------------------------------------------------- /example-transformers/pragma-check/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/remove-node/source.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import lodash from 'lodash'; 3 | 4 | console.log(lodash); 5 | -------------------------------------------------------------------------------- /example-transformers/remove-node/transformed/source.js: -------------------------------------------------------------------------------- 1 | console.log(lodash); 2 | export {}; 3 | -------------------------------------------------------------------------------- /example-transformers/remove-node/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = context => { 4 | return sourceFile => { 5 | const visitor = (node: ts.Node): ts.Node | undefined => { 6 | if (ts.isImportDeclaration(node)) { 7 | return undefined; 8 | } 9 | 10 | return ts.visitEachChild(node, visitor, context); 11 | }; 12 | 13 | const sourceFileVisitor = (sourceFile: ts.SourceFile): ts.SourceFile => { 14 | return ts.visitEachChild(sourceFile, visitor, context); 15 | }; 16 | 17 | return ts.visitNode(sourceFile, sourceFileVisitor, ts.isSourceFile); 18 | }; 19 | }; 20 | 21 | export default transformer; 22 | -------------------------------------------------------------------------------- /example-transformers/remove-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/replace-node/source.ts: -------------------------------------------------------------------------------- 1 | function helloWorld() {} 2 | -------------------------------------------------------------------------------- /example-transformers/replace-node/transformed/source.js: -------------------------------------------------------------------------------- 1 | const helloWorld = () => { } 2 | -------------------------------------------------------------------------------- /example-transformers/replace-node/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = context => { 4 | return sourceFile => { 5 | const visitor = (node: ts.Node): ts.Node => { 6 | if (ts.isFunctionDeclaration(node)) { 7 | // Will replace any function it finds with an arrow function. 8 | return ts.factory.createVariableDeclarationList( 9 | [ 10 | ts.factory.createVariableDeclaration( 11 | ts.factory.createIdentifier(node.name!.escapedText as string), 12 | undefined /* exclamation token */, 13 | undefined /* type */, 14 | ts.factory.createArrowFunction( 15 | undefined /* modifiers */, 16 | undefined /* typeParameters */, 17 | [] /* parameters */, 18 | undefined /* type */, 19 | ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 20 | ts.factory.createBlock([], false) 21 | ) 22 | ), 23 | ], 24 | ts.NodeFlags.Const 25 | ); 26 | } 27 | 28 | return ts.visitEachChild(node, visitor, context); 29 | }; 30 | 31 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 32 | }; 33 | }; 34 | 35 | export default transformer; 36 | -------------------------------------------------------------------------------- /example-transformers/replace-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/return-multiple-node/source.ts: -------------------------------------------------------------------------------- 1 | let a = 1; 2 | { 3 | const a = 'abcd'; 4 | } 5 | a = 2; 6 | console.log(a); 7 | -------------------------------------------------------------------------------- /example-transformers/return-multiple-node/transformed/source.js: -------------------------------------------------------------------------------- 1 | let a = 1; 2 | { 3 | const a = 'abcd'; 4 | } 5 | a = 2; 6 | a = 2; 7 | console.log(a); 8 | console.log(a); 9 | -------------------------------------------------------------------------------- /example-transformers/return-multiple-node/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = context => { 4 | return sourceFile => { 5 | const visitor = (node: ts.Node): ts.VisitResult => { 6 | // If it is a expression statement, 7 | if (ts.isExpressionStatement(node)) { 8 | // Return it twice. 9 | // Effectively duplicating the statement 10 | return [node, node]; 11 | } 12 | 13 | return ts.visitEachChild(node, visitor, context); 14 | }; 15 | 16 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 17 | }; 18 | }; 19 | 20 | export default transformer; 21 | -------------------------------------------------------------------------------- /example-transformers/return-multiple-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example-transformers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example-transformers/update-node/source.ts: -------------------------------------------------------------------------------- 1 | const hello = true; 2 | -------------------------------------------------------------------------------- /example-transformers/update-node/transformed/source.js: -------------------------------------------------------------------------------- 1 | const hello = "updated-world"; 2 | -------------------------------------------------------------------------------- /example-transformers/update-node/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | const transformer: ts.TransformerFactory = context => { 4 | return sourceFile => { 5 | const visitor = (node: ts.Node): ts.Node => { 6 | if (ts.isVariableDeclaration(node)) { 7 | return ts.factory.updateVariableDeclaration( 8 | node, 9 | node.name, 10 | undefined /* exclamation token */, 11 | node.type, 12 | ts.factory.createStringLiteral('updated-world') 13 | ); 14 | } 15 | 16 | return ts.visitEachChild(node, visitor, context); 17 | }; 18 | 19 | return ts.visitNode(sourceFile, visitor, ts.isSourceFile); 20 | }; 21 | }; 22 | 23 | export default transformer; 24 | -------------------------------------------------------------------------------- /example-transformers/update-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "transformed", 5 | "plugins": [{ "transform": "./transformer.ts", "type": "raw" }] 6 | }, 7 | "files": ["source.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "generate-toc": "markdown-toc -i ./translations/en/transformer-handbook.md", 5 | "build-all": "sh ./example-transformers/build.sh", 6 | "build": "sh ./example-transformers/build-single.sh" 7 | }, 8 | "dependencies": { 9 | "@types/node": "^22.10.1", 10 | "markdown-toc": "^1.2.0", 11 | "ts-node": "^8.5.4", 12 | "ts-patch": "^3.3.0", 13 | "typescript": "5.7.2" 14 | }, 15 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 16 | } 17 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/node@^22.10.1": 6 | version "22.10.1" 7 | resolved "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz" 8 | integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== 9 | dependencies: 10 | undici-types "~6.20.0" 11 | 12 | ansi-red@^0.1.1: 13 | version "0.1.1" 14 | resolved "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz" 15 | dependencies: 16 | ansi-wrap "0.1.0" 17 | 18 | ansi-regex@^5.0.1: 19 | version "5.0.1" 20 | resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" 21 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 22 | 23 | ansi-styles@^4.1.0: 24 | version "4.3.0" 25 | resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" 26 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 27 | dependencies: 28 | color-convert "^2.0.1" 29 | 30 | ansi-wrap@0.1.0: 31 | version "0.1.0" 32 | resolved "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz" 33 | 34 | arg@^4.1.0: 35 | version "4.1.2" 36 | resolved "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz" 37 | 38 | argparse@^1.0.10, argparse@^1.0.7: 39 | version "1.0.10" 40 | resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" 41 | dependencies: 42 | sprintf-js "~1.0.2" 43 | 44 | autolinker@~0.28.0: 45 | version "0.28.1" 46 | resolved "https://registry.npmjs.org/autolinker/-/autolinker-0.28.1.tgz" 47 | dependencies: 48 | gulp-header "^1.7.1" 49 | 50 | buffer-from@^1.0.0: 51 | version "1.1.1" 52 | resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz" 53 | 54 | chalk@^4.1.2: 55 | version "4.1.2" 56 | resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" 57 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 58 | dependencies: 59 | ansi-styles "^4.1.0" 60 | supports-color "^7.1.0" 61 | 62 | coffee-script@^1.12.4: 63 | version "1.12.7" 64 | resolved "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz" 65 | 66 | color-convert@^2.0.1: 67 | version "2.0.1" 68 | resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" 69 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 70 | dependencies: 71 | color-name "~1.1.4" 72 | 73 | color-name@~1.1.4: 74 | version "1.1.4" 75 | resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" 76 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 77 | 78 | concat-stream@^1.5.2: 79 | version "1.6.2" 80 | resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz" 81 | dependencies: 82 | buffer-from "^1.0.0" 83 | inherits "^2.0.3" 84 | readable-stream "^2.2.2" 85 | typedarray "^0.0.6" 86 | 87 | concat-with-sourcemaps@*: 88 | version "1.1.0" 89 | resolved "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz" 90 | dependencies: 91 | source-map "^0.6.1" 92 | 93 | core-util-is@~1.0.0: 94 | version "1.0.2" 95 | resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" 96 | 97 | diacritics-map@^0.1.0: 98 | version "0.1.0" 99 | resolved "https://registry.npmjs.org/diacritics-map/-/diacritics-map-0.1.0.tgz" 100 | 101 | diff@^4.0.1: 102 | version "4.0.1" 103 | resolved "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz" 104 | 105 | esprima@^4.0.0: 106 | version "4.0.1" 107 | resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" 108 | 109 | expand-range@^1.8.1: 110 | version "1.8.2" 111 | resolved "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz" 112 | dependencies: 113 | fill-range "^2.1.0" 114 | 115 | extend-shallow@^2.0.1: 116 | version "2.0.1" 117 | resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz" 118 | dependencies: 119 | is-extendable "^0.1.0" 120 | 121 | fill-range@^2.1.0: 122 | version "2.2.4" 123 | resolved "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz" 124 | dependencies: 125 | is-number "^2.1.0" 126 | isobject "^2.0.0" 127 | randomatic "^3.0.0" 128 | repeat-element "^1.1.2" 129 | repeat-string "^1.5.2" 130 | 131 | for-in@^1.0.2: 132 | version "1.0.2" 133 | resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz" 134 | 135 | function-bind@^1.1.2: 136 | version "1.1.2" 137 | resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" 138 | integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 139 | 140 | global-prefix@^4.0.0: 141 | version "4.0.0" 142 | resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz" 143 | integrity sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA== 144 | dependencies: 145 | ini "^4.1.3" 146 | kind-of "^6.0.3" 147 | which "^4.0.0" 148 | 149 | gray-matter@^2.1.0: 150 | version "2.1.1" 151 | resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-2.1.1.tgz" 152 | dependencies: 153 | ansi-red "^0.1.1" 154 | coffee-script "^1.12.4" 155 | extend-shallow "^2.0.1" 156 | js-yaml "^3.8.1" 157 | toml "^2.3.2" 158 | 159 | gulp-header@^1.7.1: 160 | version "1.8.12" 161 | resolved "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz" 162 | dependencies: 163 | concat-with-sourcemaps "*" 164 | lodash.template "^4.4.0" 165 | through2 "^2.0.0" 166 | 167 | has-flag@^4.0.0: 168 | version "4.0.0" 169 | resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" 170 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 171 | 172 | hasown@^2.0.2: 173 | version "2.0.2" 174 | resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" 175 | integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== 176 | dependencies: 177 | function-bind "^1.1.2" 178 | 179 | inherits@^2.0.3, inherits@~2.0.3: 180 | version "2.0.4" 181 | resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" 182 | 183 | ini@^4.1.3: 184 | version "4.1.3" 185 | resolved "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz" 186 | integrity sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg== 187 | 188 | is-buffer@^1.1.5: 189 | version "1.1.6" 190 | resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" 191 | 192 | is-core-module@^2.13.0: 193 | version "2.15.1" 194 | resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz" 195 | integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== 196 | dependencies: 197 | hasown "^2.0.2" 198 | 199 | is-extendable@^0.1.0: 200 | version "0.1.1" 201 | resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" 202 | 203 | is-extendable@^1.0.1: 204 | version "1.0.1" 205 | resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz" 206 | dependencies: 207 | is-plain-object "^2.0.4" 208 | 209 | is-number@^2.1.0: 210 | version "2.1.0" 211 | resolved "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz" 212 | dependencies: 213 | kind-of "^3.0.2" 214 | 215 | is-number@^4.0.0: 216 | version "4.0.0" 217 | resolved "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz" 218 | 219 | is-plain-object@^2.0.4: 220 | version "2.0.4" 221 | resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" 222 | dependencies: 223 | isobject "^3.0.1" 224 | 225 | isarray@~1.0.0, isarray@1.0.0: 226 | version "1.0.0" 227 | resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" 228 | 229 | isexe@^3.1.1: 230 | version "3.1.1" 231 | resolved "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz" 232 | integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== 233 | 234 | isobject@^2.0.0: 235 | version "2.1.0" 236 | resolved "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz" 237 | dependencies: 238 | isarray "1.0.0" 239 | 240 | isobject@^3.0.1: 241 | version "3.0.1" 242 | resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" 243 | 244 | js-yaml@^3.8.1: 245 | version "3.13.1" 246 | resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz" 247 | dependencies: 248 | argparse "^1.0.7" 249 | esprima "^4.0.0" 250 | 251 | kind-of@^3.0.2: 252 | version "3.2.2" 253 | resolved "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" 254 | dependencies: 255 | is-buffer "^1.1.5" 256 | 257 | kind-of@^6.0.0: 258 | version "6.0.2" 259 | resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz" 260 | 261 | kind-of@^6.0.3: 262 | version "6.0.3" 263 | resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" 264 | integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== 265 | 266 | lazy-cache@^2.0.2: 267 | version "2.0.2" 268 | resolved "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz" 269 | dependencies: 270 | set-getter "^0.1.0" 271 | 272 | list-item@^1.1.1: 273 | version "1.1.1" 274 | resolved "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz" 275 | dependencies: 276 | expand-range "^1.8.1" 277 | extend-shallow "^2.0.1" 278 | is-number "^2.1.0" 279 | repeat-string "^1.5.2" 280 | 281 | lodash._reinterpolate@^3.0.0: 282 | version "3.0.0" 283 | resolved "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz" 284 | 285 | lodash.template@^4.4.0: 286 | version "4.5.0" 287 | resolved "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz" 288 | dependencies: 289 | lodash._reinterpolate "^3.0.0" 290 | lodash.templatesettings "^4.0.0" 291 | 292 | lodash.templatesettings@^4.0.0: 293 | version "4.2.0" 294 | resolved "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz" 295 | dependencies: 296 | lodash._reinterpolate "^3.0.0" 297 | 298 | make-error@^1.1.1: 299 | version "1.3.5" 300 | resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz" 301 | 302 | markdown-link@^0.1.1: 303 | version "0.1.1" 304 | resolved "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz" 305 | 306 | markdown-toc@^1.2.0: 307 | version "1.2.0" 308 | resolved "https://registry.npmjs.org/markdown-toc/-/markdown-toc-1.2.0.tgz" 309 | dependencies: 310 | concat-stream "^1.5.2" 311 | diacritics-map "^0.1.0" 312 | gray-matter "^2.1.0" 313 | lazy-cache "^2.0.2" 314 | list-item "^1.1.1" 315 | markdown-link "^0.1.1" 316 | minimist "^1.2.0" 317 | mixin-deep "^1.1.3" 318 | object.pick "^1.2.0" 319 | remarkable "^1.7.1" 320 | repeat-string "^1.6.1" 321 | strip-color "^0.1.0" 322 | 323 | math-random@^1.0.1: 324 | version "1.0.4" 325 | resolved "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz" 326 | 327 | minimist@^1.2.0: 328 | version "1.2.5" 329 | resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" 330 | 331 | minimist@^1.2.8: 332 | version "1.2.8" 333 | resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" 334 | integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== 335 | 336 | mixin-deep@^1.1.3: 337 | version "1.3.2" 338 | resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz" 339 | dependencies: 340 | for-in "^1.0.2" 341 | is-extendable "^1.0.1" 342 | 343 | object.pick@^1.2.0: 344 | version "1.3.0" 345 | resolved "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz" 346 | dependencies: 347 | isobject "^3.0.1" 348 | 349 | path-parse@^1.0.7: 350 | version "1.0.7" 351 | resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" 352 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 353 | 354 | process-nextick-args@~2.0.0: 355 | version "2.0.1" 356 | resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" 357 | 358 | randomatic@^3.0.0: 359 | version "3.1.1" 360 | resolved "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz" 361 | dependencies: 362 | is-number "^4.0.0" 363 | kind-of "^6.0.0" 364 | math-random "^1.0.1" 365 | 366 | readable-stream@^2.2.2, readable-stream@~2.3.6: 367 | version "2.3.6" 368 | resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz" 369 | dependencies: 370 | core-util-is "~1.0.0" 371 | inherits "~2.0.3" 372 | isarray "~1.0.0" 373 | process-nextick-args "~2.0.0" 374 | safe-buffer "~5.1.1" 375 | string_decoder "~1.1.1" 376 | util-deprecate "~1.0.1" 377 | 378 | remarkable@^1.7.1: 379 | version "1.7.4" 380 | resolved "https://registry.npmjs.org/remarkable/-/remarkable-1.7.4.tgz" 381 | dependencies: 382 | argparse "^1.0.10" 383 | autolinker "~0.28.0" 384 | 385 | repeat-element@^1.1.2: 386 | version "1.1.3" 387 | resolved "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz" 388 | 389 | repeat-string@^1.5.2, repeat-string@^1.6.1: 390 | version "1.6.1" 391 | resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" 392 | 393 | resolve@^1.22.2: 394 | version "1.22.8" 395 | resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" 396 | integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== 397 | dependencies: 398 | is-core-module "^2.13.0" 399 | path-parse "^1.0.7" 400 | supports-preserve-symlinks-flag "^1.0.0" 401 | 402 | safe-buffer@~5.1.0, safe-buffer@~5.1.1: 403 | version "5.1.2" 404 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" 405 | 406 | semver@^7.6.3: 407 | version "7.6.3" 408 | resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" 409 | integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== 410 | 411 | set-getter@^0.1.0: 412 | version "0.1.0" 413 | resolved "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz" 414 | dependencies: 415 | to-object-path "^0.3.0" 416 | 417 | source-map-support@^0.5.6: 418 | version "0.5.16" 419 | resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz" 420 | dependencies: 421 | buffer-from "^1.0.0" 422 | source-map "^0.6.0" 423 | 424 | source-map@^0.6.0, source-map@^0.6.1: 425 | version "0.6.1" 426 | resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" 427 | 428 | sprintf-js@~1.0.2: 429 | version "1.0.3" 430 | resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" 431 | 432 | string_decoder@~1.1.1: 433 | version "1.1.1" 434 | resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" 435 | dependencies: 436 | safe-buffer "~5.1.0" 437 | 438 | strip-ansi@^6.0.1: 439 | version "6.0.1" 440 | resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" 441 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 442 | dependencies: 443 | ansi-regex "^5.0.1" 444 | 445 | strip-color@^0.1.0: 446 | version "0.1.0" 447 | resolved "https://registry.npmjs.org/strip-color/-/strip-color-0.1.0.tgz" 448 | 449 | supports-color@^7.1.0: 450 | version "7.2.0" 451 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" 452 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 453 | dependencies: 454 | has-flag "^4.0.0" 455 | 456 | supports-preserve-symlinks-flag@^1.0.0: 457 | version "1.0.0" 458 | resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" 459 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 460 | 461 | through2@^2.0.0: 462 | version "2.0.5" 463 | resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz" 464 | dependencies: 465 | readable-stream "~2.3.6" 466 | xtend "~4.0.1" 467 | 468 | to-object-path@^0.3.0: 469 | version "0.3.0" 470 | resolved "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz" 471 | dependencies: 472 | kind-of "^3.0.2" 473 | 474 | toml@^2.3.2: 475 | version "2.3.6" 476 | resolved "https://registry.npmjs.org/toml/-/toml-2.3.6.tgz" 477 | 478 | ts-node@^8.5.4: 479 | version "8.5.4" 480 | resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz" 481 | dependencies: 482 | arg "^4.1.0" 483 | diff "^4.0.1" 484 | make-error "^1.1.1" 485 | source-map-support "^0.5.6" 486 | yn "^3.0.0" 487 | 488 | ts-patch@^3.3.0: 489 | version "3.3.0" 490 | resolved "https://registry.npmjs.org/ts-patch/-/ts-patch-3.3.0.tgz" 491 | integrity sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg== 492 | dependencies: 493 | chalk "^4.1.2" 494 | global-prefix "^4.0.0" 495 | minimist "^1.2.8" 496 | resolve "^1.22.2" 497 | semver "^7.6.3" 498 | strip-ansi "^6.0.1" 499 | 500 | typedarray@^0.0.6: 501 | version "0.0.6" 502 | resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" 503 | 504 | typescript@>=2.0, typescript@5.7.2: 505 | version "5.7.2" 506 | resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz" 507 | integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== 508 | 509 | undici-types@~6.20.0: 510 | version "6.20.0" 511 | resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz" 512 | integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== 513 | 514 | util-deprecate@~1.0.1: 515 | version "1.0.2" 516 | resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" 517 | 518 | which@^4.0.0: 519 | version "4.0.0" 520 | resolved "https://registry.npmjs.org/which/-/which-4.0.0.tgz" 521 | integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg== 522 | dependencies: 523 | isexe "^3.1.1" 524 | 525 | xtend@~4.0.1: 526 | version "4.0.2" 527 | resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" 528 | 529 | yn@^3.0.0: 530 | version "3.1.1" 531 | resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" 532 | --------------------------------------------------------------------------------