Rolldown viteManifestPlugin 插件指南:在 Vite 构建中生成 manifest.json 资产映射
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
导读
viteManifestPlugin是 Rolldown 内置(builtin)插件之一,专门用于在构建结束后生成一份manifest.json,将源码中的原始文件名映射到最终产出的 chunk 与 asset 文件。它由 Vite 的manifestPlugin移植而来,核心价值在于服务端渲染(SSR)时的资源注入与构建产物的清单管理。读完本文,你将掌握该插件的引入方式、两个核心配置项(root/outPath)的用法、manifest 输出的字段结构,以及它在 Rolldown 内部的实现原理与测试验证方式。
插件定位:为 Vite 而生,为 SSR 服务
该插件位于仓库的 crates/rolldown_plugin_vite_manifest/ 目录,其官方说明(README.md)开宗明义:这是一个为vite生成manifest.json(将原始文件名映射到产出的 assets/chunks)的插件,移植自 Vite 的manifestPlugin。
其核心工作内容是:收集构建产生的所有 chunk 与 asset,将它们与源码中的原始名称关联起来,最终输出一份 manifest JSON。这份清单文件在以下场景中尤为实用:
- 服务端渲染(SSR):服务端需要根据入口模块名定位对应的打包产物(如入口 chunk、其依赖的动态导入 chunk 与样式文件),从而在 HTML 中注入正确的
<script>/<link>标签; - 资源注入:前端框架或构建编排系统可以通过 manifest 反向查找某个原始资源文件被编译成了哪个最终文件。
值得注意的是,README 中有一则明确提示(> [!NOTE]):
该插件仅面向
vite使用,不推荐外部单独使用;其 API 可能在 Rolldown 的 minor 版本之间发生变化,但在同一个 minor 版本内保持兼容。
这意味着如果你想在自己的非 Vite 构建脚本中稳定使用它,需要自行承担 API 变动的风险;同时由于插件深度依赖 Vite 生态约定(如isLegacy、cssEntries等回调语义),脱离 Vite 环境使用价值有限。
快速上手:Debug 用法示例
README 中给出了一份可直接运行的调试用法示例,完整继承了原文的配置骨架。在项目的入口配置(如rolldown.config.js)中引入:
import { defineConfig } from 'rolldown'; import { viteManifestPlugin } from 'rolldown/experimental'; export default defineConfig({ input: { entry: './main.ts', }, plugins: [ viteManifestPlugin({ root: path.resolve(import.meta.dirname), outPath: path.resolve(import.meta.dirname, 'dist/manifest.json'), }), ], });这段配置的关键信息有三点:
- 插件入口路径:
viteManifestPlugin从rolldown/experimental导出(对应源码 packages/rolldown/src/experimental-index.ts),表明其当前处于 experimental 阶段,API 可能随 minor 版本演进; root必须显式提供:插件需要以项目根目录为基准,把模块的绝对路径换算成相对路径作为 manifest 的 key(详见下文实现解析);outPath决定输出位置:manifest 最终会作为一份 emit 资源写入该路径。
配置项一览
README 中的 Options 表格完整如下:
| Option | Type | Description |
|---|---|---|
root | string | Project root directory |
outPath | string | Where to write the manifest output file |
root:项目根目录
作为 manifest key 的计算基准。实现中,root被用于把 chunk 的facade_module_id(入口模块绝对路径)裁剪为相对路径,从而得到稳定、可读的清单键名(见 src/utils.rs 中get_chunk_name对module_id.relative_path(&self.root)的调用)。因此,传入不一致的root会直接导致 manifest 中的 key 与源码路径对不上号,务必使用与构建入口一致的绝对路径。
outPath:manifest 输出文件路径
manifest 的写入位置。它在插件内部并非直接落盘,而是通过emit_file_async以EmittedAsset的形式交还给打包器统一输出(见 src/lib.rs),这样能够复用 Rolldown 的产物发射机制,与其他 emit 文件保持一致的命名与写入流程。
绑定层的额外选项(JS 侧可见)
在 Rust 绑定层,配置结构还包含两个回调字段(见 crates/rolldown_binding/src/options/plugin/config/binding_vite_manifest_plugin_config.rs):
is_legacy(JS 侧isLegacy):(options) => boolean形式的回调,用于判断当前输出是否针对 legacy 构建。为true时,chunk 名会被追加-legacy后缀(实现见 utils.rs),这是 Vite legacy 插件生态的常见约定;css_entries(JS 侧cssEntries):() => Record<string, string>形式的回调,返回「css 产物文件名 → 入口模块名」的映射,用于在 manifest 中为 asset 标注其所属的 CSS 入口(见 lib.rs)。
这两个回调是 Vite 侧集成时由 Vite 包装层注入的,普通调试场景可像测试用例那样传入空实现(见下文测试章节)。
manifest 输出格式详解
插件输出的 JSON 主体由ManifestChunk结构序列化而来,字段采用 camelCase 命名,并且大量使用skip_serializing_if省略空字段,让最终清单保持精简(定义见 src/utils.rs):
| 字段 | 类型 | 含义 | 何时出现 |
|---|---|---|---|
file | string | 最终产出的文件名 | 始终 |
name | string | chunk 名(如入口main) | 有值时 |
names | string[] | asset 的完整名称列表 | 有值时 |
src | string | 原始源文件路径(相对root) | chunk 有facade_module_id时 |
isEntry | boolean | 是否为入口 chunk | 仅true时输出 |
isDynamicEntry | boolean | 是否为动态入口 chunk | 仅true时输出 |
imports | string[] | 静态依赖的 chunk 名列表 | 非空时输出 |
dynamicImports | string[] | 动态 import 的 chunk 名列表 | 非空时输出 |
css | string[] | 关联的 CSS 文件 | 预留字段(当前固定为None) |
assets | string[] | 关联的 asset 文件 | 预留字段(当前固定为None) |
一个真实的输出样例来自仓库测试快照 packages/rolldown/tests/fixtures/builtin-plugin/manifest/manifest.json.snap:
{ "asset.txt": { "file": "asset.txt", "src": "asset.txt" }, "chunk.js": { "file": "chunk.js", "isDynamicEntry": true, "name": "chunk", "src": "chunk.js" }, "main.js": { "dynamicImports": ["chunk.js"], "file": "main.js", "isEntry": true, "name": "main", "src": "main.js" } }可以直观看到:manifest 的 key 是源码路径(如main.js),而值中file指向最终产物(main.js),并且通过imports/dynamicImports完整描述了 chunk 之间的依赖拓扑。
底层实现原理
挂钩点:generate_bundle
插件注册了唯一一个 hook:generateBundle(见 src/lib.rs 的register_hook_usage),插件内部名称为builtin:vite-manifest(lib.rs),该名称也注册在绑定层的 binding_builtin_plugin_name.rs 中,JS 侧通过 vite-manifest-plugin.ts 的new BuiltinPlugin('builtin:vite-manifest', config)与之对应。这意味着整个插件以 Rust 原生实现运行,没有额外的 JS 插件调度开销。
输出排序:BTreeMap
在 generate_bundle 中,插件用BTreeMap收集 manifest 条目,从而保证最终 JSON 的 key 按字典序排列、输出稳定可复现:
// Use BTreeMap to make the result sorted let mut manifest = BTreeMap::default();随后遍历bundle中的每个Output,按 chunk 与 asset 分派处理。
Chunk 的命名规则
对每个Output::Chunk,通过get_chunk_name计算 manifest key(utils.rs):
- 若 chunk 存在
facade_module_id(即它是某个模块的包装入口),则取该模块相对root的路径作为名称,并做normalize_path归一化、剔除\0字符; - 若没有
facade_module_id(内部拆分出来的 chunk),则以_+ 产物文件名的形式兜底命名。
同时,create_chunk会通过get_internal_imports把 chunk 的静态imports与dynamic_imports映射为「内部 chunk 名列表」——只有当依赖文件能在当前 bundle 中命中同名 chunk 时才保留,天然过滤掉 external 的外部模块。
Asset 的处理与「JS 优先」去重策略
对Output::Asset,插件会把每个唯一的 asset 按原始文件名写入 manifest;若 asset 没有original_file_names,则退化为_+ 文件名。这里有一个值得注意的细节(lib.rs):
如果 JS chunk 与 asset chunk 由同一个源文件生成,优先保留 JS chunk,因为它包含更丰富的信息(
isEntry、imports、dynamicImports等)。
这一点由工具函数is_non_js_file(utils.rs)保证:只有当当前已存在的 manifest 条目对应的产物不是.js/.cjs/.mjs时,asset 才会覆盖写入;否则跳过。这避免了「同一源文件既产出 JS 又产出 asset 时 key 冲突被错误覆盖」的问题。
输出与未来规划
最后,manifest 以serde_json::to_string_pretty的美化 JSON 格式,通过emit_file_async发射为EmittedAsset(文件名为outPath)lib.rs。源码中还保留了一段注释掉的逻辑(TODO: uncomment these when multiple outputs are supported,对应 Vite 侧对rollupOptions.output数组长度的判断),说明当前实现假定单输出场景,多输出(multiple outputs)支持仍在规划中。
测试验证:一个可复现的闭环
仓库在 packages/rolldown/tests/fixtures/builtin-plugin/manifest/_config.ts 提供了一个完整的集成测试夹具,可作为自定义验证的最小模板:
import path from 'node:path'; import { defineTest } from 'rolldown-tests'; import { viteManifestPlugin } from 'rolldown/experimental'; import { expect } from 'vitest'; export default defineTest({ sequential: true, config: { output: { chunkFileNames: '[name].js', assetFileNames: '[name][extname]', }, plugins: [ viteManifestPlugin({ root: path.resolve(import.meta.dirname), outPath: 'manifest.json', cssEntries: () => Object.fromEntries(new Map().entries()), }), { name: 'test', buildStart() { this.emitFile({ type: 'asset', name: 'asset.txt', source: 'hello world', originalFileName: 'asset.txt', }); }, }, ], }, async afterTest() { const manifest = await import('./dist/manifest.json'); await expect(manifest.default).toMatchFileSnapshot( path.resolve(import.meta.dirname, 'manifest.json.snap'), ); }, });该测试同时覆盖了三条核心行为,值得在接入该插件时重点回归:
- 入口 chunk 记录:
main.js以isEntry: true与dynamicImports: ["chunk.js"]呈现; - 动态入口 chunk 记录:动态导入产生的
chunk.js被标记为isDynamicEntry: true; - emit 的 asset 记录:通过
emitFile发射的asset.txt以原始文件名出现在 manifest 中,且没有多余的空字段(names等被省略)。
使用注意事项小结
- 仅在 Vite 生态内使用:README 明确不推荐外部使用,且 API 在 minor 版本间可能变动,升级 Rolldown 时需关注 changelog;
root与outPath建议传绝对路径:前者决定 manifest key 的相对路径基准,后者决定产物写入位置,两者语义不同但都影响最终清单的正确性;- manifest key 可能包含非 JS 产物:asset 条目与 chunk 条目共存,且同一源文件的 JS 产物优先级更高,解析清单时应以
file字段为准而非假设 key 即源码路径; - 当前为单输出假设:多输出配置(
output数组)尚未支持,源码中相关逻辑仍处于 TODO 状态。
通过这份指南,你可以快速在自己的 Rolldown + Vite 项目中接入viteManifestPlugin,并结合上文给出的源码路径与测试夹具,深入理解乃至扩展这份 manifest 生成能力。
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考