15
头图

With the continuous expansion and maturity of the Vue3 ecosystem, Vue3 has entered the production project from the initial early adopter stage. Followed by the update of the development scaffolding, the new Vite scaffolding, based on the performance advantages of esbuild using the go language, has a performance advantage that is not an order of magnitude compared to Webpack. The packaging is based on Rollup expansion, inheriting lightweight and The advantages of the bright plug-in Api.

What, you don’t know yet? You should hurry up.

Vue3 official Chinese document

Vite official Chinese document

Don't talk nonsense, start to get into the topic.

Create project

This article focuses on how to generate a type declaration file, so the project creation part is only briefly described.

Quickly build a simple project through the official template:

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

Then changed its name to src/main.ts to src/index.ts and modified its content:

export { default as App } from './App.vue'
Don't care about the name of App, we just assume that we have written a component and exported it as a plug-in.

Then adjust vite.config.ts to package in library mode:

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()]
})

At this point, a simple plug-in project is complete.

Generate type declaration file

In the past, when developing the Rollup plug-in, we mainly used the rollup-plugin-typescript2 plug-in to generate the .d.ts declaration file based on the source code.

However, the plug-in has several problems. One is that it cannot parse the .vue file, and the other is incompatibility under the environment of Vite + Vue3 (the third is that Vite internally supports typescript , and the plug-in has a large part of the repeated functions). To put it bluntly It's not very reliable.

Of course, some people proposed in the issue that they hope that Vite will support exporting declaration files when packaged in library mode, but Vite officially stated that they do not want to increase the burden of maintenance and the complexity of the structure.

Therefore, in Vite development, we have to think of some other ways to generate declaration files.

The generation method introduced in this article still relies on some ready-made libraries, and then uses some programming scripts to achieve the goal. After all, starting from the packaging principle, that space may not be enough.

Install the library used to generate the declaration file:

yarn add ts-morph -D

In fact .vue file that wants to generate the type declaration file is to <script> part for analysis. After understanding this principle, many things are actually very simple.

Start writing our script after scripts/build-types.js new 061761064efbed.

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')
    }
  }
}

Add a package type file command at package.json

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

In the project root path, execute the following command:

yarn run build:types

You're done, you can see that dist are already types of declaration files such as index.d.ts and App.vue.d.ts

Vite plugin

In fact, in the process of Vite packaging, the @vitejs/plugin-vue plugin will .vue file into three parts, including template, script and style; we only need to get the content of the compiled script part, through the above method, or even You can easily generate type declaration files without compiling the files yourself.

Create 061761064efce9 and start to 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
  })

  let root: string
  
  return {
    name: 'gen-dts',
    apply: 'build',
    enforce: 'pre', // 需要在 pre 才能正确拿到 ts 的 script 部分
    configResolved(config) {
      root = config.root
    },
    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')
        }
      }
    }
  }
}

So easy, a simple dts plug-in is done.

We only need to reference the plugin vite.config.ts

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

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

Then execute the original command, you can see that the packaging and generating type declaration files are completed in one go:

yarn run build

image.png

Write at the end

Of course, the above plug-in only contains the most basic functions. The author wrote a plug-in covering a wider range of functions. The source code has been placed on github, and npm has also been released.

yarn add vite-plugin-dts -D

Everyone is welcome to use and feedback, if you think this is helpful to you, please like, bookmark, and reward one ⭐.

Plug-in address: https://github.com/qmhc/vite-plugin-dts

Finally, I secretly take a look at the Vue3 component library that I have used, which is currently mainly used for company projects: vexip-ui

2021-10-25 supplement

In a few months of updates, setup-script has become an official feature. For this compiler-sfc , some features of 061761064efe4b have also changed. In actual use, there has been a discrepancy with the original, so some of the above code may not be completely correct. However, the plug-in vite-plugin-dts has been updated and optimized. If you are interested in learning more, please read the plug-in source code~


未觉雨声
1.5k 声望70 粉丝

Vue3 组件库 VexipUI 作者,擅长 js 和 vue 系列技术,主攻前端(交互),稍微会一点点 Java(Spring Boot)。