返回博客

@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 就不会执行。

坑:裸包副作用导入没有类型声明 → TS2882

@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 确认。
正解:/index.css 子路径借用 vite/client 的通配类型

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。