想让 tsup 打包流程"开口干活"?从 tsup 插件的四个钩子说起
【免费下载链接】tsupThe simplest and fastest way to bundle your TypeScript libraries.项目地址: https://gitcode.com/gh_mirrors/ts/tsup
tsup 是基于 esbuild 的 TypeScript 打包工具,零配置就能同时产出 ESM/CJS。它的核心矛盾在于:内置钩子覆盖不到的需求——产物占位符替换、按格式定制压缩、写盘后统计——总得有一处官方预留的插口。tsup 插件正是这套插口,理解它比记住 API 更重要。
tsup 插件钩子触发时机
先看 tsup 层插件(注意区别于透传给 esbuild 的esbuildPlugins)。Plugin 接口只有一个必填字段name,其余四个方法全部可选:buildStart、esbuildOptions、renderChunk、buildEnd,类型定义在src/plugin.ts。
PluginContainer负责按序驱动它们,完整链路是:buildStart→(进入 esbuild)setup回调里触发esbuildOptions→ esbuild 完成 resolve、transform、bundle → 拿到内存产物后,对每个 chunk 循环执行renderChunk→ 写盘 → 全部写完再跑buildEnd,此时能拿到writtenFiles列表。
图中的"bundle 还是 external"判定发生在 esbuild 的 resolve 阶段,由内置externalPlugin完成,不属于Plugin接口暴露的能力。也就是说:模块级拦截(谁进包、谁外置)走noExternal/external配置或 esbuild 层插件;产物级改写(代码长什么样)才是tsup 插件的主场。
四个钩子按生命周期分组
- buildStart:esbuild 启动前,此时产物还不存在。典型场景是前置校验(缺文件、缺依赖提前报错)和构建期埋点,属于"开跑之前"。
- esbuildOptions:esbuild 初始化阶段,能直接改
options,比如加define、调target。官方swc-target就靠它把es5/es3临时改写成es2020让 esbuild 先跑通。 - renderChunk:每个 chunk 生成后、写盘前,逐 chunk 串行触发。拿得到当前
code、path、map,返回新code即可替换该 chunk,还能设置info.mode改文件权限。产物干预基本都发生在这一层:terser 压缩、shebang 权限位、CJS 互操作尾行,全在此完成。 - buildEnd:全部文件写盘后触发,只关心"写出去了什么"。典型用途是体积报告、把
writtenFiles上报到 CI。
tsup 官方插件速览
内置插件都收在 src/plugins/,按主钩子归位:
| 插件 | 主钩子 | 用途 | 源文件 |
|---|---|---|---|
| swc-target | esbuildOptions + renderChunk | target 为 es5/es3 时交给 swc 降级 | src/plugins/swc-target.ts |
| terser | renderChunk | minify: 'terser'时用 terser 压缩 | src/plugins/terser.ts |
| tree-shaking | renderChunk | 再套一层 Rollup 做二次 tree shaking | src/plugins/tree-shaking.ts |
| cjs-splitting | renderChunk | esbuild 不支持 CJS 分割,手动 ESM 转 CJS | src/plugins/cjs-splitting.ts |
| cjs-interop | renderChunk | 默认导出时补module.exports = exports.default | src/plugins/cjs-interop.ts |
| shebang | renderChunk | 产物以#!开头则写0o755权限 | src/plugins/shebang.ts |
| size-reporter | buildEnd | 汇总打印各产物体积 | src/plugins/size-reporter.ts |
一眼可见:绝大多数官方插件都压在renderChunk上,说明它是扩展收益最高的钩子。
在 tsup.config.ts 挂载 tsup 自定义插件
一个完整小场景:产物里的@APP_VERSION@占位符,希望写盘前被替换成真实版本号。
步骤是:新建插件文件实现钩子 → 在tsup.config.ts的plugins数组挂载 → 验证输出。
// plugins/replace.ts import type { Plugin } from 'tsup' const replacePlugin = (): Plugin => ({ name: 'replace-placeholder', renderChunk(code, info) { if (!info.path.endsWith('.js')) return return { code: code.replace(/@APP_VERSION@/g, '1.0.0') } }, }) export default replacePlugin注意renderChunk只对 JS/CSS chunk 触发(src/plugin.ts里isJS || isCSS的过滤逻辑),所以按扩展名判断是常规操作;需要改写 sourcemap 时再返回map,否则沿用原 map。
// tsup.config.ts import { defineConfig } from 'tsup' import replacePlugin from './plugins/replace' export default defineConfig({ entry: 'src/index.ts', plugins: [replacePlugin()], })跑一次npx tsup后grep APP_VERSION dist/index.js,应无输出;grep 1.0.0 dist/index.js命中占位符位置,说明替换发生在写盘前。
下一步
- 顺着钩子调用链读一遍
src/plugin.ts的PluginContainer,比任何 API 表都快。 - 挑一个官方实现精读:
src/plugins/cjs-interop.ts只有 26 行,是最干净的renderChunk范例。 - 涉及模块级拦截时再翻 docs/README.md 里 "Custom esbuild plugin and options" 一节,区分
esbuildPlugins与plugins两条路线。
【免费下载链接】tsupThe simplest and fastest way to bundle your TypeScript libraries.项目地址: https://gitcode.com/gh_mirrors/ts/tsup
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考