Files
jscriptx/esm/npm/semver_test.go
T
what 00d95c496f 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。
2026-09-07 10:11:51 +08:00

124 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}
}