@fontsource 裸包 side-effect import 触发 TS2882,用 /index.css 子路径修复
做桌面端应用时,为了让字体完全离线可用,通常会把 Google Fonts 的 CDN @import 替换成本地打包的 @fontsource 系列包。在 Vite + TypeScript 项目里写下那行看起来无害的 import '@fontsource-variable/some-font',结果构建直接在类型检查阶段炸了——Vite 还没来得及跑,tsc 就先拦住了。
现象
在 src/main.tsx 顶部加上字体副作用导入后,执行 pnpm build(内部跑 tsc -b && vite build),终端输出:
src/main.tsx(4,8): error TS2882: Cannot find module or type declarations for
side-effect import of '@fontsource-variable/bricolage-grotesque'.
构建挂在类型检查,woff2 文件从未进入 dist/。
根因
@fontsource / @fontsource-variable 包的 main entry(package.json 的 "main" 或 "exports"."." 字段)指向一个 CSS 文件,但包里没有 .d.ts 类型声明。TypeScript 在严格模式下对裸包副作用导入(import 'pkg')要求必须能解析到类型声明,找不到就报 TS2882。
这不是 Vite 的问题——Vite 在运行时完全能处理这个裸包,CSS 和 woff2 都会被正确处理。错误完全发生在 TypeScript 的类型解析阶段,两者互不干扰,但构建脚本里 tsc 跑在前面,一旦失败后面的 vite 就不会执行。
@fontsource 系列包的主入口是 CSS,但不附带任何 .d.ts。TypeScript 无法为副作用导入推断出"这个模块存在",于是报 TS2882 中止编译。Vite 能处理但 tsc 不行——这是纯粹的类型解析限制,不是打包问题。
正解
不要用裸包说明符,改成显式的 /index.css 子路径:
// ❌ 裸包说明符 → TS2882
import '@fontsource-variable/bricolage-grotesque'
import '@fontsource-variable/jetbrains-mono'
// ✅ 显式 CSS 子路径 → vite/client 的 *.css 环境类型覆盖
import '@fontsource-variable/bricolage-grotesque/index.css'
import '@fontsource-variable/jetbrains-mono/index.css'vite/client 提供了 declare module '*.css' 环境声明,任何以 .css 结尾的 import 路径都能通过类型检查,子路径写法正好命中这条规则。
前置条件:tsconfig.json 的 "types" 数组里必须包含 "vite/client"(通常 Vite 脚手架项目默认已有):
{
"compilerOptions": {
"types": ["vite/client"]
}
}另外两个细节:
- 字体 import 放在
index.css前面,确保@font-face先注册。 - Variable 字体包注册的字族名带
Variable后缀,例如'Bricolage Grotesque Variable',CSS 里使用时要对齐。可以用grep font-family node_modules/@fontsource-variable/<name>/index.css确认。
将 import '@fontsource-variable/xxx' 改为 import '@fontsource-variable/xxx/index.css',TypeScript 通过 vite/client 的 *.css 环境声明完成类型检查,woff2 被正常打包,无需任何 declare module 补丁。
验证
pnpm build # tsc + vite 均通过
ls dist/assets/*.woff2 # woff2 文件存在
grep -rl "googleapis\|gstatic" dist/ # 无输出 → 完全离线一句话外卖
@fontsource裸包 import 是类型解析陷阱,不是 Vite 问题——加上/index.css后缀,借用vite/client的通配声明即可,无需任何 shim。