在 NPM workspace 中使用 Vite 重新构建本地依赖

原文信息: 查看原文查看原文

Using Vite To Rebuild Local Dependencies in an NPM Workspace

- prosopo.io

在 Prosopo,我们使用 Vite 构建应用程序。我们有一个包含多个相互依赖的包的单体仓库结构。当我们对 Vite 项目所依赖的包进行更改时,Vite 不会自动重新构建本地包依赖项,因为它位于模块图之外。本文解释了如何让 Vite 在 NPM 工作区中重新构建本地依赖。

NPM 工作区结构

类似于 这个问题,引用 Yarn 工作区,我们有以下 npm 工作区结构:

package.json // 工作区根
packages
  package-a (@prosopo/a)
    dist // 构建的 JS
    src
    package.json
    tsconfig.json
  package-b (@prosopo/b)
    dist
    src
    package.json
    tsconfig.json
  package-c (@prosopo/c)
    dist
    src
    package.json
    tsconfig.json
  ...

这些包在工作区根 package JSON 中如下引用:

{
  "workspaces": [
    "packages/*",
  ],
}

以这个例子来说,假设 @prosopo/c 依赖于 @prosopo/b,而 @prosopo/b 依赖于 @prosopo/a。它们通过各自 tsconfig.json 文件中的 references 字段链接。

@prosopo/c tsconfig.json 中,references 看起来像这样:

"references": [
  {
    "path": "../package-b"
  }
]

每个包在其 package.json 中都有自己的构建命令。

{
  "build": "tsc --build --verbose tsconfig.json",
}

tsc 命令同时构建类型和 JS,这对于使用本地包进行开发时的导入至关重要。

注意

Vite 不构建类型。要构建类型,您可以:

  • 使用 tsc
  • 添加一个构建 TypeScript 类型的 Vite 插件,例如 vite-plugin-dts
  • 向 VitePluginWatchExternal 添加一个 esBuild 插件 以构建类型。

对于本演示,我们将假设类型已经被构建,我们只关心按需构建 JavaScript。

Vite Serve 命令

@prosopo/c 是一个 Web 应用程序,例如,一个 React 应用程序。我们使用以下命令在 package-c 文件夹内运行 @prosopo/c

> npx vite serve --mode=development --config vite.config.ts

  VITE v5.2.9  准备就绪,耗时 646 毫秒

  ➜  本地:   http://localhost:5173/
  ➜  网络:   使用 --host 公开
  ➜  按 h + enter 显示帮助

问题

不幸的是,上述 Vite 命令不识别本地工作区依赖项作为项目的一部分。当我们对 @prosopo/a@prosopo/b 进行更改时,Vite 不会重新构建它们。这意味着我们必须每次更改它们时手动构建这些包。

这些包没有被添加到 Vite 模块图中,因此它们没有被监视以检测更改。这是因为这些包被符号链接到 node_modules 文件夹中,而 Vite 默认不跟踪符号链接。Yarn 工作区问题的答案是在 Vite 配置中将 preserveSymlinks 设置为 true。然而,这对我们不起作用。

那么,我们如何让 @prosopo/c vite serve 命令在更改时重新构建 @prosopo/a@prosopo/b 呢?

解决方案

解决方案的第一部分是创建一个 Vite 插件,在 buildStart 事件上将文件添加到监视列表。

type FilePath = string

type ExternalFiles = Record<FilePath, TsConfigPath> // 将文件路径映射到 tsconfig 路径

export const vitePluginWatchExternal = async (config: VitePluginWatchExternalOptions): Promise<Plugin<any>> => {

  // 一个辅助函数,接受工作区根并获取所有本地包文件
  const externalFiles: ExternalFiles = await getExternalFileLists(config.workspaceRoot, config.fileTypes || FILE_TYPES)

  return {
    name: 'vite-plugin-watch-external',
    async buildStart() {
      Object.keys(externalFiles).map((file) => {
        this.addWatchFile(file)
      })
    },
    ...
  }
}

然而,这并不足以让 Vite 重新构建这些文件。通过向模块图之外添加文件,Vite 将在它们更改时简单地返回一个 no modules matched 消息。这是因为这些文件不是模块图的一部分。

我们需要再进一步,监听 handleHotUpdate 事件。当文件更改并且 Vite 即将将更新发送到客户端时,将触发此事件。我们可以使用这个事件使用 esbuild 重新构建我们新监视的文件,esbuild 是 Vite 的默认打包器。

export const vitePluginWatchExternal = async (config: VitePluginWatchExternalOptions): Promise<Plugin<any>> => {

  // 一个辅助函数,接受工作区根并获取所有本地包文件
  const externalFiles: ExternalFiles = await getExternalFileLists(config.workspaceRoot, config.fileTypes || FILE_TYPES)

  return {

    ...

    async handleHotUpdate({ file, server }) {

      // 我们之前定义的 externalFiles 对象,在插件创建时设置
      const tsconfigPath = externalFiles[file]

      if (!tsconfigPath) {
        log(`tsconfigPath 未找到文件 ${file}`)
        return
      }
      // 辅助函数加载与文件关联的 tsconfig
      const tsconfig = getTsConfigFollowExtends(tsconfigPath)

      // 辅助函数获取文件扩展名和加载器
      const fileExtension = path.extname(file)
      const loader = getLoader(fileExtension)

      // 辅助函数获取文件的 outdir 和 outfile
      const outdir = getOutDir(file, tsconfig)
      const outfile = getOutFile(outdir, file, fileExtension)

      // 使用加载的 tsconfig 和上述派生的正确的文件路径和文件扩展名,使用 esbuild 构建结果
      const buildResult = await build({
        tsconfig: tsconfigPath,
        stdin: {
          contents: fs.readFileSync(file, 'utf8'),
          loader,
          resolveDir: path.dirname(file),
        },
        outfile,
        platform: config.format === 'cjs' ? 'node' : 'neutral',
        format: config.format || 'esm',
      })

      // 重新加载客户端
      server.ws.send({
        type: 'full-reload',
      })
    },
  }
}

实际操作视频

在 npm 工作区中重新构建本地包依赖项

就这样!

有了这个插件,Vite 现在将在更改时重新构建本地依赖。这在具有多个相互依赖的包的单体仓库结构中特别有用。

您可以在 我们的 Procaptcha 存储库 中查看插件的完整代码,并通过 npm 注册表 自行使用该插件。

在 Prosopo,我们一直在寻找改进开发工作流程的方法。如果您有任何建议或问题,请随时在 这里 联系我们。

分享于 2024-05-09

访问量 122

预览图片