I am trying to write a typescript compiler transform using the typescript compiler API. However, when creating new Identifier nodes, even though the nodes get emitted to the final .js file, they seem to lack symbol binding information so the final output is incorrect.
Suppose I have the following program:
A.ts
export class A {
static myMethod() {
return 'value';
}
}
index.ts
import { A } from './A';
export function main() {
const value1 = 'replaceMe';
const value2 = A.myMethod();
const equals = value1 == value2;
}
Suppose I try to compile the above program with the following transformer:
function transformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => (file: ts.SourceFile) => transformFile(program, context, file);
}
function transformFile(program: ts.Program, context: ts.TransformationContext, file: ts.SourceFile): ts.SourceFile {
const transformedFile = ts.visitEachChild(file, child => visit(child, context), context);
console.log(ts.createPrinter().printFile(transformedFile));
return transformedFile;
}
function visit(node: ts.Node, context: ts.TransformationContext): ts.Node {
if (ts.isStringLiteral(node) && node.text == 'replaceMe') {
return ts.createCall(
ts.createPropertyAccess(
ts.createIdentifier('A'),
'myMethod'),
[],
[]);
}
return ts.visitEachChild(node, child => visit(child, context), context);
}
The intermediate AST actually looks correct when pretty-printed:
import { A } from './A';
export function main() {
const value1 = A.myMethod();
const value2 = A.myMethod();
const equals = value1 == value2;
}
But the output javascript is incorrect:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var A_1 = require("./A");
function main() {
var value1 = A.myMethod();
var value2 = A_1.A.myMethod();
var equals = value1 == value2;
}
exports.main = main;
I understand this may be because by creating a new identifier with ts.createIdentitier('A')
, this new identifier isn't bound to the same symbol as the other A
identifier in the same file.
Is there a way to bind a new identifier to an existing symbol using the public compiler API?
The AST is a data structure to represent the structure of your source file in a format readable by machines. Indeed, if I throw the above example in the TypeScript AST Viewer I get immediate access to the AST.
The TypeScript Compiler − The TypeScript compiler (tsc) converts the instructions written in TypeScript to its JavaScript equivalent. The TypeScript Language Service − The "Language Service" exposes an additional layer around the core compiler pipeline that are editor-like applications.
TypeScript is a free and open source programming language developed and maintained by Microsoft. It is a strict syntactical superset of JavaScript and adds optional static typing to the language. It is designed for the development of large applications and transpiles to JavaScript.
Typescript compilation happens in phases (parsing, binding, type checking, emitting, a bit more detail here). You can use information form a previous phase but you can't typically change it. The transformations you can do during the emit phase are intended to allow you to bring the AST from Typescript to Javascript, not to refactor the code.
One way to accomplish your goal, would be to create the program, apply the transformations, and then create a new program with the modified code, reusing as much of the original program as possible (reuse the same SourceFile
where no changes occurred)
function transformFile(program: ts.Program, file: ts.SourceFile): ts.SourceFile {
let empty = ()=> {};
// Dummy transformation context
let context: ts.TransformationContext = {
startLexicalEnvironment: empty,
suspendLexicalEnvironment: empty,
resumeLexicalEnvironment: empty,
endLexicalEnvironment: ()=> [],
getCompilerOptions: ()=> program.getCompilerOptions(),
hoistFunctionDeclaration: empty,
hoistVariableDeclaration: empty,
readEmitHelpers: ()=>undefined,
requestEmitHelper: empty,
enableEmitNotification: empty,
enableSubstitution: empty,
isEmitNotificationEnabled: ()=> false,
isSubstitutionEnabled: ()=> false,
onEmitNode: empty,
onSubstituteNode: (hint, node)=>node,
};
const transformedFile = ts.visitEachChild(file, child => visit(child, context), context);
return transformedFile;
}
function visit(node: ts.Node, context: ts.TransformationContext): ts.Node {
if (ts.isStringLiteral(node) && node.text == 'replaceMe') {
return ts.createCall(
ts.createPropertyAccess(
ts.createIdentifier('A'),
'myMethod'),
[],
[]);
}
return ts.visitEachChild(node, child => visit(child, context), context);
}
let host = ts.createCompilerHost({});
let program = ts.createProgram(["toTrans.ts"], {}, host)
let transformed = program.getSourceFiles()
.map(f=> ({ original: f, transformed: transformFile(program, f) }))
.reduce<{ [name: string] : {original: ts.SourceFile, transformed: ts.SourceFile }}>((r, f)=> { r[f.original.fileName] = f; return r; }, {});
let originalGetSourceFile = host.getSourceFile;
let printer = ts.createPrinter();
// Rig the host to return the new verisons of transformed files.
host.getSourceFile = function(fileName, languageVersion, onError, shouldCreateNewSourceFile){
let file = transformed[fileName];
if(file){
if(file.original != file.transformed){
// Since we need to return a SourceFile it is tempting to return the transformed source file and not parse it again
// The compiler doe not support Synthesized nodes in the AST except during emit, and it will check node positions
// (which for Synthesized are -1) and fail. So we need to reparse
return ts.createSourceFile(fileName, printer.printFile(file.transformed), languageVersion);
} else {
// For unchanged files it should be safe to reuse the source file
return file.original;
}
}
return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
}
// Recreate the program, we pass in the original to
program = ts.createProgram(["toTrans.ts"], {}, host, program);
var result = program.emit();
Another way would be to use the language service and apply these changes through the language service, but honestly I have no experience with that part of the compiler and it seemed more complicated than this approach.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With