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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user