Vite 開發插件並生成 .d.ts 類型聲明文件

隨着 Vue3 生態的不斷擴展與日漸成熟,Vue3 已從最開始的嚐鮮階段步入到投入生產項目中。隨之而來的還有開發腳手架的更新換代,全新的 Vite 腳手架,基於 esbuild 利用 go 語言的性能優點,相較 Webpack 有着不在一個量級的性能優點,打包方面基於 Rollup 拓展,繼承了輕量化和明朗的插件 Api 的優勢。vue

什麼,你還不知道?你該抓緊了。node

Vue3 官方中文文檔git

Vite 官方中文文檔github

廢話很少說,開始進入的正題。typescript

建立項目

本文重點講述如何生成類型聲明文件,所以項目建立部分只一些簡單描述。npm

經過官方提供的模版快速搭建一個簡單的項目:編程

yarn create @vitejs/app my-vue-app --template vue-ts

隨後改名 src/main.tssrc/index.ts 並修改其內容:json

export { default as App } from './App.vue'
不要在乎 App 這個名字,咱們只是假設咱們寫了一個組件,而且做爲插件導出。

接着調整 vite.config.ts 的配置爲庫模式打包:promise

import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'Plugin',
      formats: ['es'],
      fileName: 'index'
    }
  },
  plugins: [vue()]
})

至此,一個簡單的插件項目就完成了。app

生成類型聲明文件

在使用 rollup 開發插件時,咱們主要藉助 rollup-plugin-typescript2 這個插件來實現根據源碼生成 .d.ts 聲明文件。

可是該插件存在幾個問題,一是沒法解析 .vue 文件,二是在 Vite + Vue3 的環境下,存在不兼容性(三是 Vite 內部支持 typescript,該插件存在很大部分的重複功能),說白了就是用不了。

固然,也有人在 issue 中提出但願 Vite 內部支持在庫模式導出聲明文件,但 Vite 官方表示不但願所以增長維護的負擔和結構的複雜性。

所以在 Vite 開發中,咱們要想一些其餘辦法來生成聲明文件。

本文介紹的生成方式仍是依賴一些現成的庫,而後經過一些編程腳本以達到目的,畢竟從打包原理開始講,那篇幅可能不太夠。

安裝生成聲明文件的核心庫:

yarn add ts-morph -D

其實 .vue 文件想要生成類型聲明文件的核心點在於把 <script> 部分的內容提取出來進行解析,當明白了這個原理後,其實不少東西就很簡單了。

新建 scripts/build-types.js 後開始編寫咱們的腳本。

const path = require('path')
const fs = require('fs')
const glob = require('fast-glob')
const { Project } = require('ts-morph')
const { parse, compileScript } = require('@vue/compiler-sfc')

let index = 1

main()

async function main() {
  // 這部份內容具體能夠查閱 ts-morph 的文檔
  // 這裏僅須要知道這是用來處理 ts 文件並生成類型聲明文件便可
  const project = new Project({
    compilerOptions: {
      declaration: true,
      emitDeclarationOnly: true,
      noEmitOnError: true,
      allowJs: true, // 若是想兼容 js 語法須要加上
      outDir: 'dist' // 能夠設置自定義的打包文件夾,如 'types'
    },
    tsConfigFilePath: path.resolve(__dirname, '../tsconfig.json'),
    skipAddingFilesFromTsConfig: true
  })

  // 獲取 src 下的 .vue 和 .ts 文件
  const files = await glob(['src/**/*.ts', 'src/**/*.vue'])
  const sourceFiles = []

  await Promise.all(
    files.map(async file => {
      if (/\.vue$/.test(file)) {
        // 對於 vue 文件,藉助 @vue/compiler-sfc 的 parse 進行解析
        const sfc = parse(await fs.promises.readFile(file, 'utf-8'))
        // 提取出 script 中的內容
        const { script, scriptSetup } = sfc.descriptor

        if (script || scriptSetup) {
          let content = ''
          let isTs = false

          if (script && script.content) {
            content += script.content

            if (script.lang === 'ts') isTs = true
          }

          if (scriptSetup) {
            const compiled = compileScript(sfc.descriptor, {
              id: `${index++}`
            })

            content += compiled.content

            if (scriptSetup.lang === 'ts') isTs = true
          }

          sourceFiles.push(
            // 建立一個同路徑的同名 ts/js 的映射文件
            project.createSourceFile(file + (isTs ? '.ts' : '.js'), content)
          )
        }
      } else {
        // 若是是 ts 文件則直接添加便可
        sourceFiles.push(project.addSourceFileAtPath(file))
      }
    })
  )

  const diagnostics = project.getPreEmitDiagnostics()

  // 輸出解析過程當中的錯誤信息
  console.log(project.formatDiagnosticsWithColorAndContext(diagnostics))

  project.emitToMemory()

  // 隨後將解析完的文件寫道打包路徑
  for (const sourceFile of sourceFiles) {
    const emitOutput = sourceFile.getEmitOutput()

    for (const outputFile of emitOutput.getOutputFiles()) {
      const filePath = outputFile.getFilePath()

      await fs.promises.mkdir(path.dirname(filePath), { recursive: true })
      await fs.promises.writeFile(filePath, outputFile.getText(), 'utf8')
    }
  }
}

package.json 添加一個打包類型文件的命令:

{
  "scripts": {
    "build:types": "node scripts/build-types.js"
  }
}

在項目根路徑下,執行如下命令:

yarn run build:types

大功告成,能夠看到 dist 目錄下已經有了 index.d.tsApp.vue.d.ts 等類型聲明文件。

Vite 插件

其實,在 Vite 打包的過程當中,@vitejs/plugin-vue 插件會將 .vue 文件編譯並拆分紅三個部分,包括模版,腳本和樣式;咱們只須要拿到編譯後的腳本部分的內容,經過上面的方法,甚至不須要本身編譯文件,就能夠輕鬆生成類型聲明文件。

開始擼代碼:

// plugins/dts.ts
import { resolve, dirname } from 'path'
import fs from 'fs/promises'
import { createFilter } from '@rollup/pluginutils'
import { normalizePath } from 'vite'
import { Project } from 'ts-morph'

import type { Plugin } from 'vite'
import type { SourceFile } from 'ts-morph'

export default (): Plugin => {
  const filter = createFilter(['**/*.vue', '**/*.ts'], 'node_modules/**')
  const sourceFiles: SourceFile[] = []
  
  const project = new Project({
    compilerOptions: {
      declaration: true,
      emitDeclarationOnly: true,
      noEmitOnError: true,
      allowJs: true, // 若是想兼容 js 語法須要加上
      outDir: 'dist' // 能夠設置自定義的打包文件夾,如 'types'
    },
    tsConfigFilePath: resolve(__dirname, '../tsconfig.json'),
    skipAddingFilesFromTsConfig: true
  })

  const root = process.cwd()
  
  return {
    name: 'gen-dts',
    apply: 'build',
    enforce: 'post',
    transform(code, id) {
      if (!code || !filter(id)) return null

      // 拆分後的文件 id 具備一些特徵,能夠用正則的方式來捕獲
      if (/\.vue(\?.*type=script.*)$/.test(id)) {
        const filePath = resolve(root, normalizePath(id.split('?')[0]))

        sourceFiles.push(
          project.createSourceFile(filePath + (/lang.ts/.test(id) ? '.ts' : '.js'), code)
        )
      } else if (/\.ts$/.test(id)) {
        const filePath = resolve(root, normalizePath(id))

        sourceFiles.push(project.addSourceFileAtPath(filePath))
      }
    },
    async generateBundle() {
      const diagnostics = project.getPreEmitDiagnostics()

      // 輸出解析過程當中的錯誤信息
      console.log(project.formatDiagnosticsWithColorAndContext(diagnostics))

      project.emitToMemory()

      // 隨後將解析完的文件寫道打包路徑
      for (const sourceFile of sourceFiles) {
        const emitOutput = sourceFile.getEmitOutput()

        for (const outputFile of emitOutput.getOutputFiles()) {
          const filePath = outputFile.getFilePath()

          await fs.mkdir(dirname(filePath), { recursive: true })
          await fs.writeFile(filePath, outputFile.getText(), 'utf8')
        }
      }
    }
  }
}

如此輕鬆,一個簡單的 dts 插件就完成了。

咱們只須要在 vite.config.ts 中引用插件:

import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from './dts'

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'Plugin',
      formats: ['es'],
      fileName: 'index'
    }
  },
  plugins: [vue(), dts()]
})

而後執行原來的命令,就能夠看到打包和生成類型聲明文件一條龍了:

yarn run build

image.png

寫在最後

固然了,上述插件只包含了最基礎的功能,筆者本身寫了一個涵蓋功能更加普遍的插件,源碼已放在 github 上,同時 npm 也進行了發佈。

yarn add vite-plugin-dts -D

歡迎你們進行使用和反饋,若是以爲這對你有所幫助,還請點贊、收藏,和賞一顆⭐。

插件地址:https://github.com/qmhc/vite-...
最後偷偷安利一下本身的組件庫:vexip-ui