首页 > 解决方案 > 带有 ts.createWatchProgram 的 TypeScript 自定义转换器

问题描述

TypeScript 有几个高级 API 来实现watch/compile, 例如:

它们中的任何一个都可以与定制变压器一起使用吗?

解决方案生成器的评论。getNextInvalidatedProject ()提到了通过转换器的能力,但它不能与观察者一起使用。

基本上,我需要通过 API 在--watch模式下运行 TypeScript 编译器,但传入我的自定义转换器。有什么线索吗?

标签: typescript

解决方案


更好的方法,更符合solutionBuilder。getNextInvalidatedProject()建议。

当您手动调用solutionBuilder.getNextInvalidatedProject() .emit(...)时,您可以传入转换器。调用该 API 将构建标记为已完成,这意味着它不会以通常的非自定义方式再次发出。

您将在初始build()WatchStatusReporter回调之前调用它。

这样,您可以注入自定义转换器,但仍保留内置的观看逻辑。请参阅下面的概念验证脚本:

这是完整代码,另请参阅GistRepl.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);
  }
}


推荐阅读