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:
2026-09-07 10:11:51 +08:00
parent efb3141734
commit 00d95c496f
6 changed files with 1029 additions and 4 deletions
+123
View File
@@ -0,0 +1,123 @@
package npm
import "testing"
func TestRange_挑版本(t *testing.T) {
// 一个包发布过的所有版本,挑的时候要从里面选
versions := []string{
"0.0.1", "0.0.2", "0.1.0", "0.1.9", "0.2.0",
"1.0.0", "1.2.3", "1.2.9", "1.9.0",
"2.0.0", "2.1.0",
"3.0.0-beta.1", // 预发布,任何情况下都不该被选中
}
for _, c := range []struct {
rng string
want string
why string
}{
{"^1.2.3", "1.9.0", "^ 锁主版本,取该主版本下最高的"},
{"^1", "1.9.0", "只写主版本"},
{"^2", "2.1.0", ""},
{"^0.1.0", "0.1.9", "主版本是 0 时,^ 锁到次版本"},
{"^0.0.1", "0.0.1", "主次都是 0 时,^ 锁死修订号"},
{"~1.2.3", "1.2.9", "~ 锁到次版本"},
{"~1.2", "1.2.9", ""},
{"~1", "1.9.0", "只写主版本时 ~ 等于锁主版本"},
{"1.2.3", "1.2.3", "精确"},
{"=1.2.3", "1.2.3", ""},
{"1.x", "1.9.0", "x 等价于锁住写出来的那几段"},
{"0.x", "0.2.0", "主版本 0 时也是锁主版本,不能套 ^ 的特殊规则"},
{"1.2.x", "1.2.9", ""},
{">=2.0.0", "2.1.0", "只有下界"},
{"*", "2.1.0", "任意版本取最高的——但不能是预发布"},
{"", "2.1.0", "空范围同上"},
} {
r, err := parseRange(c.rng)
if err != nil {
t.Errorf("parseRange(%q) 报错: %v", c.rng, err)
continue
}
got, err := pick(versions, r)
if err != nil {
t.Errorf("%q 挑不出版本: %v%s", c.rng, err, c.why)
continue
}
if got != c.want {
t.Errorf("%q → %s,该是 %s%s", c.rng, got, c.want, c.why)
}
}
}
// 预发布版本一律不选,跟 npm 的默认行为一致。
func TestRange_不选预发布(t *testing.T) {
r, _ := parseRange("^3")
if got, err := pick([]string{"3.0.0-beta.1", "3.0.0-rc.2"}, r); err == nil {
t.Errorf("只有预发布版本时该挑不出来,却挑了 %s", got)
}
// 但正式版在的话要选得到
r2, _ := parseRange("^3")
if got, _ := pick([]string{"3.0.0-beta.1", "3.0.0"}, r2); got != "3.0.0" {
t.Errorf("拿到 %s,该是 3.0.0", got)
}
}
// 支持不了的写法要**明确报错**,不能猜一个版本——猜错了装出来能跑但行为不对。
func TestRange_不支持的写法要报错(t *testing.T) {
for _, s := range []string{
"^1 || ^2", // 或
">=1.0 <2.0", // 复合
"<2.0.0", // 只有上界
"npm:other@^1",
"github:user/repo",
"file:../local",
} {
if _, err := parseRange(s); err == nil {
t.Errorf("%q 该报错,却解析成功了", s)
}
}
}
func TestVersion_解析(t *testing.T) {
for _, c := range []struct {
in string
maj, min, pat int
pre string
}{
{"1.2.3", 1, 2, 3, ""},
{"0.0.1", 0, 0, 1, ""},
{"10.20.30", 10, 20, 30, ""},
{"1.2.3-beta.1", 1, 2, 3, "beta.1"},
{"1.2.3+build.5", 1, 2, 3, ""}, // 构建元数据不参与比较,丢掉
} {
v, err := parseVersion(c.in)
if err != nil {
t.Errorf("parseVersion(%q): %v", c.in, err)
continue
}
if v.major != c.maj || v.minor != c.min || v.patch != c.pat || v.pre != c.pre {
t.Errorf("parseVersion(%q) = %d.%d.%d-%q", c.in, v.major, v.minor, v.patch, v.pre)
}
}
for _, bad := range []string{"1.2", "1", "a.b.c", "", "1.2.x"} {
if _, err := parseVersion(bad); err == nil {
t.Errorf("parseVersion(%q) 该报错", bad)
}
}
}
func TestSplitSpec(t *testing.T) {
for _, c := range []struct{ spec, name, rng string }{
{"qs", "qs", ""},
{"qs@6.16.0", "qs", "6.16.0"},
{"qs@^6", "qs", "^6"},
{"@scope/pkg", "@scope/pkg", ""},
{"@scope/pkg@~1.2", "@scope/pkg", "~1.2"},
} {
name, rng := SplitSpec(c.spec)
if name != c.name || rng != c.rng {
t.Errorf("SplitSpec(%q) = %q, %q,该是 %q, %q", c.spec, name, rng, c.name, c.rng)
}
}
}