如何键入检查内存中的 TypeScript 代码片段?
How do I type check a snippet of TypeScript code in memory?
我正在我的应用程序中实现 TypeScript 支持Data-Forge Notebook。
我需要编译、类型检查和评估 TypeScript 代码片段。
编译好像没问题,我正在使用如下图的transpileModule
将一小段TS代码转换成JavaScript可以计算的代码:
import { transpileModule, TranspileOptions } from "typescript";
const transpileOptions: TranspileOptions = {
compilerOptions: {},
reportDiagnostics: true,
};
const tsCodeSnippet = " /* TS code goes here */ ";
const jsOutput = transpileModule(tsCodeSnippet, transpileOptions);
console.log(JSON.stringify(jsOutput, null, 4));
但是我尝试编译TS代码时出现问题
例如,以下函数有类型错误,但它在没有任何错误诊断的情况下被转译:
function foo(): string {
return 5;
}
转译很棒,但我也希望能够向我的用户显示错误。
所以我的问题是如何做到这一点,同时进行类型检查并为语义错误产生错误?
请注意,我不想将 TypeScript 代码保存到文件中。这对我的应用程序来说是不必要的性能负担。我只想编译和键入保存在内存中的代码检查片段。
情况 1 - 仅使用内存 - 无法访问文件系统(例如在网络上)
这不是一项简单的任务,可能需要一些时间才能完成。也许有更简单的方法,但我还没有找到。
- 实施
ts.CompilerHost
,其中 fileExists
、readFile
、directoryExists
、getDirectories()
等方法从内存而不是实际文件系统读取.
- 根据需要将适当的 lib 文件加载到内存文件系统中(例如 lib.es6.d.ts 或 lib.dom.d.ts).
- 也将您的内存文件添加到内存文件系统。
- 创建一个程序(使用
ts.createProgram
)并传入您的自定义 ts.CompilerHost
。
- 致电
ts.getPreEmitDiagnostics(program)
获取诊断信息。
不完美的例子
这是一个不完美的简短示例,它没有正确实现内存文件系统并且没有加载 lib 文件(所以会有全局诊断错误......这些可以忽略或者你可以调用特定方法 program
而不是 program.getGlobalDiagnostics()
。请注意 ts.getPreEmitDiagnostics
here):
的行为
import * as ts from "typescript";
console.log(getDiagnosticsForText("const t: number = '';").map(d => d.messageText));
function getDiagnosticsForText(text: string) {
const dummyFilePath = "/file.ts";
const textAst = ts.createSourceFile(dummyFilePath, text, ts.ScriptTarget.Latest);
const options: ts.CompilerOptions = {};
const host: ts.CompilerHost = {
fileExists: filePath => filePath === dummyFilePath,
directoryExists: dirPath => dirPath === "/",
getCurrentDirectory: () => "/",
getDirectories: () => [],
getCanonicalFileName: fileName => fileName,
getNewLine: () => "\n",
getDefaultLibFileName: () => "",
getSourceFile: filePath => filePath === dummyFilePath ? textAst : undefined,
readFile: filePath => filePath === dummyFilePath ? text : undefined,
useCaseSensitiveFileNames: () => true,
writeFile: () => {}
};
const program = ts.createProgram({
options,
rootNames: [dummyFilePath],
host
});
return ts.getPreEmitDiagnostics(program);
}
情况 2 - 访问文件系统
如果您可以访问文件系统,那么这就容易多了,您可以使用类似于下面的函数:
import * as path from "path";
function getDiagnosticsForText(
rootDir: string,
text: string,
options?: ts.CompilerOptions,
cancellationToken?: ts.CancellationToken
) {
options = options || ts.getDefaultCompilerOptions();
const inMemoryFilePath = path.resolve(path.join(rootDir, "__dummy-file.ts"));
const textAst = ts.createSourceFile(inMemoryFilePath, text, options.target || ts.ScriptTarget.Latest);
const host = ts.createCompilerHost(options, true);
overrideIfInMemoryFile("getSourceFile", textAst);
overrideIfInMemoryFile("readFile", text);
overrideIfInMemoryFile("fileExists", true);
const program = ts.createProgram({
options,
rootNames: [inMemoryFilePath],
host
});
return ts.getPreEmitDiagnostics(program, textAst, cancellationToken);
function overrideIfInMemoryFile(methodName: keyof ts.CompilerHost, inMemoryValue: any) {
const originalMethod = host[methodName] as Function;
host[methodName] = (...args: unknown[]) => {
// resolve the path because typescript will normalize it
// to forward slashes on windows
const filePath = path.resolve(args[0] as string);
if (filePath === inMemoryFilePath)
return inMemoryValue;
return originalMethod.apply(host, args);
};
}
}
// example...
console.log(getDiagnosticsForText(
__dirname,
"import * as ts from 'typescript';\n const t: string = ts.createProgram;"
));
这样做,编译器将在提供的 rootDir
中搜索 node_modules
文件夹并使用其中的类型(它们不需要以其他方式加载到内存中) ).
更新:最简单的解决方案
我创建了一个名为 @ts-morph/bootstrap 的库,它使编译器 API 的设置变得更加容易。即使使用内存文件系统,它也会为您加载 TypeScript lib 文件。
import { createProject, ts } from "@ts-morph/bootstrap";
const project = await createProject({ useInMemoryFileSystem: true });
const myClassFile = project.createSourceFile(
"MyClass.ts",
"export class MyClass { prop: string; }",
);
const program = project.createProgram();
ts.getPreEmitDiagnostics(program); // check these
我已经根据 David Sherret 的一些原始帮助以及 Fabian Pirklbauer (creator of TypeScript Playground) 的提示解决了这个问题。
我创建了一个代理 CompilerHost 来包装一个真正的 CompilerHost。该代理能够返回内存中的 TypeScript 代码进行编译。底层真正的 CompilerHost 能够加载默认的 TypeScript 库。这些库是必需的,否则您会收到大量与内置 TypeScript 数据类型相关的错误。
代码
import * as ts from "typescript";
//
// A snippet of TypeScript code that has a semantic/type error in it.
//
const code
= "function foo(input: number) {\n"
+ " console.log('Hello!');\n"
+ "};\n"
+ "foo('x');"
;
//
// Result of compiling TypeScript code.
//
export interface CompilationResult {
code?: string;
diagnostics: ts.Diagnostic[]
};
//
// Check and compile in-memory TypeScript code for errors.
//
function compileTypeScriptCode(code: string, libs: string[]): CompilationResult {
const options = ts.getDefaultCompilerOptions();
const realHost = ts.createCompilerHost(options, true);
const dummyFilePath = "/in-memory-file.ts";
const dummySourceFile = ts.createSourceFile(dummyFilePath, code, ts.ScriptTarget.Latest);
let outputCode: string | undefined = undefined;
const host: ts.CompilerHost = {
fileExists: filePath => filePath === dummyFilePath || realHost.fileExists(filePath),
directoryExists: realHost.directoryExists && realHost.directoryExists.bind(realHost),
getCurrentDirectory: realHost.getCurrentDirectory.bind(realHost),
getDirectories: realHost.getDirectories.bind(realHost),
getCanonicalFileName: fileName => realHost.getCanonicalFileName(fileName),
getNewLine: realHost.getNewLine.bind(realHost),
getDefaultLibFileName: realHost.getDefaultLibFileName.bind(realHost),
getSourceFile: (fileName, languageVersion, onError, shouldCreateNewSourceFile) => fileName === dummyFilePath
? dummySourceFile
: realHost.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile),
readFile: filePath => filePath === dummyFilePath
? code
: realHost.readFile(filePath),
useCaseSensitiveFileNames: () => realHost.useCaseSensitiveFileNames(),
writeFile: (fileName, data) => outputCode = data,
};
const rootNames = libs.map(lib => require.resolve(`typescript/lib/lib.${lib}.d.ts`));
const program = ts.createProgram(rootNames.concat([dummyFilePath]), options, host);
const emitResult = program.emit();
const diagnostics = ts.getPreEmitDiagnostics(program);
return {
code: outputCode,
diagnostics: emitResult.diagnostics.concat(diagnostics)
};
}
console.log("==== Evaluating code ====");
console.log(code);
console.log();
const libs = [ 'es2015' ];
const result = compileTypeScriptCode(code, libs);
console.log("==== Output code ====");
console.log(result.code);
console.log();
console.log("==== Diagnostics ====");
for (const diagnostic of result.diagnostics) {
console.log(diagnostic.messageText);
}
console.log();
输出
==== Evaluating code ====
function foo(input: number) {
console.log('Hello!');
};
foo('x');
=========================
Diagnosics:
Argument of type '"x"' is not assignable to parameter of type 'number'.
我正在我的应用程序中实现 TypeScript 支持Data-Forge Notebook。
我需要编译、类型检查和评估 TypeScript 代码片段。
编译好像没问题,我正在使用如下图的transpileModule
将一小段TS代码转换成JavaScript可以计算的代码:
import { transpileModule, TranspileOptions } from "typescript";
const transpileOptions: TranspileOptions = {
compilerOptions: {},
reportDiagnostics: true,
};
const tsCodeSnippet = " /* TS code goes here */ ";
const jsOutput = transpileModule(tsCodeSnippet, transpileOptions);
console.log(JSON.stringify(jsOutput, null, 4));
但是我尝试编译TS代码时出现问题
例如,以下函数有类型错误,但它在没有任何错误诊断的情况下被转译:
function foo(): string {
return 5;
}
转译很棒,但我也希望能够向我的用户显示错误。
所以我的问题是如何做到这一点,同时进行类型检查并为语义错误产生错误?
请注意,我不想将 TypeScript 代码保存到文件中。这对我的应用程序来说是不必要的性能负担。我只想编译和键入保存在内存中的代码检查片段。
情况 1 - 仅使用内存 - 无法访问文件系统(例如在网络上)
这不是一项简单的任务,可能需要一些时间才能完成。也许有更简单的方法,但我还没有找到。
- 实施
ts.CompilerHost
,其中fileExists
、readFile
、directoryExists
、getDirectories()
等方法从内存而不是实际文件系统读取. - 根据需要将适当的 lib 文件加载到内存文件系统中(例如 lib.es6.d.ts 或 lib.dom.d.ts).
- 也将您的内存文件添加到内存文件系统。
- 创建一个程序(使用
ts.createProgram
)并传入您的自定义ts.CompilerHost
。 - 致电
ts.getPreEmitDiagnostics(program)
获取诊断信息。
不完美的例子
这是一个不完美的简短示例,它没有正确实现内存文件系统并且没有加载 lib 文件(所以会有全局诊断错误......这些可以忽略或者你可以调用特定方法 program
而不是 program.getGlobalDiagnostics()
。请注意 ts.getPreEmitDiagnostics
here):
import * as ts from "typescript";
console.log(getDiagnosticsForText("const t: number = '';").map(d => d.messageText));
function getDiagnosticsForText(text: string) {
const dummyFilePath = "/file.ts";
const textAst = ts.createSourceFile(dummyFilePath, text, ts.ScriptTarget.Latest);
const options: ts.CompilerOptions = {};
const host: ts.CompilerHost = {
fileExists: filePath => filePath === dummyFilePath,
directoryExists: dirPath => dirPath === "/",
getCurrentDirectory: () => "/",
getDirectories: () => [],
getCanonicalFileName: fileName => fileName,
getNewLine: () => "\n",
getDefaultLibFileName: () => "",
getSourceFile: filePath => filePath === dummyFilePath ? textAst : undefined,
readFile: filePath => filePath === dummyFilePath ? text : undefined,
useCaseSensitiveFileNames: () => true,
writeFile: () => {}
};
const program = ts.createProgram({
options,
rootNames: [dummyFilePath],
host
});
return ts.getPreEmitDiagnostics(program);
}
情况 2 - 访问文件系统
如果您可以访问文件系统,那么这就容易多了,您可以使用类似于下面的函数:
import * as path from "path";
function getDiagnosticsForText(
rootDir: string,
text: string,
options?: ts.CompilerOptions,
cancellationToken?: ts.CancellationToken
) {
options = options || ts.getDefaultCompilerOptions();
const inMemoryFilePath = path.resolve(path.join(rootDir, "__dummy-file.ts"));
const textAst = ts.createSourceFile(inMemoryFilePath, text, options.target || ts.ScriptTarget.Latest);
const host = ts.createCompilerHost(options, true);
overrideIfInMemoryFile("getSourceFile", textAst);
overrideIfInMemoryFile("readFile", text);
overrideIfInMemoryFile("fileExists", true);
const program = ts.createProgram({
options,
rootNames: [inMemoryFilePath],
host
});
return ts.getPreEmitDiagnostics(program, textAst, cancellationToken);
function overrideIfInMemoryFile(methodName: keyof ts.CompilerHost, inMemoryValue: any) {
const originalMethod = host[methodName] as Function;
host[methodName] = (...args: unknown[]) => {
// resolve the path because typescript will normalize it
// to forward slashes on windows
const filePath = path.resolve(args[0] as string);
if (filePath === inMemoryFilePath)
return inMemoryValue;
return originalMethod.apply(host, args);
};
}
}
// example...
console.log(getDiagnosticsForText(
__dirname,
"import * as ts from 'typescript';\n const t: string = ts.createProgram;"
));
这样做,编译器将在提供的 rootDir
中搜索 node_modules
文件夹并使用其中的类型(它们不需要以其他方式加载到内存中) ).
更新:最简单的解决方案
我创建了一个名为 @ts-morph/bootstrap 的库,它使编译器 API 的设置变得更加容易。即使使用内存文件系统,它也会为您加载 TypeScript lib 文件。
import { createProject, ts } from "@ts-morph/bootstrap";
const project = await createProject({ useInMemoryFileSystem: true });
const myClassFile = project.createSourceFile(
"MyClass.ts",
"export class MyClass { prop: string; }",
);
const program = project.createProgram();
ts.getPreEmitDiagnostics(program); // check these
我已经根据 David Sherret 的一些原始帮助以及 Fabian Pirklbauer (creator of TypeScript Playground) 的提示解决了这个问题。
我创建了一个代理 CompilerHost 来包装一个真正的 CompilerHost。该代理能够返回内存中的 TypeScript 代码进行编译。底层真正的 CompilerHost 能够加载默认的 TypeScript 库。这些库是必需的,否则您会收到大量与内置 TypeScript 数据类型相关的错误。
代码
import * as ts from "typescript";
//
// A snippet of TypeScript code that has a semantic/type error in it.
//
const code
= "function foo(input: number) {\n"
+ " console.log('Hello!');\n"
+ "};\n"
+ "foo('x');"
;
//
// Result of compiling TypeScript code.
//
export interface CompilationResult {
code?: string;
diagnostics: ts.Diagnostic[]
};
//
// Check and compile in-memory TypeScript code for errors.
//
function compileTypeScriptCode(code: string, libs: string[]): CompilationResult {
const options = ts.getDefaultCompilerOptions();
const realHost = ts.createCompilerHost(options, true);
const dummyFilePath = "/in-memory-file.ts";
const dummySourceFile = ts.createSourceFile(dummyFilePath, code, ts.ScriptTarget.Latest);
let outputCode: string | undefined = undefined;
const host: ts.CompilerHost = {
fileExists: filePath => filePath === dummyFilePath || realHost.fileExists(filePath),
directoryExists: realHost.directoryExists && realHost.directoryExists.bind(realHost),
getCurrentDirectory: realHost.getCurrentDirectory.bind(realHost),
getDirectories: realHost.getDirectories.bind(realHost),
getCanonicalFileName: fileName => realHost.getCanonicalFileName(fileName),
getNewLine: realHost.getNewLine.bind(realHost),
getDefaultLibFileName: realHost.getDefaultLibFileName.bind(realHost),
getSourceFile: (fileName, languageVersion, onError, shouldCreateNewSourceFile) => fileName === dummyFilePath
? dummySourceFile
: realHost.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile),
readFile: filePath => filePath === dummyFilePath
? code
: realHost.readFile(filePath),
useCaseSensitiveFileNames: () => realHost.useCaseSensitiveFileNames(),
writeFile: (fileName, data) => outputCode = data,
};
const rootNames = libs.map(lib => require.resolve(`typescript/lib/lib.${lib}.d.ts`));
const program = ts.createProgram(rootNames.concat([dummyFilePath]), options, host);
const emitResult = program.emit();
const diagnostics = ts.getPreEmitDiagnostics(program);
return {
code: outputCode,
diagnostics: emitResult.diagnostics.concat(diagnostics)
};
}
console.log("==== Evaluating code ====");
console.log(code);
console.log();
const libs = [ 'es2015' ];
const result = compileTypeScriptCode(code, libs);
console.log("==== Output code ====");
console.log(result.code);
console.log();
console.log("==== Diagnostics ====");
for (const diagnostic of result.diagnostics) {
console.log(diagnostic.messageText);
}
console.log();
输出
==== Evaluating code ====
function foo(input: number) {
console.log('Hello!');
};
foo('x');
=========================
Diagnosics:
Argument of type '"x"' is not assignable to parameter of type 'number'.