在 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',
})
},
}
}
实际操作视频
就这样!
有了这个插件,Vite 现在将在更改时重新构建本地依赖。这在具有多个相互依赖的包的单体仓库结构中特别有用。
您可以在 我们的 Procaptcha 存储库 中查看插件的完整代码,并通过 npm 注册表 自行使用该插件。
在 Prosopo,我们一直在寻找改进开发工作流程的方法。如果您有任何建议或问题,请随时在 这里 联系我们。