• Deno 中使用 @typescript/vfs 生成 DTS 文件


    背景

    前段时间开源的 STC 工具,这是一个将 OpenApi 规范的 Swagger/Apifox 文档转换成代码的工具。可以在上一篇(《OpenApi(Swagger)快速转换成 TypeScript 代码 - STC》)随笔里面查看这个工具的介绍和使用。

    为了支持生成 Javascript,近期添加了 JavaScript 插件,并且生成 DTS 文件。实现它有两个设想:

    • 重新写一遍解析 OpenApi 规范的文档数据。
    • 基于 TypeScript 插件生成的 TypeScript 代码字符串,通过编译工具转换成 JavaScript。

    最终选择第二种实现方式,原因也很简单,TypeScript 是 JavaScript 的超集,有着丰富的编译工具(tsc、esbuild、swc、rome 等等)。相比第一种方式起来更简单,出现问题时只需要修改 TypeScript 的转译部分,还能减少多次修改的情况。通过实践,选择 swc 编译 TypeScript 代码 DTS 文件则由 tsc 生成。

    face-1

    代码实现

    首先在 Deno 文档找了一遍,是否有满足需求我们的 Api 提供。看到文档上写着:

    deno bundle

    于是,开始另寻他路。

    在尝试了 esbuild 失败后,决定使用 swc 将 TypeScript 编译成 JavaScript 代码,可是不支持生成 DTS 文件,这还需要用 tsc 来实现。其中比较棘手是 tsc 在 Deno 里面实现(应该是对 TypeScript compiler Api 不熟的原因)。

    通过在网上查阅 TypeScript compiler Api 的使用资料,同时还借助 ChatGPT 的协助,对 TypeScript compiler Api 有了个初步的认识。

    摘自 TypeScript wiki 的示例(从 JavaScript 文件获取 DTS):

    import * as ts from "typescript";
    
    function compile(fileNames: string[], options: ts.CompilerOptions): void {
      // 创建一个带有内存发射的编译程序
      const createdFiles = {}
      const host = ts.createCompilerHost(options); // 创建编译器主机
      host.writeFile = (fileName: string, contents: string) => createdFiles[fileName] = contents // 覆盖写入文件的方法
      
      // 准备并发射类型声明文件
      const program = ts.createProgram(fileNames, options, host);
      program.emit();
    
      // 遍历所有输入文件
      fileNames.forEach(file => {
        console.log("### JavaScript\n")
        console.log(host.readFile(file))
    
        console.log("### Type Definition\n")
        const dts = file.replace(".js", ".d.ts")
        console.log(createdFiles[dts])
      })
    }
    
    // 运行编译器
    compile(process.argv.slice(2), {
      allowJs: true,
      declaration: true,
      emitDeclarationOnly: true,
    });
    

    我想法是直接是用代码字符串生成的方式,不是文件,所以这段示例不能直接应用到我们的代码里面来。结合 ChatGPT 的一些回答和网上的资料,改造如下:

    import ts from "npm:typescript";
    
    const generateDeclarationFile = () => {
      const sourceCode = `
        export type TypeTest = 1 | 0 | 3;
        export interface ISwagger {
          name: string;
          age: number;
          test: string; // Array;
        }
    
        /**
         * Adds two numbers.
         * @param a The first number.
         * @param b The second number.
         * @returns The sum of a and b.
         */
        export function add(a: any, b: any): number {
          return a + b;
        }
      `;
      const filename = "temp.ts";
    
      // 创建一个编译选项对象
      const compilerOptions: ts.CompilerOptions = {
        target: ts.ScriptTarget.ESNext,
        declaration: true,
        emitDeclarationOnly: true,
        lib: ["ESNext"],
      };
    
      let declarationContent = "";
      const sourceFile = ts.createSourceFile(
        filename,
        sourceCode,
        ts.ScriptTarget.ESNext,
        true,
      );
    
      const defaultCompilerHost = ts.createCompilerHost(compilerOptions);
      const host: ts.CompilerHost = {
        getSourceFile: (fileName, languageVersion) => {
          if (fileName === filename) {
            return sourceFile;
          }
          return defaultCompilerHost.getSourceFile(fileName, languageVersion);
        },
        writeFile: (_name, text) => {
          declarationContent = text;
          // console.log(text);
        },
        getDefaultLibFileName: () => "./registry.npmjs.org/typescript/5.1.6/lib/lib.d.ts"
        useCaseSensitiveFileNames: () => true,
        getCanonicalFileName: (fileName) => fileName,
        getCurrentDirectory: () => "",
        getNewLine: () => "\n",
        getDirectories: () => [],
        fileExists: () => true,
        readFile: () => "",
      };
    
      // 创建 TypeScript 编译器实例
      const program = ts.createProgram(
        [filename],
        compilerOptions,
        host,
      );
    
      // 执行编译并处理结果
      const emitResult = program.emit();
    
      if (emitResult.emitSkipped) {
        console.error("Compilation failed");
        const allDiagnostics = ts
          .getPreEmitDiagnostics(program)
          .concat(emitResult.diagnostics);
    
        allDiagnostics.forEach((diagnostic) => {
          if (diagnostic.file) {
            const { line, character } = ts.getLineAndCharacterOfPosition(
              diagnostic.file,
              diagnostic.start!,
            );
            const message = ts.flattenDiagnosticMessageText(
              diagnostic.messageText,
              "\n",
            );
            console.log(
              `${diagnostic.file.fileName} (${line + 1},${
                character + 1
              }): ${message}`,
            );
          } else {
            console.log(
              ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
            );
          }
        });
      }
    
      return declarationContent;
    };
    

    运行结果,一切正常,DTS 内容也拿到了。

    compilation successful

    这就结束了吗?修改一下 sourceCode 的内容,test: string; 改成 Array;,出错了。

    compilation failed

    这个问题是由于 lib.d.ts 文件的找不到导致的,比较棘手的是,尝试了几种修改 lib.d.ts 文件的路径方式,结果都以是吧告终。

    face-2

    不愿妥协的我,又开始另辟蹊径了,在网上开始搜索一番。可谓皇天不负有心人,于是找到了 @typescript/vfs 这个 npm 库。@typescript/vfs 是一个基于映射的 TypeScript 虚拟文件系统。这对于我们在 Deno 环境中很有用,它可以运行虚拟的 TypeScript 环境,其中文件不是来源于真实磁盘上的。按照文档开始改造,最终核心的实现:

    import vfs from "npm:@typescript/vfs";
    
    const generateDeclarationFile = async (sourceCode: string) => {
      // ...
    
      // 创建一个编译选项对象
      const compilerOptions: ts.CompilerOptions = {
        declaration: true,
        emitDeclarationOnly: true,
        lib: ["ESNext"],
      };
    
      // 创建一个虚拟文件系统映射,并加载 lib.d.ts 文件
      const fsMap = await vfs.createDefaultMapFromCDN(
        compilerOptions,
        ts.version,
        true,
        ts,
      );
      fsMap.set(filename, sourceCode);
    
      // 创建虚拟文件系统
      const system = vfs.createSystem(fsMap);
      // 创建虚拟 TypeScript 环境
      const env = vfs.createVirtualTypeScriptEnvironment(
        system,
        [filename],
        ts,
        compilerOptions,
      );
    
      // 获取 TypeScript 编译输出
      const output = env.languageService.getEmitOutput(filename);
      // 将输出的声明文件内容拼接起来
      const declarationContent = output.outputFiles.reduce((prev, current) => {
        prev += current.text;
        return prev;
      }, "");
    
      // 创建虚拟编译器主机
      const host = vfs.createVirtualCompilerHost(system, compilerOptions, ts);
      // 创建 TypeScript 程序
      const program = ts.createProgram({
        rootNames: [...fsMap.keys()],
        options: compilerOptions,
        host: host.compilerHost,
      });
    
      // 执行编译并获取输出结果
      const emitResult = program.emit();
    
      // ...
    }
    

    看一下输出结果,符合我们的结果期望了,并且没有错误。

    @typescript/vfs output

    总结

    问题最终是得到了很好的解决,是值得庆祝🎉的。使用了 @typescript/vfs 库提供的虚拟文件系统,达到了我们需要的结果。期间想通过解析 AST,来生成 DTS 文件,这个工程有点大,最终被劝退了。本文介绍了在 Deno 中使用 @typescript/vfs 库生成 DTS 文件的步骤和方法,希望对你有所帮助。

    如果你觉得不错,可以点个 star 表示支持一下 https://github.com/long-woo/stc

    参考资料:

  • 相关阅读:
    AntDB-M高性能设计之hash索引动态rehash
    当线程池任务抛出异常
    过了那么多1024节才知道……
    Nacos源码分析专题(五)-Nacos小结
    接口测试实战教程01:接口测试环境搭建
    构造函数可以在类的私有部分中定义吗?
    pdf转换器是什么东西?看这篇就懂了!
    二十三、Redis集群
    【开发教程3】开源蓝牙心率防水运动手环-开发环境搭建
    JAVA线程池详解
  • 原文地址:https://www.cnblogs.com/JasonLong/p/17638932.html