首页 > 其他分享 >Deno 中使用 @typescript/vfs 生成 DTS 文件

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

时间:2023-08-19 20:44:41浏览次数:37  
标签:typescript const true ts TypeScript Deno DTS vfs

背景

前段时间开源的 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<string>;
    }

    /**
     * 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<string>;,出错了。

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,const,true,ts,TypeScript,Deno,DTS,vfs
From: https://www.cnblogs.com/JasonLong/p/17638932.html

相关文章

  • TypeScript学习
    TypeScript快速入门JavaScript是一种属于网络的高级脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美欢的浏览效果。TypeScript是JavaScript的一个超集,它扩展了JavaScript的语法通过在JavaScript的基础上添加静态类型定义......
  • [React Typescript] Discriminated Tuples in Custom Hooks
    import{useEffect,useState}from"react";exporttypeResult<T>=|["loading",undefined?]|["error",Error]|["success",T];exportconstuseData=<T,>(url:string):Result<T>=>......
  • typeScript学习-类、静态数据、静态属性应用
    typeScript学习类、静态数据、静态属性应用类:定义:类就是拥有相同属性和方法的一系列对象的集合。展开理解:类是一个模具,是从这该类包含的所有具体对象中抽象出来的一个概念,类定义了它包含的全体对象的静态特征和动态特征。举例:people类静态特征【属性】name、age、address......
  • typeScript学习-TS类型-其他特殊类型-可变元组
    typeScript学习可变元组:letpeople:[string,number,string,string,string]=["wangwu",23,"地址",'13312341234','备注']//当前三个数据固定格式,后面数据不确认格式时用可变元组//可变元组//letcustomers:[string,number,string,...any[]]=[&qu......
  • typeScript学习-TS类型-其他特殊类型-元组(tuple)
    typeScript学习元组(tuple):满足以下3点的数组就是元组(1)在定义时每个元素的类型都是确定(2)元素值的数据类型必须是当前元素定义的类型(3)元素值的个数必须和定义时个数相同 letsalary:[string,number,number,number,number]=["zhangsan",5000,5000,5000,5000] ......
  • [React Typescript] Well typed a React Context provider
    importReactfrom"react";import{Equal,Expect}from"../helpers/type-utils";constcreateRequiredContext=<Textendsany>()=>{constcontext=React.createContext<T|null>(null);constuseContext=<Te......
  • [React Typescript] Fixing type inference in a Custom React Hook
    //Problemimport{useState}from"react";import{Equal,Expect}from"../helpers/type-utils";exportconstuseId=(defaultId:string)=>{const[id,setId]=useState(defaultId);return[id,setId];};const[id,setI......
  • typeScript学习-interface和type 区别
    typeScript学习interface(接口)和type区别type和接口类似,都用来定义类型,但type和interface区别如下:区别1:定义类型范围不同interface只能定义对象类型或接口当名字的函数类型。type可以定义任何类型,包括基础类型、联合类型、交叉类型,元组。//type定义基础类型typ......
  • 前端周刊第66期:TypeScript教程、ESM、React泡沫、htmx、测试文章
    周刊同步发表于微信公众号“写代码的宝哥”,欢迎各位小伙伴前来关注......
  • vue3+typescript中的props
     以上是子组件 以上是父组件<scriptsetuplangs="ts">letprops=defineProps(['info','money'])//父子组件的通信需要用到defineProps方法去接受父组件想要传递的数据console.info(props)</script>需要注意的就是:props可以实现父子组件的通信,但是props的......