Utilizing TypeScript custom transformers with the ts.createWatchProgram function can enhance the functionality

Implementing watch/compile in TypeScript can be achieved using various high-level APIs, such as:

Is it possible to use any of these with custom transformers?

A comment on solutionBuilder.getNextInvalidatedProject() mentions the ability to pass transformers, but notes that it cannot be used with watchers.

In essence, I am looking for a way via API to run the TypeScript compiler in --watch mode while also incorporating my custom transformers. Any suggestions?

Answer №1

Improved method that closely follows the recommendation of solutionBuilder.getNextInvalidatedProject().

By manually invoking solutionBuilder.getNextInvalidatedProject() .emit(...), you have the ability to include transformers. This action signifies that the build process is complete, and it will not emit again in its standard uncustomized manner.

You should utilize this method both before the initial build() step and within the WatchStatusReporter callback.

This approach allows you to introduce custom transformers while preserving the built-in watching mechanism. You can see this strategy in action with the proof-of-concept script provided below:

Below is the complete code snippet; also view it on Gist and Repl.it.

// @ts-check

var ts = require('typescript');

var tsconfig_json = JSON.stringify({
  compilerOptions: {
    outFile: __filename + '.out.js',
    allowJs: true,
    checkJs: true,
    target: 'es3'
  },
  files: [__filename]
}, null, 2);

var s = {
  delete: 3
};

/** @type {import('typescript').System} */
var sysOverride = {};
for (var k in ts.sys) { sysOverride[k] = ts.sys[k]; }
sysOverride.readFile = function (file) {
  if (ts.sys.resolvePath(file) === ts.sys.resolvePath(__dirname + '/tsconfig.json')) {
    // console.log('readFile(', file, ') -> overridden tsconfig_json');
    return tsconfig_json;
  }
  else {
    var result = ts.sys.readFile(file);
    // if (!/node_modules/.test(file))
    //   console.log('readFile(', file, ') -> ' + (typeof result === 'string' ? '"' + result.length + '"' : typeof result));
    return result;
  }
};
sysOverride.writeFile = function (file, content) {
  console.log('  sys.writeFile(', file, ', [', content.length, '])');
  ts.sys.writeFile(file, content);
};

var host = ts.createSolutionBuilderWithWatchHost(
  sysOverride,
  void 0,
  reportDiag,
  reportDiag,
  reportWatch);

var buildStart = Date.now();

var solutionBuilder = ts.createSolutionBuilderWithWatch(
  host,
  [__dirname],
  { incremental: false }, {});

initiateFirstBuild();


function initiateFirstBuild() {
  var firstBuild = solutionBuilder.getNextInvalidatedProject();
  if (firstBuild) {
    buildStart = Date.now();
    startBuild(firstBuild);
  }

  solutionBuilder.build();
}

/**
 * @param {import('typescript').InvalidatedProject<import('typescript').EmitAndSemanticDiagnosticsBuilderProgram>} proj
 * @param {import('typescript').Diagnostic=} watchDiag 
 */
function startBuild(proj, watchDiag) {
  ts.sys.write(
    '\x1b[93m ' + (ts.InvalidatedProjectKind[proj.kind] + '          ').slice(0, 10) + '\x1b[0m' +
    (watchDiag ? '' : '\n'));

  if (watchDiag) reportDiag(watchDiag);

  buildStart = Date.now();

  if (proj && proj.kind === ts.InvalidatedProjectKind.Build) {
    progSource = proj;
    proj.emit(
      void 0,
      void 0,
      void 0,
      void 0,
      { after: [transformInjectStatementNumbers] });
  }

}


function completeBuild(watchDiag) {
  ts.sys.write('\x1b[90m ' + (((Date.now() - buildStart) / 1000) + 's        ').slice(0, 10) + '\x1b[0m');
  if (watchDiag) reportDiag(watchDiag);
}

/** @type {import('typescript').FormatDiagnosticsHost} */
var diagHost;
/** @param {import('typescript').Diagnostic} diag */
function reportDiag(diag) {
  if (!diagHost) {
    diagHost = {
      getCanonicalFileName: function (fileName) {
        return ts.sys.resolvePath(fileName)
      },
      getCurrentDirectory: function () {
        return ts.sys.getCurrentDirectory();
      },
      getNewLine: function () {
        return ts.sys.newLine;
      }
    };
  }

  var output = ts.sys.writeOutputIsTTY && ts.sys.writeOutputIsTTY() ?
    ts.formatDiagnosticsWithColorAndContext([diag], diagHost) :
    ts.formatDiagnostic(diag, diagHost);

  output = output.replace(/^[\r\n]+/, '').replace(/[\r\n]+$/, '');

  ts.sys.write(output + '\n');
}

/** @param {import('typescript').Diagnostic} diag */
function reportWatch(diag) {
  var proj = solutionBuilder.getNextInvalidatedProject();
  if (proj && /** @type {*} */(proj).getProgram) {
    progSource = /** @type {*} */(proj);
  }

  if (proj)
    startBuild(proj, diag);
  else
    completeBuild(diag);
}


/** @type {{ getProgram(): import('typescript').Program }} */
var progSource;
/** @type {import('typescript').TypeChecker} */
var checker;
/** @param {import('typescript').TransformationContext} context */
function transformInjectStatementNumbers(context) {
  checker = progSource.getProgram().getTypeChecker();
  return transformFile;

  function transformFile(sourceFile) {
    console.log('   transforming(', sourceFile.fileName, ')...');
    return ts.updateSourceFileNode(
      sourceFile,
      sourceFile.statements.map(decorateStatementWithComplexityAndType));
  }
}

/**
 * @param {import('typescript').Statement} statement 
 */
function decorateStatementWithComplexityAndType(statement) {
  var nodeCount = 0;
  var type;
  ts.forEachChild(statement, visitStatementChild);

  return ts.addSyntheticLeadingComment(
    statement, ts.SyntaxKind.SingleLineCommentTrivia,
    ' INJECTED >> complexity: ' + nodeCount +
    (!type ? '' : ' : ' + checker.typeToString(type)));

  /**
   * @param {import('typescript').Node} child 
   */
  function visitStatementChild(child) {
    nodeCount++;
    if (!type) type = checker.getTypeAtLocation(child);
    if (type.getFlags() === ts.TypeFlags.Any) type = null;
    ts.forEachChild(child, visitStatementChild);
  }
}

Answer №2

UPDATE: find a more effective solution using

createSolutionBuilderWithWatchHost

To achieve this, you can intercept the createProgram function and override its emit method.

Below is a proof of concept script with a demonstration. The custom transform provided in the script enhances each top-level statement with AST statistics and types.

https://i.sstatic.net/2uiWc.gif

Repl.IT        https://repl.it/@OlegMihailik/custom-transformers-tscreateWatchProgram#index.js GitHub gist https://gist.github.com/mihailik/11369fd2b5e0603a14bc5d883d47dd6c/6a505999a8bfcc25bbcac8befd6c4060591bf4e7

var ts = require('typescript');
var diagReport = ts.createBuilderStatusReporter(ts.sys, ts.sys.writeOutputIsTTY && ts.sys.writeOutputIsTTY());

var host = ts.createWatchCompilerHost(
  [__filename],
  { allowJs: true, checkJs: true, outFile: __filename + '.out.js' },
  ts.sys,
  createAndPatchProgram,
  void 0,
  reportWatch);

var buildStart = Date.now();

ts.createWatchProgram(host);

function reportWatch(diag, newLine, options, errorCount) {
  ts.sys.write(typeof errorCount === 'number' ?
    '\x1b[90m ' + (((Date.now() - buildStart) / 1000) + 's        ').slice(0, 10) + '\x1b[0m' :
    'WATCH DIAG ');
  diagReport(diag);

  if (typeof errorCount !== 'number') buildStart = Date.now();
}

var prog;
function createAndPatchProgram() {
  prog = ts.createEmitAndSemanticDiagnosticsBuilderProgram.apply(ts, arguments);
  prog.__oldEmit = prog.emit;
  prog.emit = overrideEmit;
  return prog;
}

function overrideEmit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) {
  return this.__oldEmit(
    targetSourceFile,
    writeFile,
    cancellationToken,
    emitOnlyDtsFiles,
    { after: [transformInjectStatementNumbers] }
  );
}

var checker;
/** @param {import('typescript').TransformationContext} context */
function transformInjectStatementNumbers(context) {
  checker = prog.getProgram().getTypeChecker();
  return transformFile;

  function transformFile(sourceFile) {
    return ts.updateSourceFileNode(
      sourceFile,
      sourceFile.statements.map(decorateStatementWithComplexityAndType));
  }
}

function decorateStatementWithComplexityAndType(statement) {
  var nodeCount = 0;
  var type;
  ts.forEachChild(statement, visitStatementChild);

  return ts.addSyntheticLeadingComment(
    statement, ts.SyntaxKind.SingleLineCommentTrivia,
    ' complexity: ' + nodeCount +
    (!type ? '' : ' : ' + checker.typeToString(type)));

  function visitStatementChild(child) {
    nodeCount++;
    if (!type) type = checker.getTypeAtLocation(child);
    if (type.getFlags() === ts.TypeFlags.Any) type = null;
    ts.forEachChild(child, visitStatementChild);
  }
}

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

Can you provide instructions on how to make a fixed-length array in Typescript?

Can I define a fixed-length array property in Typescript? For example: //example code , not my actual case but similar export type Car = { doors:Door[]; //I want this to be exactly 4 doors /// rest of code } I attempted the following: export type Pat ...

Angular error message: Trying to access the property 'name' of an undefined object leads to a TypeError

I'm having trouble phrasing this question differently, but I am seeking assistance in comprehending how to address this issue. The error message I am encountering is as follows: TypeError: _co.create is not a function TypeError: Cannot read property ...

Exploring the synergy between Visual Studio 2015 and Angular2 Beta 2's dependency injection functionality

Currently, I am using VS2015 alongside TypeScript 1.7.3 and Angular2 Beta 2 with a target of ECMA 5. In order to enable experimental decorators in my project, I have made modifications to the csproj file by including TypeScriptExperimentalDecorators = true ...

Instructions for setting 0 as a valid value in Html code and displaying it

I have a query regarding HTML code within an Angular app. My inquiry is, is there an alternative method to check for null or undefined values in an ngIf statement? The code I am working with looks like this: <div ngif= "value !== null and value ! ...

Ways to resolve typing mistakes in a JavaScript Library

Currently, I am utilizing the Leaflet Library (http://leafletjs.com) within a TypeScript project. Within this project, I am showcasing markers on a map which are configured using options detailed here: http://leafletjs.com/reference-1.3.0.html#marker-l-ma ...

React-Redux button unit test in Vitest encounters failure

I'm struggling with passing a button click test in my app component using Vitest, react-testing-library, and jest dom. As a newcomer to unit testing, I'm having difficulty figuring out how to make my test for the submit button in my form function ...

Typescript Algorithm - Node Tree: A unique approach combining algorithmic concepts and

I am dealing with a json data in raw format that is unsorted. Here is a snippet of the data: [ { "level": 1, "id": 34, "name": "example-name", "father_id": 10 }, ... ] My goal is to o ...

I'm encountering an error stating that a property does not exist for Twilio.Response() while attempting to convert a Twilio CORS Node.js example to TypeScript. Can anyone shed

In my current project, I am converting a CORS Node.js example from Twilio's documentation into TypeScript. Here is the original Node.js code: exports.handler = (context, event, callback) => { const client = context.getTwilioClient(); const resp ...

Validating Forms in TypeScript

Currently in the process of learning Angular 10, but encountering a challenge I have an HTML document that validates a form group in my component. When I set a value for a textbox from my component, the value is displayed correctly, but my submit button c ...

Generate a unique identifier based on data type

Is it feasible to create a function signature based on type in this manner? I have successfully created a function signature as shown below. type Data = { update: boolean }; type Distribution<T> = { on: <K extends keyof T>(key: K, list ...

Facing unexpected behavior with rxjs merge in angular5

import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/merge'; this.data = this.itemsCollection.valueChanges() this.foo = this.afs.collection<Item>('products') .doc('G2loKLqNQJUQIsDmzSNahlopOyk ...

CDK Error: Unable to locate MethodResponse in AWS API Gateway configuration

I'm facing an issue in vscode while trying to access the MethodResponse interface from apigateway. Unfortunately, I'm getting an error message: The type 'typeof import(".../node_modules/aws-cdk-lib/aws-apigateway/index")' d ...

Angular component initialization problems

Hey everyone, I'm encountering a small issue. I am attempting to retrieve some images from Flickr using their API, but for an unknown reason, the list is not populating at the desired time. Below is my code for better clarification. This is the code ...

The data is not being displayed in the table

I am encountering an issue while attempting to populate the table with data received through props by looping over it. Unfortunately, the data is not rendering on the UI :( However, when I manually input data, it does show up. Below is my code: Code for P ...

Incorporating a new attribute into the JQueryStatic interface

I am trying to enhance the JQueryStatic interface by adding a new property called someString, which I intend to access using $.someString. Within my index.ts file, I have defined the following code: interface JQueryStatic { someString: string; } $.s ...

What is the best way to assign default values when destructuring interfaces within interfaces in TypeScript?

My goal here is to create a function that can be used with or without arguments. If arguments are provided, it should work with those values; if not, default values should be used. The issue I'm facing is that although there are no TypeScript errors ...

Can the tag function arguments be employed to generate an identical sequence of literals and placeholders?

A section from the tutorial on template strings at this link includes an interesting example: var say = "a bird in hand > two in the bush"; var html = htmlEscape `<div> I would just like to say : ${say}</div>`; // A basic tag function func ...

Tips for finding the displayRows paragraph within the MUI table pagination, nestled between the preceding and succeeding page buttons

Incorporating a Material-UI table pagination component into my React application, I am striving to position the text that indicates the current range of rows between the two action buttons (previous and next). <TablePagination ...

Issues with command functionality within the VS Code integrated terminal (Bash) causing disruptions

When using Visual Studio Code's integrated terminal with bash as the shell, I have noticed that commands like ng and tsc are not recognized. Can anyone shed some light on why this might be happening? ...

Transform a string into an array using Angular 2 and TypeScript

JSON.stringify(this.p) console.log(this.p + " " + typeof(this.p)) When I execute these commands, the output is: [{lat:52.52193980072258,lng:13.401432037353516},{lat:52.52319316685915,lng:13.407096862792969},{lat:52.51969409696076,lng:13.407225608825684}] ...