代码之家  ›  专栏  ›  技术社区  ›  Ashley Davis

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

  •  1
  • Ashley Davis  · 技术社区  · 6 年前

    我正在我的应用程序中实现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 回复  |  直到 6 年前
        1
  •  10
  •   David Sherret    4 年前

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

    1. 实施 ts.CompilerHost 方法在哪里 fileExists , readFile , directoryExists , getDirectories()
    2. lib 根据需要将文件放入内存中的文件系统(例如。 lib.dom.d.ts
    3. 创建一个程序(使用 ts.createProgram )把你的习惯传下去 编译主机 .
    4. 呼叫 ts.getPreEmitDiagnostics(program) 获取诊断信息。

    不完美的例子

    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库文件。

    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
    
        2
  •  3
  •   Ashley Davis    6 年前

    我在David Sherret的一些原始帮助和Fabian Pirklbauer的提示的基础上解决了这个问题( creator of TypeScript Playground ).

    我创建了一个代理编译器主机来包装一个真正的编译器主机。代理能够返回内存中的TypeScript代码进行编译。底层real 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 .