feat(esm): Vendor 把装好的 npm 包连同依赖摊平成单文件

解决的是「脚本要用第三方库,但目标机器上没有 node」。

npm 真正干的活是解析依赖树——读 semver 范围、查注册表定版本、递归、处理冲突。
这步绕不开,得在有 node 的机器上做一次。但做完之后依赖树就是死数据了,用
esbuild 摊平成一个文件,发布物里只带那一个就够:

    qs         v6.16.0   打进  47 个文件 -> 73.6 KB   (原 19 个包 1.7 MB)
    es-toolkit v1.52.0   打进 219 个文件 -> 51.9 KB

产物是最小的 node_modules 布局,脚本照常 import,写法完全不变。

两个实现细节:

  - 入口不能直接写包名,esbuild 的 EntryPoints 是文件路径。所以造一段转发
    源码当 stdin 入口,包名放进 import,才走正常的 node_modules 解析。
  - 转发源码里写 export { default } 时,只有具名导出的包会报错(ESM 原生的
    很多是这样),退回去用只带具名导出的版本重打一次。

打包目标从 Loader 里提成了共用常量:摊平出来的库必须跟脚本同一档,
否则库能打出脚本引擎跑不了的语法。

零依赖的包不用这个——直接下 tarball 解开就行,README 里记了命令。
This commit is contained in:
2026-09-07 09:54:18 +08:00
parent c2ce37ad2d
commit efb3141734
4 changed files with 420 additions and 1 deletions
+6 -1
View File
@@ -11,6 +11,11 @@ import (
// outDir 只是给 esbuild 算相对路径用的虚拟目录,不落盘。
const outDir = "__jsx_out"
// defaultTarget 是打包输出的默认 ECMAScript 版本。goja 对更新的语法覆盖不全,
// 所以不跟着 esnext 走。Loader 和 Vendor 共用它——摊平出来的库要跟脚本同一档,
// 否则库能打出脚本引擎跑不了的语法。
const defaultTarget = api.ES2017
// DefaultGlobs 是默认的入口规则:根目录第一层子目录下的 js/ts 文件。
// 对应 app/PkgVersion/PkgImportController.ts 这样的结构。
var DefaultGlobs = []string{"*/*.js", "*/*.ts", "*/*.mjs"}
@@ -121,7 +126,7 @@ func NewLoader(dir string, opts ...Option) (*Loader, error) {
l := &Loader{
dir: dir,
globs: DefaultGlobs,
target: api.ES2017,
target: defaultTarget,
}
for _, opt := range opts {
opt(l)