Vue SFC bundleless 特判

问题背景

Vue 单文件组件不是普通 JavaScript 文件。@rsbuild/plugin-vue 底层使用 rspack-vue-loader.vue 文件拆成 script、template、style 等部分,再通过 helper 组合成最终组件。

在普通 bundle 模式下,这些 loader 内部模块会被打进 bundle,用户通常感知不到。但在 Rslib 的 bundleless 模式下,外部化逻辑会拦截模块请求并尝试保留模块结构。这就可能把 Vue loader 内部 request 错当成用户源码模块。

具体冲突

Rslib bundleless 想保留的是:

import Button from './Button.vue';

这种用户源码层面的 import。

rspack-vue-loader 可能生成:

rspack-vue-loader/dist/exportHelper.js
Button.vue?vue&type=script&setup=true&lang.ts
Button.vue?vue&type=template
Button.vue?vue&type=style&index=0&lang.css

这些不是公共模块边界。它们只是 loader 编译 .vue 时的内部模块。

如果 Rslib 把它们 external 到最终产物中,消费者会看到 loader 内部路径,或者看到带 ?vue&type= 的虚拟 request。消费者项目没有义务安装相同 loader,也不应该解析这些内部 request。

当前策略

Core 在 composeBundlelessExternalConfig 中定义:

const isRspackVueLoaderRequest = (
  request?: string,
  suffix?: string,
): request is string =>
  typeof request === 'string' &&
  request.includes('rspack-vue-loader') &&
  (suffix ? request.includes(suffix) : true);

然后对两类 request 放行:

isRspackVueLoaderRequest(request, 'dist/exportHelper.js') ||
  isRspackVueLoaderRequest(request, '?vue&type=');

这里的 callback() 表示不 external、不重写,让 Rspack 正常处理并 bundle 这些内部模块。

关键源码

源码位置:

  • packages/core/src/config.ts:1379
  • packages/core/src/config.ts:1499

相关测试:

  • tests/integration/vue
  • tests/e2e/vue-component

tests/integration/vue/index.test.ts 的 bundleless 快照能看到最终输出是普通 JS 和 CSS 文件,而不是 loader 内部 request。

坏产物示例

如果没有这层特判,可能出现类似:

import exportHelper from 'rspack-vue-loader/dist/exportHelper.js';
import script from './Button.vue?vue&type=script&setup=true&lang.ts';
import render from './Button.vue?vue&type=template';

export default exportHelper(script, [['render', render]]);

这种产物有几个问题:

  1. 消费者需要解析 rspack-vue-loader 内部模块。
  2. 消费者需要理解 ?vue&type= 虚拟请求。
  3. .vue 的内部拆分结构泄露到库 API。
  4. loader 版本变化会破坏已发布产物。

Rslib 要发布的是库,不是把 loader 编译过程交给消费者重跑。

正确产物目标

正确目标是:

  • 用户源码的模块边界保留。
  • Vue SFC 内部 block 组合逻辑被编译进输出 JS。
  • CSS 被抽取到对应 CSS 文件。
  • import 路径只指向正常 JS/CSS 产物。
  • 产物不包含 rspack-vue-loader 内部 request。

例如 bundleless 下可以看到类似:

export { default } from './Button.js';

Button.js 内部已经是 Vue 运行时可执行的组件对象和 render 相关逻辑。

为什么不是所有 Vue request 都放行

判断条件有两个约束:

  • request 必须包含 rspack-vue-loader
  • request 必须包含 dist/exportHelper.js?vue&type=

这避免误伤用户正常 import。例如用户自己的文件路径里包含 vue,不会被放行。用户 import vue 包本身,也不会被这个规则处理。

这个特判范围窄,是合理的。

为什么 Svelte 没同类逻辑

Svelte 目前没有在 core 中出现类似 rspack-svelte-loader 的 request 特判。原因不是 Svelte 不需要编译,而是当前 loader 输出没有以同样方式把内部 block request 暴露到 Rslib bundleless external 的冲突点。

如果未来 Svelte loader 也生成类似:

some-svelte-loader/dist/helper.js
Button.svelte?svelte&type=...

并且这些 request 进入 bundleless external,那么才需要同类处理。

修改风险

这段逻辑绑定 rspack-vue-loader 的内部 request 形态。升级 loader 时要确认:

  • helper 路径是否仍是 dist/exportHelper.js
  • virtual block query 是否仍包含 ?vue&type=
  • 新增的内部 request 是否需要放行。
  • 旧规则是否会误伤新的用户 request。

如果改动,必须跑 Vue integration 和 E2E。