如何键入检查内存中的TypeScript代码片段?

时间:2018-12-11 22:13:38

标签: typescript typechecking transpiler typescript-compiler-api

我正在我的应用程序 Data-Forge Notebook 中实现TypeScript支持。

我需要编译,键入检查并评估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代码保存到文件中。这将对我的应用程序造成不必要的性能负担。我只想编译并键入保存在内存中的代码检查片段。

2 个答案:

答案 0 :(得分:1)

情况1-仅使用内存-无法访问文件系统(例如,在网络上)

这不是一项简单的任务,可能需要一些时间。也许有一种更简单的方法,但是我还没有找到。

  1. 实施ts.CompilerHost,其中fileExistsreadFiledirectoryExistsgetDirectories()等方法是从内存而不是实际文件系统中读取的。 / li>
  2. 根据需要将相应的lib文件加载到内存文件系统中(例如 lib.es6.d.ts lib.dom.d .ts )。
  3. 将您的内存文件也添加到内存文件系统中。
  4. 创建一个程序(使用ts.createProgram)并传递自定义ts.CompilerHost
  5. 致电ts.getPreEmitDiagnostics(program)进行诊断。

不完美的示例

这是一个不完善的简短示例,该示例未正确实现内存文件系统且未加载lib文件(因此将出现全局诊断错误...这些错误可以忽略,也可以在{{1上调用特定方法}},而不是program。请注意program.getGlobalDiagnostics() here)的行为:

ts.getPreEmitDiagnostics

情况2-访问文件系统

如果您有权访问文件系统,那么这会容易得多,并且可以使用类似于以下功能的功能:

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);
}

通过这种方式,编译器将在提供的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文件夹,并在其中使用键入内容(不需要以其他方式将它们加载到内存中)。

答案 1 :(得分:0)

我已经在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'.

Full working example available on my Github

相关问题