在*某些*文件被修改后,在程序实例中重新生成类型的最快途径是什么?

What is the fastest route to regenerate Types in a Program instance after *some* files modified?

我有一个 Program 实例,我在 SourceFile 中修改了几个节点。我想以尽可能高效的方式为修改后的 SourceFile 重新生成类型。 (注意:我发射)

我最初的做法是:

我正在尝试确定是否有更优化的方法来执行此操作。

我调查了使用语言服务的可能性,但似乎 ts.createLanguageService 只是在文件更改时触发了 Program 的重新创建。

我看过的另一条路线是 ts.createWatchProgram。它似乎在实现 isProgramUptoDate() 函数时使用了类似的逻辑,如果文件名已更改,该函数将运行 createProgram 。出于我的目的,看起来两者都会增加不必要的复杂性,并且可能会拖到我原来的路线上。

但是,因为我还没有太多经验,所以我可能遗漏了一些东西。

在花了一天时间进行了一些繁重的性能测试和挖掘之后,我学到了一些东西。

似乎最好的结果可以通过缓存不变 SourceFile 并在可用时从缓存中挂钩 CompilerHost 到 return 来获得。 (最近有人告诉我 LanguageService 以类似的方式工作)

如果您只修改几个文件,这可以极大地提高性能。当我修改了 230 个 TS 文件中的 none 时,Program 一直在 1ms 中重新加载,而没有在提供时提供 oldProgram100ms

但是,即使修改了很多文件,还是有收获的。

对我来说,最大的收获是每个从 CompilerHost 获得的 new SourceFile 都必须遍历其类型再次,这可能会变得昂贵。

所以提高速度的方法有以下几种:

  1. 创建一个 CompilerHost 并挂钩 getSourceFile() 以在缓存映射可用时为 SourceFile 提供服务。

  2. 如果您可以在 ts.transform() 期间不使用 TypeChecker,则在转换之前不要创建 Program 实例。 (使用您的 compilerHost 加载 SourceFile[] 以与 ts.transform() 一起使用)

  3. 在转换过程中,跟踪哪些文件实际被修改并且只更新缓存中的那些文件。

示例代码:

import * as ts from 'typescript';
import * as glob from 'glob';
import {
  CompilerHost, CompilerOptions, HeritageClause, IndexedAccessTypeNode, SourceFile, SyntaxKind, TypeReferenceNode
} from 'typescript';


/* ********************************************************* *
 * Helpers
 * ********************************************************* */

export const nodeIsKind = <T extends ts.Node = never>(node: ts.Node, ...kind: ts.SyntaxKind[]): node is T =>
  kind.some(k => node.kind === k);


/* ********************************************************* *
 * Compiler
 * ********************************************************* */

function createHookedCompilerHost(hostFiles: Map<string, SourceFile>, compilerOptions: CompilerOptions) {
  const host = ts.createCompilerHost(compilerOptions);
  const originalGetSourceFile = host.getSourceFile as Function;

  return Object.assign(host, {
    getSourceFile(fileName: string, languageVersion: ts.ScriptTarget) {
      return hostFiles.get(fileName) || originalGetSourceFile(...arguments);
    }
  });
}

function transformNodes(program: ts.Program) {
  const srcFiles = program.getSourceFiles() as SourceFile[];
  const updatedFiles = new Set<string>();

  const transformer = (context: ts.TransformationContext) => {
    function visit(fileName: string) {
      return (node: ts.Node): ts.Node => {
        /* Ignore these */
        if (nodeIsKind<HeritageClause>(node, SyntaxKind.HeritageClause)) return node;

        /* Wrap in tuple */
        if (nodeIsKind<TypeReferenceNode>(node, SyntaxKind.TypeReference) || nodeIsKind<IndexedAccessTypeNode>(node, SyntaxKind.IndexedAccessType)) {
          updatedFiles.add(fileName); // Mark file as modified
          return ts.createTupleTypeNode([ node ]);
        }

        return ts.visitEachChild(node, visit(fileName), context);
      }
    }

    return (sourceFile: ts.SourceFile) => ts.visitNode(sourceFile, visit(sourceFile.fileName));
  };

  const { transformed } = ts.transform(srcFiles, [ transformer ], program.getCompilerOptions());
  return transformed.filter(sourceFile => updatedFiles.has(sourceFile.fileName));
}


/* ********************************************************* *
 * Config
 * ********************************************************* */

const fileNames = glob.sync('./test/assets/**/*.ts');

const compilerOptions = {
  noEmit: true,
  target: ts.ScriptTarget.ES5,
  module: ts.ModuleKind.CommonJS,
  strictNullChecks: false,
};


/* ********************************************************* *
 * Main
 * ********************************************************* */

/* Setup Host & Program */
const hostFiles = new Map<string, SourceFile>();
const host = createHookedCompilerHost(hostFiles, compilerOptions);

/* Load SourceFiles */
const sourceFiles = fileNames.map(fileName => host.getSourceFile(fileName, compilerOptions.target));

/* Build Program */
let program = ts.createProgram(fileNames, compilerOptions, host);

/* Pre-cache sourceFiles */
program.getSourceFiles().forEach(srcFile => hostFiles.set(srcFile.fileName, srcFile));

/* Transform files & update affected SourceFiles */
const transformed = transformNodes(program);
for (const sourceFile of transformed)
  hostFiles.set(
    sourceFile.fileName,
    ts.createSourceFile(sourceFile.fileName, ts.createPrinter().printFile(sourceFile), sourceFile.languageVersion)
  );

/* Re-generate Program */
program = ts.createProgram(fileNames, compilerOptions, host);

/* Do what you need with the TypeChecker here */

如果您有任何疑问或需要帮助,请随时发表评论。