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

打印 上一主题 下一主题

主题 905|帖子 905|积分 2715

背景

前段时间开源的 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 生成。

代码实现

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

于是,开始另寻他路。
在尝试了 esbuild 失败后,决定使用 swc 将 TypeScript 编译成 JavaScript 代码,可是不支持生成 DTS 文件,这还需要用 tsc 来实现。其中比较棘手是 tsc 在 Deno 里面实现(应该是对 TypeScript compiler Api 不熟的原因)。
通过在网上查阅 TypeScript compiler Api 的使用资料,同时还借助 ChatGPT 的协助,对 TypeScript compiler Api 有了个初步的认识。
摘自 TypeScript wiki 的示例(从 JavaScript 文件获取 DTS):
  1. import * as ts from "typescript";
  2. function compile(fileNames: string[], options: ts.CompilerOptions): void {
  3.   // 创建一个带有内存发射的编译程序
  4.   const createdFiles = {}
  5.   const host = ts.createCompilerHost(options); // 创建编译器主机
  6.   host.writeFile = (fileName: string, contents: string) => createdFiles[fileName] = contents // 覆盖写入文件的方法
  7.   
  8.   // 准备并发射类型声明文件
  9.   const program = ts.createProgram(fileNames, options, host);
  10.   program.emit();
  11.   // 遍历所有输入文件
  12.   fileNames.forEach(file => {
  13.     console.log("### JavaScript\n")
  14.     console.log(host.readFile(file))
  15.     console.log("### Type Definition\n")
  16.     const dts = file.replace(".js", ".d.ts")
  17.     console.log(createdFiles[dts])
  18.   })
  19. }
  20. // 运行编译器
  21. compile(process.argv.slice(2), {
  22.   allowJs: true,
  23.   declaration: true,
  24.   emitDeclarationOnly: true,
  25. });
复制代码
我想法是直接是用代码字符串生成的方式,不是文件,所以这段示例不能直接应用到我们的代码里面来。结合 ChatGPT 的一些回答和网上的资料,改造如下:
  1. import ts from "npm:typescript";
  2. const generateDeclarationFile = () => {
  3.   const sourceCode = `
  4.     export type TypeTest = 1 | 0 | 3;
  5.     export interface ISwagger {
  6.       name: string;
  7.       age: number;
  8.       test: string; // Array<string>;
  9.     }
  10.     /**
  11.      * Adds two numbers.
  12.      * @param a The first number.
  13.      * @param b The second number.
  14.      * @returns The sum of a and b.
  15.      */
  16.     export function add(a: any, b: any): number {
  17.       return a + b;
  18.     }
  19.   `;
  20.   const filename = "temp.ts";
  21.   // 创建一个编译选项对象
  22.   const compilerOptions: ts.CompilerOptions = {
  23.     target: ts.ScriptTarget.ESNext,
  24.     declaration: true,
  25.     emitDeclarationOnly: true,
  26.     lib: ["ESNext"],
  27.   };
  28.   let declarationContent = "";
  29.   const sourceFile = ts.createSourceFile(
  30.     filename,
  31.     sourceCode,
  32.     ts.ScriptTarget.ESNext,
  33.     true,
  34.   );
  35.   const defaultCompilerHost = ts.createCompilerHost(compilerOptions);
  36.   const host: ts.CompilerHost = {
  37.     getSourceFile: (fileName, languageVersion) => {
  38.       if (fileName === filename) {
  39.         return sourceFile;
  40.       }
  41.       return defaultCompilerHost.getSourceFile(fileName, languageVersion);
  42.     },
  43.     writeFile: (_name, text) => {
  44.       declarationContent = text;
  45.       // console.log(text);
  46.     },
  47.     getDefaultLibFileName: () => "./registry.npmjs.org/typescript/5.1.6/lib/lib.d.ts"
  48.     useCaseSensitiveFileNames: () => true,
  49.     getCanonicalFileName: (fileName) => fileName,
  50.     getCurrentDirectory: () => "",
  51.     getNewLine: () => "\n",
  52.     getDirectories: () => [],
  53.     fileExists: () => true,
  54.     readFile: () => "",
  55.   };
  56.   // 创建 TypeScript 编译器实例
  57.   const program = ts.createProgram(
  58.     [filename],
  59.     compilerOptions,
  60.     host,
  61.   );
  62.   // 执行编译并处理结果
  63.   const emitResult = program.emit();
  64.   if (emitResult.emitSkipped) {
  65.     console.error("Compilation failed");
  66.     const allDiagnostics = ts
  67.       .getPreEmitDiagnostics(program)
  68.       .concat(emitResult.diagnostics);
  69.     allDiagnostics.forEach((diagnostic) => {
  70.       if (diagnostic.file) {
  71.         const { line, character } = ts.getLineAndCharacterOfPosition(
  72.           diagnostic.file,
  73.           diagnostic.start!,
  74.         );
  75.         const message = ts.flattenDiagnosticMessageText(
  76.           diagnostic.messageText,
  77.           "\n",
  78.         );
  79.         console.log(
  80.           `${diagnostic.file.fileName} (${line + 1},${
  81.             character + 1
  82.           }): ${message}`,
  83.         );
  84.       } else {
  85.         console.log(
  86.           ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
  87.         );
  88.       }
  89.     });
  90.   }
  91.   return declarationContent;
  92. };
复制代码
运行结果,一切正常,DTS 内容也拿到了。

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

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

不愿妥协的我,又开始另辟蹊径了,在网上开始搜索一番。可谓皇天不负有心人,于是找到了 @typescript/vfs 这个 npm 库。@typescript/vfs 是一个基于映射的 TypeScript 虚拟文件系统。这对于我们在 Deno 环境中很有用,它可以运行虚拟的 TypeScript 环境,其中文件不是来源于真实磁盘上的。按照文档开始改造,最终核心的实现:
  1. import vfs from "npm:@typescript/vfs";
  2. const generateDeclarationFile = async (sourceCode: string) => {
  3.   // ...
  4.   // 创建一个编译选项对象
  5.   const compilerOptions: ts.CompilerOptions = {
  6.     declaration: true,
  7.     emitDeclarationOnly: true,
  8.     lib: ["ESNext"],
  9.   };
  10.   // 创建一个虚拟文件系统映射,并加载 lib.d.ts 文件
  11.   const fsMap = await vfs.createDefaultMapFromCDN(
  12.     compilerOptions,
  13.     ts.version,
  14.     true,
  15.     ts,
  16.   );
  17.   fsMap.set(filename, sourceCode);
  18.   // 创建虚拟文件系统
  19.   const system = vfs.createSystem(fsMap);
  20.   // 创建虚拟 TypeScript 环境
  21.   const env = vfs.createVirtualTypeScriptEnvironment(
  22.     system,
  23.     [filename],
  24.     ts,
  25.     compilerOptions,
  26.   );
  27.   // 获取 TypeScript 编译输出
  28.   const output = env.languageService.getEmitOutput(filename);
  29.   // 将输出的声明文件内容拼接起来
  30.   const declarationContent = output.outputFiles.reduce((prev, current) => {
  31.     prev += current.text;
  32.     return prev;
  33.   }, "");
  34.   // 创建虚拟编译器主机
  35.   const host = vfs.createVirtualCompilerHost(system, compilerOptions, ts);
  36.   // 创建 TypeScript 程序
  37.   const program = ts.createProgram({
  38.     rootNames: [...fsMap.keys()],
  39.     options: compilerOptions,
  40.     host: host.compilerHost,
  41.   });
  42.   // 执行编译并获取输出结果
  43.   const emitResult = program.emit();
  44.   // ...
  45. }
复制代码
看一下输出结果,符合我们的结果期望了,并且没有错误。

总结


问题最终是得到了很好的解决,是值得庆祝
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

盛世宏图

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表