Tips for associating an identifier with a preexisting symbol in a TypeScript compiler transformer

Trying to implement a typescript compiler transform with the typescript compiler API has been challenging. Despite emitting new Identifier nodes to the final .js file, they appear to lack symbol binding information, resulting in incorrect output.

For instance, consider the following program structure:

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;
}

If we attempt to compile the program above with a specific transformer implementation:

function transformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
    return (context: ts.TransformationContext) => (file: ts.SourceFile) => transformFile(program, context, file);
}

// More transformer functions...

Upon pretty-printing the intermediate AST, it appears correct:

import { A } from './A';

export function main() {
    const value1 = A.myMethod();
    const value2 = A.myMethod();
    const equals = value1 == value2;
}

However, the resulting javascript is flawed:

"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;

It seems that creating a new identifier with ts.createIdentitier('A') causes it not to be bound to the same symbol as the existing A identifier in the file.

Is there a method within the public compiler API to bind a new identifier to an existing symbol?

Answer №1

Compiling Typescript involves several phases such as parsing, binding, type checking, and emitting. The transformations made during the emission phase are primarily for converting the AST from Typescript to Javascript, rather than refactoring the code itself. You can utilize information from earlier phases but typically cannot alter it.

To achieve your objective, one approach is to create a program, apply the necessary transformations, and then generate a new program with the modified code while reusing the original program's components whenever possible (such as using the same SourceFile where no changes were made).

function transformFile(program: ts.Program, file: ts.SourceFile): ts.SourceFile {
    // Transformation context
    let empty = ()=> {};
    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)

// Map original and transformed files
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();

// Override getSourceFile in compiler host to return transformed files
host.getSourceFile = function(fileName, languageVersion, onError, shouldCreateNewSourceFile){
    let file = transformed[fileName];
    if(file){
        // Check if the file was changed
        if(file.original != file.transformed){
            // Need to parse the transformed source file again due to compiler limitations
            return ts.createSourceFile(fileName, printer.printFile(file.transformed), languageVersion);
        } else {
            return file.original; // Reuse unaltered files
        }
    }
    return  originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
}

// Recreate the program with updated files
program = ts.createProgram(["toTrans.ts"], {}, host, program);

var result = program.emit();

An alternative method could involve utilizing the language service to implement these changes, though I have limited experience in that aspect of the compiler and found this approach to be more straightforward.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best approach for integrating a Material UI Autocomplete component with graphql queries?

Hello there! I'm currently working with React Typescript and trying to incorporate query suggestions into an Autocomplete Material UI component in my project. Below is a snippet of my GraphQL queries: Query Definition: import gql from 'graphql- ...

In TypeScript and React, what is the appropriate type to retrieve the ID of a div when clicked?

I am facing an issue in finding the appropriate type for the onClick event that will help me retrieve the id of the clicked div. const getColor = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => { const color = event.target.id; // ...

What is the best way to transform a string into emojis using TypeScript or JavaScript?

Looking to convert emoji from string in typescript to display emoji in html. Here is a snippet of the Typescript file: export class Example { emoji:any; function(){ this.emoji = ":joy:" } } In an HTML file, I would like it to dis ...

Creating a TypeScript definition file to allow instantiation of a module

I'm encountering an issue with creating a declaration file for an existing module. When using JavaScript, the module is imported using the following syntax: var Library = require('thirdpartylibs'); var libInstance = new Library(); I have ...

The name 'const' is missing or not found

While working on my Angular application, I attempted to utilize the Typescript 3.4 feature known as "const assertion" in the following manner: const counterSettingsDefaults = { setTo: 10, tickSpeed: 200, increment: 1 } as const; Unfortunately, this resul ...

Refresh Angular component upon navigation

I have set up routes for my module: const routes: Routes = [ { path: ":level1/:level2/:level3", component: CategoriesComponent }, { path: ":level1/:level2", component: CategoriesComponent}, { path: ":level1", component: ...

Create a checklist with unique identification, value, and description by utilizing an array of objects

Utilizing React with Typescript, I am tasked with constructing the React component MultiSelectCheckBoxes by supplying an array of Objects of type Color to the constructor: The structure for a checkbox list should be like this: const options = [{ "id": ...

How can typescript configurations be imported/exported in a node environment?

I'm encountering difficulties while trying to set up a TypeScript node project and import modules: Below is the structure of my project: /build /src main.ts ...

Ways to retrieve particular items/properties from the JSON response string

Is there a way to extract and display only the first 3 items from my JSON data instead of the entire string? Below is the code I am currently using to loop through and display my records: <tr v-for="(list) in myData" v-bind:key="list.ema ...

When running on localhost, IE11 only shows a white screen while the other browsers function properly

I have recently completed a web-based project and successfully deployed it. The project is running on port 8080. Upon testing in Chrome, Safari, and Firefox, the project functions without any issues, and no errors are displayed in the console. However, wh ...

Is there a way to update JSON data through a post request without it getting added to the existing data?

Hello, I am currently delving into Angular2 and encountering a problem concerning RestAPI. When I send a post request to the server with a JSON file, I intend to update the existing data in the JSON file; however, what actually happens is that the new data ...

How to efficiently register services for multiple instances in Angular

Currently, my service includes a field that represents a ViewContainerRef and a method to set the value of this field. @Injectable({ providedIn: 'root' }) export class SomeService { public viewContainerRef: ViewContainerRef setViewContaine ...

Typescript compiler reconciles with npm linked node packages

Encountering an Issue: I am facing errors when performing type checking on my application using tsc, particularly with modules connected via npm link. Below is the command I use for type-checking: "type-check": "tsc --noEmit -p tsconfig.json" and here ...

Is there a way for me to retrieve the bodyHeight attribute of ag-grid using public variables or data?

Working on a project using ag-grid community react. The main feature is a scrollable section filled with data, which can range from one piece to millions of pieces. I'm also incorporating a footer component for the grid that needs to adjust its height ...

Angular: Ensuring Paypal button does not display twice without a hard reload

I have encountered an issue with a PayPal payment button on my page. The button displays fine when I generate it for the first time, but if I try to generate it again for another order, it doesn't show up. I have to hard reload the page to get it to w ...

The art of expanding Angular's HTTP client functionality

I understand that the following code is not valid in Angular, but I am using it for visual demonstration purposes. My goal is to enhance the Angular HTTP client by adding custom headers. I envision creating a class like this, where I extend the Angular h ...

Guide to incorporating @types/module with the corresponding npm module that has type definitions available

This is an example of a module I am currently utilizing in my project. I have come across TypeScript type definitions for the npm module polylabel, which can be found at https://github.com/mapbox/polylabel. When I run npm install --save @types/polylabel, ...

Error in Typescript with Ionic 2 involving the MapSubscriber, lack of helpful error information

Greetings to all who come across this message. I am currently developing an app using Ionic2 where users can send image data to the server. If the server successfully receives the data, a confirmation message is displayed; otherwise, an error notification ...

Show the Array List in a left-to-right format

Is there a way to display an array list from left to right with a div scroll, instead of top to bottom? I am having trouble achieving this. Here is my demo code for reference. HTML <div> <h2 class="ylet-primary-500 alignleft">Sessions</h ...

Error: Typescript error at line 18 in app.ts - Cannot locate the 'server' namespace

Check out this straightforward code snippet: "use strict"; import * as express from "express"; class Server { public app: express.Application; public static start(): Server { return new Server(); } constructor() { this. ...