☰
想让 tsup 打包流程“开口干活“?从 tsup 插件的四个钩子说起
2026/11/4 1:29:01 网站建设 项目流程

想让 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-targetesbuildOptions + renderChunktarget 为 es5/es3 时交给 swc 降级src/plugins/swc-target.ts
terserrenderChunkminify: 'terser'时用 terser 压缩src/plugins/terser.ts
tree-shakingrenderChunk再套一层 Rollup 做二次 tree shakingsrc/plugins/tree-shaking.ts
cjs-splittingrenderChunkesbuild 不支持 CJS 分割,手动 ESM 转 CJSsrc/plugins/cjs-splitting.ts
cjs-interoprenderChunk默认导出时补module.exports = exports.defaultsrc/plugins/cjs-interop.ts
shebangrenderChunk产物以#!开头则写0o755权限src/plugins/shebang.ts
size-reporterbuildEnd汇总打印各产物体积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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询