Search code examples
typescriptabstract-syntax-treets-node

When using ts-node, how to manipulate Typescript AST at runtime and execute it?


Im executing a ts file with:

npx ts-node ./tinker.ts

The file reads and parses the AST of another file sample.ts containing a single line:

console.log(123)

Next it should execute that code, but manipulate it before doing so - for example I want to change 123 to 1337.

So, the final result of running npx ts-node ./tinker.ts should be that 1337 is printed in my terminal. Please see my draft below. The code comments is the part that I could not understand how to do.

sample.ts

console.log(123);

tinker.ts

import * as fs from "fs";
const ts = require("typescript");
const path = "./sample.ts";
const code = fs.readFileSync(path, "utf-8"); // "console.log(123)"

const node = ts.createSourceFile("TEMP.ts", code, ts.ScriptTarget.Latest);

let logStatement = node.statements.at(0);
logStatement.expression.arguments.at(0).text = "1337";

// execute the manipulated code!
// expect to see 1337 logged!

To reiterate, running npx ts-node ./tinker.ts should log 1337. How can I achieve this?


Solution

  • Possible solution

    The idea of the solution is to perform the following actions:

    1. Create a modified copy of the input TypeScript module.
    2. Dynamically import the created TypeScript module to «evaluate» it.

    Draft example program

    package.json file

    {
      "name": "question-73432934",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "type": "module",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "BSD",
      "dependencies": {
        "ts-morph": "15.1.0",
        "typescript": "4.7.4"
      },
      "devDependencies": {
        "ts-node": "10.9.1"
      }
    }
    

    After updating the file content, please, execute the command:

    $ npm clean-install
    

    tsconfig.json file

    {
      "compilerOptions": {
        "target": "es2017",
        "module": "esnext",
        "moduleResolution": "node",
        "esModuleInterop": true
      },
      "ts-node": {
        "esm": true
      }
    }
    

    sample.ts file

    console.log(123);
    

    tinker.ts file

    import * as fs from "fs";
    import { Project, SyntaxKind, CallExpression } from "ts-morph";
    
    async function createModifiedFile(inputFilePath: string, outputFilePath: string) {
        const code = await fs.promises.readFile(inputFilePath, "utf8");
    
        const project = new Project();
        const sourceFile = project.createSourceFile(
            outputFilePath,
            code,
            { overwrite: true }
        );
    
        const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
        const callExpression = callExpressions[0] as CallExpression;
    
        callExpression.removeArgument(0);
        callExpression.addArgument("1337");
    
        await sourceFile.save();
    };
    
    async function importModuleDynamically(filePath: string) {
        await import(filePath);
    };
    
    await createModifiedFile("sample.ts", "temp.ts");
    await importModuleDynamically("./temp.ts");
    

    Run

    $ npx ts-node tinker.ts
    

    Program output:

    1337
    

    The created TypeScript module:

    $ cat temp.ts 
    console.log(1337);
    

    Additional references

    TypeScript: Source code transformation

    TypeScript: Dynamic module import