Turborepo + Rollup 实战:用 Rollup 打包共享 UI 库并集成到 Next.js Monorepo
【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo
导读
本指南围绕 Turborepo 官方示例 examples/with-rollup 展开,讲解如何在一个 Turborepo monorepo 中,用 Rollup 将独立的 React 组件库(@repo/ui)编译为可直接消费的产物,并让 Next.js 应用(web)跨包引用它。读完本文,你将掌握从create-turbo一键初始化、工作区划分、Rollup 多入口打包配置、TS/ESLint 配置包复用,到 Turbo 任务编排(build/dev/lint)与本地/远程缓存的完整落地路径。
该示例由社区维护(见 meta.json),目录结构小但链路完整,非常适合作为"纯库用 Rollup 打包、应用层用框架构建"这一混合构建模式的参考蓝本。
示例仓库总览
Turborepo 官方为常见构建工具提供了大量-e示例(如with-nextjs、with-vite、with-svelte等),with-rollup是其中之一。通过 meta.json 可以确认其定位:"Monorepo with a single Next.js app sharing a UI library bundled with Rollup"(一个 Next.js 应用 + 一个用 Rollup 打包的共享 UI 库)。
使用 create-turbo 一键初始化
原文档给出的创建方式是最直接、最不易出错的路径:
npx create-turbo@latest -e with-rollup命令执行后会在当前目录生成一个名为my-turborepo的项目(也可通过附加参数自定义名称)。随后即可进入项目:
cd my-turborepo pnpm run build目录与包结构
以当前仓库的 examples/with-rollup 为基准,初始化后的结构如下:
with-rollup/ ├── apps/ │ └── web/ # Next.js 应用 │ ├── pages/index.tsx # 页面入口,直接消费 @repo/ui │ ├── next.config.js │ ├── package.json │ └── tsconfig.json ├── packages/ │ ├── config-eslint/ # @repo/eslint-config:ESLint flat config 集合 │ ├── config-typescript/ # @repo/typescript-config:共享 tsconfig 集合 │ └── ui/ # @repo/ui:Rollup 打包的 React 组件库 │ ├── Button.tsx │ ├── Header.tsx │ ├── rollup.config.js │ └── package.json ├── pnpm-workspace.yaml # 工作区声明:apps/* 与 packages/* ├── turbo.json # Turbo 任务编排 └── package.json # 根脚本:build / dev / lint / format四个核心组成部分:
web:一个 Next.js 应用,通过@repo/ui/button、@repo/ui/header两个子路径导入 UI 组件;@repo/eslint-config:ESLint flat 配置包(包含@next/eslint-plugin-next与eslint-config-prettier),对外提供base、next-js、react-internal三份配置;@repo/typescript-config:共享的tsconfig.json集合(base.json、nextjs.json、react-library.json),供各包通过 extends 继承;@repo/ui:一个纯 React 组件库,编译工具是 Rollup,产物输出到dist/,是本文的主角。
每个包/应用都是 100% TypeScript,并统一配好了 TypeScript(静态类型检查)、ESLint(代码检查)、Prettier(代码格式化)三件套。
工作区与依赖声明
工作区由 pnpm-workspace.yaml 声明:
packages: - "apps/*" - "packages/*" onlyBuiltDependencies: - "@swc/core"apps/*与packages/*两个通配符让 pnpm 把子目录识别为 workspace 成员;onlyBuiltDependencies白名单允许@swc/core执行安装后的原生构建脚本(SWC 的 native 二进制需要 postinstall 编译/下载),这是 Rollup 链路能正常工作的前提之一。
根目录 package.json 统一了脚本入口与包管理器版本约束:
{ "scripts": { "build": "turbo run build", "dev": "turbo run dev", "lint": "turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, "packageManager": "pnpm@11.21.0", "engines": { "node": ">=24.19.0" } }跨包依赖通过 workspace 协议声明:web依赖"@repo/ui": "workspace:*",@repo/ui依赖"@repo/eslint-config": "workspace:*"与"@repo/typescript-config": "workspace:*"(见 apps/web/package.json 与 packages/ui/package.json)。
Rollup 打包共享 UI 库
多入口配置
@repo/ui的核心是 packages/ui/rollup.config.js。它没有采用单入口打包一个 bundle 的常见做法,而是每个组件一个入口,产出独立文件:
import swc from "@rollup/plugin-swc"; export default [ { input: "Button.tsx", output: { file: "dist/button.js" }, }, { input: "Header.tsx", output: { file: "dist/header.js" }, }, ].map((entry) => ({ ...entry, external: ["react/jsx-runtime"], plugins: [ swc({ swc: { jsc: { transform: { react: { runtime: "automatic" }, }, }, }, }), ], }));要点拆解:
- 多入口数组配置:Rollup 支持导出配置数组,每个元素是一个独立 build。这里
Button.tsx→dist/button.js、Header.tsx→dist/header.js,避免消费方把整个组件库一起打进包; external: ["react/jsx-runtime"]:React 19 自动 JSX runtime 的产物不应打进库文件,而是作为 peer 由应用侧提供,防止 React 被重复打包;@rollup/plugin-swc+runtime: "automatic":用 SWC 完成 TSX → JS 的转译(速度远快于 Babel),automatic运行时让 JSX 自动编译为jsx-runtime调用——这正是external需要声明react/jsx-runtime的原因,两条配置是配套的;- 编译只负责转换,不做 tree-shaking 之外的额外优化,产物是给消费方的最小可运行模块。
package.json 的 exports 映射
组件库通过 packages/ui/package.json 的exports字段对外暴露精确的入口,并做到"类型优先":
{ "name": "@repo/ui", "type": "module", "exports": { "./button": { "types": "./Button.tsx", "default": "./dist/button.js" }, "./header": { "types": "./Header.tsx", "default": "./dist/header.js" } }, "scripts": { "lint": "eslint . --max-warnings 0", "build": "rollup --config", "dev": "pnpm build --watch" } }"type": "module"配合"default": "./dist/button.js",让 Rollup 输出的 ESM 产物可以被import直接消费;"types"直接指向源文件./Button.tsx/./Header.tsx(而不是生成.d.ts),因为源码本身就是 TypeScript,无需额外声明文件生成步骤,这也保证了类型定义与实现永远同步;exports只暴露./button与./header两个子路径,外部无法通过@repo/ui根路径或任意文件路径闯入,包边界被严格锁定。
组件源码与消费方式
两个组件都非常简洁(Button.tsx、Header.tsx):
// Button.tsx export const Button = () => { return <button>Boop</button>; }; // Header.tsx export const Header = ({ text }: { text: string }) => { return <h1>{text}</h1>; };应用侧在 apps/web/pages/index.tsx 中按子路径导入:
import { Button } from "@repo/ui/button"; import { Header } from "@repo/ui/header"; export default function Page() { return ( <> <Header text="Web" /> <Button /> </> ); }从源码结构看,这条链路是:rollup --config把 TSX 编译到dist/→ pnpm workspace 把@repo/ui软链进web的 node_modules → Next.js 通过exports解析到dist/button.js并消费。中间没有任何手工复制产物的步骤,全靠包解析完成。
Turbo 任务编排:build / dev / lint
原文档给出了两个最常用的根级命令:
pnpm run build # 构建所有 apps 与 packages pnpm run dev # 开发所有 apps 与 packages它们都只是turbo run <task>的薄封装。真正的行为定义在根目录 turbo.json:
{ "$schema": "https://turborepo.org/schema.json", "tasks": { "build": { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**", ".next/**", "!.next/cache/**", "!.next/dev/**"] }, "lint": { "outputs": [] }, "dev": { "cache": false, "persistent": true } } }逐项解读:
build.dependsOn: ["^build"]:拓扑依赖。web的 build 会等待其依赖包(@repo/ui)的 build 完成后再执行,这正是"先打包 UI 库、再构建 Next.js"的保证;同时各包之间没有依赖关系时可以被 Turbo 并行执行;build.inputs:哈希参与项。$TURBO_DEFAULT$代表各包自身的文件内容,.env*让环境文件变化也触发缓存失效;build.outputs:声明可缓存的产物目录。dist/**覆盖 Rollup 的输出,.next/**覆盖 Next.js 构建输出,并显式排除.next/cache与.next/dev(这些属于过程产物,不应进入缓存);lint.outputs: []:lint 不产出文件,因此不缓存产物;dev.cache: false+persistent: true:dev 是常驻进程,不参与缓存,并标记为 persistent 让 Turbo 正确管理其生命周期。注意,ui包的 dev 脚本是pnpm build --watch(Rollup watch 模式),web的 dev 是next dev,两者都会被turbo run dev拉起——persistent: true同时要求这类长驻任务必须被单独运行,不能作为其他任务的依赖。
在各包内部,build/dev/lint的实现分别是:
@repo/ui:build: rollup --config,dev: pnpm build --watch,lint: eslint . --max-warnings 0(见 packages/ui/package.json);web:build: next build,dev: next dev,start: next start,lint: eslint . --max-warnings 0(见 apps/web/package.json),并在 next.config.js 中开启了reactStrictMode: true。
共享 ESLint 与 TypeScript 配置
与with-rollup其他示例一致,这里也预设了两套共享配置包,保证 monorepo 内 lint 与类型约束统一。
@repo/eslint-config(packages/config-eslint)提供 flat config 风格的三份配置:
base.js:基础规则,供所有包使用;next-js.js:Next.js 专用(包含@next/eslint-plugin-next与eslint-config-prettier),供web使用;react-internal.js:React 库内部规则,供@repo/ui使用。
各包的 eslint.config.mjs(及 packages/ui/eslint.config.mjs)通过 flat config 机制 import 对应配置,配合--max-warnings 0把任何 warning 都视为失败,保证 lint 质量。
@repo/typescript-config(packages/config-typescript)提供:
base.json:基础编译器选项;nextjs.json:Next.js 应用专用(供web);react-library.json:React 库专用(供@repo/ui)。
各包 tsconfig 通过extends继承对应配置,例如ui使用react-library.json,web使用nextjs.json,实现"一处定义、处处复用"。
本地缓存与远程缓存
原文档强调:默认情况下 Turborepo 会进行本地缓存——同一台机器上,只要任务的输入哈希未变(依赖文件、环境、上游产物等),再次执行就会直接命中缓存并跳过重算。这与上文build.outputs的声明直接相关:只有声明了outputs的任务才值得缓存,lint这类无产物任务则天然跳过。
在此基础上,Turborepo 还支持Remote Caching(远程缓存),把缓存产物共享到云端,让团队与 CI/CD 管道复用彼此已经构建过的结果,从而显著缩短 CI 时长。启用流程如下:
cd my-turborepo npx turbo login # 使用 Vercel 账号认证 Turborepo CLI npx turbo link # 将项目与远程缓存关联turbo login完成 CLI 与账号的认证;turbo link完成项目与远程缓存的绑定,之后每个可缓存任务的上传/下载都会走远程缓存。
原文档的说明以 Vercel 为默认实现:Vercel Remote Cache 对所有套餐免费,无需账号时可先注册再执行上述命令。需要留意的是,远程缓存行为属于官方提供的配套服务,若团队没有使用 Vercel,也可以自行搭建符合 Turborepo 协议的自托管缓存端点,但本示例默认链路仍是"本地缓存 + Vercel Remote Cache"。
进一步学习
原文档推荐继续深入以下 Turborepo 核心概念(对应官方文档):
- Pipelines(任务管道):
dependsOn、outputs、inputs等任务字段如何构成执行图与缓存键; - Caching(缓存):缓存命中判定、哈希输入与产物恢复机制;
- Remote Caching(远程缓存):跨机器共享缓存的人工制品。
在本文档仓库中,也可以直接对照源码继续探索:完整的示例结构见 examples/with-rollup,根任务编排见 turbo.json,Rollup 打包细节见 packages/ui/rollup.config.js,组件导出映射见 packages/ui/package.json,应用消费端见 apps/web/pages/index.tsx。
【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考