在*某些*文件被修改后,在程序实例中重新生成类型的最快途径是什么?
What is the fastest route to regenerate Types in a Program instance after *some* files modified?
我有一个 Program
实例,我在 SourceFile
中修改了几个节点。我想以尽可能高效的方式为修改后的 SourceFile
重新生成类型。 (注意:我不发射)
我最初的做法是:
- 创建一个
CompilerHost
并挂钩 getSourceFile
以允许其提供修改后的 SourceFile
(如果可用)
- 运行 所有
SourceFile
上的转换器,从结果 更新修改后的集合
- 使用相同的 compilerHost(提供更新的文件)重新创建
Program
我正在尝试确定是否有更优化的方法来执行此操作。
我调查了使用语言服务的可能性,但似乎 ts.createLanguageService
只是在文件更改时触发了 Program
的重新创建。
我看过的另一条路线是 ts.createWatchProgram
。它似乎在实现 isProgramUptoDate()
函数时使用了类似的逻辑,如果文件名已更改,该函数将运行 createProgram
。出于我的目的,看起来两者都会增加不必要的复杂性,并且可能会拖到我原来的路线上。
但是,因为我还没有太多经验,所以我可能遗漏了一些东西。
在花了一天时间进行了一些繁重的性能测试和挖掘之后,我学到了一些东西。
似乎最好的结果可以通过缓存不变 SourceFile
并在可用时从缓存中挂钩 CompilerHost
到 return 来获得。 (最近有人告诉我 LanguageService
以类似的方式工作)
如果您只修改几个文件,这可以极大地提高性能。当我修改了 230 个 TS 文件中的 none 时,Program
一直在 1ms
中重新加载,而没有在提供时提供 oldProgram
和 100ms
。
但是,即使修改了很多文件,还是有收获的。
对我来说,最大的收获是每个从 CompilerHost
获得的 new SourceFile
都必须遍历其类型再次,这可能会变得昂贵。
所以提高速度的方法有以下几种:
创建一个 CompilerHost
并挂钩 getSourceFile()
以在缓存映射可用时为 SourceFile
提供服务。
如果您可以在 ts.transform()
期间不使用 TypeChecker
,则在转换之前不要创建 Program
实例。 (使用您的 compilerHost 加载 SourceFile[]
以与 ts.transform()
一起使用)
在转换过程中,跟踪哪些文件实际被修改并且只更新缓存中的那些文件。
示例代码:
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 */
如果您有任何疑问或需要帮助,请随时发表评论。
我有一个 Program
实例,我在 SourceFile
中修改了几个节点。我想以尽可能高效的方式为修改后的 SourceFile
重新生成类型。 (注意:我不发射)
我最初的做法是:
- 创建一个
CompilerHost
并挂钩getSourceFile
以允许其提供修改后的SourceFile
(如果可用) - 运行 所有
SourceFile
上的转换器,从结果 更新修改后的集合
- 使用相同的 compilerHost(提供更新的文件)重新创建
Program
我正在尝试确定是否有更优化的方法来执行此操作。
我调查了使用语言服务的可能性,但似乎 ts.createLanguageService
只是在文件更改时触发了 Program
的重新创建。
我看过的另一条路线是 ts.createWatchProgram
。它似乎在实现 isProgramUptoDate()
函数时使用了类似的逻辑,如果文件名已更改,该函数将运行 createProgram
。出于我的目的,看起来两者都会增加不必要的复杂性,并且可能会拖到我原来的路线上。
但是,因为我还没有太多经验,所以我可能遗漏了一些东西。
在花了一天时间进行了一些繁重的性能测试和挖掘之后,我学到了一些东西。
似乎最好的结果可以通过缓存不变 SourceFile
并在可用时从缓存中挂钩 CompilerHost
到 return 来获得。 (最近有人告诉我 LanguageService
以类似的方式工作)
如果您只修改几个文件,这可以极大地提高性能。当我修改了 230 个 TS 文件中的 none 时,Program
一直在 1ms
中重新加载,而没有在提供时提供 oldProgram
和 100ms
。
但是,即使修改了很多文件,还是有收获的。
对我来说,最大的收获是每个从 CompilerHost
获得的 new SourceFile
都必须遍历其类型再次,这可能会变得昂贵。
所以提高速度的方法有以下几种:
创建一个
CompilerHost
并挂钩getSourceFile()
以在缓存映射可用时为SourceFile
提供服务。如果您可以在
ts.transform()
期间不使用TypeChecker
,则在转换之前不要创建Program
实例。 (使用您的 compilerHost 加载SourceFile[]
以与ts.transform()
一起使用)在转换过程中,跟踪哪些文件实际被修改并且只更新缓存中的那些文件。
示例代码:
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 */
如果您有任何疑问或需要帮助,请随时发表评论。