feat(esm/npm): 用 Go 拉依赖树,加库不再需要 node
零依赖的包下个 tarball 就能用,有依赖的得先解析依赖树——读 semver 范围、
查注册表定版本、递归。这个包把那件事用 Go 做了,于是整条链没有 node:
esm.Install(ctx, "qs", "app/node_modules")
// qs v6.16.0 打进 50 个文件 -> 73.3 KB(依赖树 19 个包 1.7 MB)
= npm.Fetch(拉依赖树到临时目录)+ Vendor(摊平成一个文件)。
刻意不做的(它不是 npm):
- 不跑安装脚本。那是供应链攻击的主要入口,而纯 JS 库没有编译步骤
- 不管 devDependencies / peerDependencies / 平台二进制
- semver 只实现 ^ / ~ / 精确 / x / >= 这个子集
- 只平铺不嵌套
碰上支持不了的(复合范围、主版本冲突)明确报错并指向 npm + Vendor,
不猜版本——猜错了装出来能跑但行为不对,比装不上难查。
子集划得这么小是有依据的:抽 8 个常见包的 44 个传递依赖统计,^ 占 95%,
~ 和精确各一两处,主版本冲突 0 个。
安全上做了两件事:校验注册表给的 sha512(中间的缓存代理、私有源镜像
是真实存在的),以及挡住 tarball 里带 ../ 的路径。
测试全部走内存假注册表,不碰网络:依赖树平铺、共同依赖只装一次、
版本冲突报错、校验和不符、目录穿越。semver 那组表驱动——写这组时抓到
一个真 bug:1.2.x 被映射成了 ^1.2.0,只锁主版本,实际该锁到 1.2。
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
package esm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.fsdpf.net/go/jscriptx/esm/npm"
|
||||
"github.com/evanw/esbuild/pkg/api"
|
||||
)
|
||||
|
||||
@@ -64,6 +66,33 @@ func Vendor(fromDir, name, outRoot string, opts ...VendorOption) (VendorResult,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Install 从注册表拉一个包(连同它的依赖),摊平后放进 outRoot。
|
||||
//
|
||||
// 等于 npm.Fetch + Vendor 两步,只是中间那棵依赖树落在临时目录里,用完就删——
|
||||
// **整条链不需要 node**。
|
||||
//
|
||||
// res, err := esm.Install(ctx, "qs", "app/node_modules")
|
||||
// // qs v6.16.0 打进 47 个文件 -> 73.6 KB
|
||||
//
|
||||
// spec 的形式是 `名字` 或 `名字@范围`,不写范围取 latest。
|
||||
//
|
||||
// 要装的包已经在本地装好了(或者碰上 npm.Fetch 支持不了的复合版本范围),
|
||||
// 就直接用 Vendor 指着那个目录。
|
||||
func Install(ctx context.Context, spec, outRoot string, opts ...VendorOption) (VendorResult, error) {
|
||||
tmp, err := os.MkdirTemp("", "jscriptx-install-")
|
||||
if err != nil {
|
||||
return VendorResult{}, fmt.Errorf("jscriptx/esm: 建临时目录失败: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
if _, err := npm.Fetch(ctx, tmp, spec); err != nil {
|
||||
return VendorResult{}, err
|
||||
}
|
||||
|
||||
name, _ := npm.SplitSpec(spec)
|
||||
return Vendor(tmp, name, outRoot, opts...)
|
||||
}
|
||||
|
||||
// VendorResult 是一次摊平的结果,用来打日志。
|
||||
type VendorResult struct {
|
||||
Name string
|
||||
|
||||
Reference in New Issue
Block a user