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,222 @@
|
||||
package npm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 这里只实现 npm semver 的一个**子集**。
|
||||
//
|
||||
// 抽了 8 个常见包的 44 个传递依赖统计,范围写法的分布是:
|
||||
//
|
||||
// ^ 95%
|
||||
// ~ 1 处
|
||||
// 精确 2 处
|
||||
//
|
||||
// 所以支持 ^ / ~ / 精确 / * / x / >= 就覆盖了实际会碰到的绝大多数。剩下的
|
||||
// (`||` 或者 `>=1 <2` 这种复合范围)**明确报错**,而不是猜一个版本——猜错了
|
||||
// 装出来的东西能跑但行为不对,比装不上难查得多。碰到了就用 npm 装,再走 Vendor。
|
||||
//
|
||||
// 另外一律**跳过预发布版本**(1.0.0-beta.1 这种),跟 npm 的默认行为一致。
|
||||
|
||||
type version struct {
|
||||
major, minor, patch int
|
||||
pre string // 预发布标识,非空表示这是个预发布版本
|
||||
}
|
||||
|
||||
func parseVersion(s string) (version, error) {
|
||||
var v version
|
||||
|
||||
// 先摘掉构建元数据(+sha),它不参与比较
|
||||
if i := strings.IndexByte(s, '+'); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
if i := strings.IndexByte(s, '-'); i >= 0 {
|
||||
v.pre, s = s[i+1:], s[:i]
|
||||
}
|
||||
|
||||
parts := strings.Split(s, ".")
|
||||
if len(parts) != 3 {
|
||||
return v, fmt.Errorf("版本号 %q 不是 x.y.z 的形式", s)
|
||||
}
|
||||
for i, p := range parts {
|
||||
n, err := strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("版本号 %q 里的 %q 不是数字", s, p)
|
||||
}
|
||||
switch i {
|
||||
case 0:
|
||||
v.major = n
|
||||
case 1:
|
||||
v.minor = n
|
||||
case 2:
|
||||
v.patch = n
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (v version) String() string {
|
||||
s := fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch)
|
||||
if v.pre != "" {
|
||||
s += "-" + v.pre
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// compare 只比主次修订三段。预发布版本我们根本不选,所以不用实现它的排序规则。
|
||||
func (v version) compare(o version) int {
|
||||
for _, p := range [][2]int{{v.major, o.major}, {v.minor, o.minor}, {v.patch, o.patch}} {
|
||||
if p[0] != p[1] {
|
||||
if p[0] < p[1] {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type rangeKind int
|
||||
|
||||
const (
|
||||
rangeAny rangeKind = iota // *、x、空
|
||||
rangeExact // 1.2.3
|
||||
rangeCaret // ^1.2.3
|
||||
rangeTilde // ~1.2.3
|
||||
rangeAtLeast // >=1.2.3
|
||||
)
|
||||
|
||||
// versionRange 是解析好的依赖范围。
|
||||
type versionRange struct {
|
||||
kind rangeKind
|
||||
base version
|
||||
// 写了几段。~1 和 ~1.2 的上界不一样,得知道原文写到哪一级
|
||||
parts int
|
||||
}
|
||||
|
||||
func parseRange(s string) (versionRange, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
if s == "" || s == "*" || s == "x" || s == "latest" {
|
||||
return versionRange{kind: rangeAny}, nil
|
||||
}
|
||||
// 复合范围一律拒绝:与其猜,不如让人换条路
|
||||
if strings.ContainsAny(s, "|") || strings.Contains(s, " ") {
|
||||
return versionRange{}, fmt.Errorf("暂不支持复合范围 %q——"+
|
||||
"用 npm 装好之后走 esm.Vendor", s)
|
||||
}
|
||||
if strings.HasPrefix(s, "npm:") || strings.Contains(s, "/") || strings.Contains(s, ":") {
|
||||
return versionRange{}, fmt.Errorf("暂不支持别名或 git/文件依赖 %q", s)
|
||||
}
|
||||
|
||||
kind := rangeExact
|
||||
switch {
|
||||
case strings.HasPrefix(s, "^"):
|
||||
kind, s = rangeCaret, s[1:]
|
||||
case strings.HasPrefix(s, "~"):
|
||||
kind, s = rangeTilde, s[1:]
|
||||
case strings.HasPrefix(s, ">="):
|
||||
kind, s = rangeAtLeast, s[2:]
|
||||
case strings.HasPrefix(s, "="):
|
||||
s = s[1:]
|
||||
case strings.HasPrefix(s, "<"), strings.HasPrefix(s, ">"):
|
||||
return versionRange{}, fmt.Errorf("暂不支持范围 %q", s)
|
||||
}
|
||||
|
||||
// 补齐省略的段:1 → 1.0.0,1.2 → 1.2.0
|
||||
segs := strings.Split(s, ".")
|
||||
parts := len(segs)
|
||||
for i, seg := range segs {
|
||||
if seg == "x" || seg == "X" || seg == "*" {
|
||||
segs = segs[:i]
|
||||
parts = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if parts == 0 {
|
||||
return versionRange{kind: rangeAny}, nil
|
||||
}
|
||||
for len(segs) < 3 {
|
||||
segs = append(segs, "0")
|
||||
}
|
||||
|
||||
base, err := parseVersion(strings.Join(segs, "."))
|
||||
if err != nil {
|
||||
return versionRange{}, err
|
||||
}
|
||||
// 段数不全的写法(1、1.2、1.x、1.2.x)意思是"这几段固定,剩下的随便",
|
||||
// 跟 ~ 的语义一致:~1 锁主版本,~1.2 锁到次版本。
|
||||
//
|
||||
// 不能映射成 ^:^ 会对主版本为 0 的情况特殊处理(^0.2.3 锁次版本、
|
||||
// ^0.0.3 锁修订号),而 0.x 的本意是"任意 0.y.z",两者对不上。
|
||||
if parts < 3 && kind == rangeExact {
|
||||
kind = rangeTilde
|
||||
}
|
||||
return versionRange{kind: kind, base: base, parts: parts}, nil
|
||||
}
|
||||
|
||||
// allows 判断某个版本满不满足这个范围。预发布版本一律不选。
|
||||
func (r versionRange) allows(v version) bool {
|
||||
if v.pre != "" {
|
||||
return false
|
||||
}
|
||||
switch r.kind {
|
||||
case rangeAny:
|
||||
return true
|
||||
case rangeExact:
|
||||
return v.compare(r.base) == 0
|
||||
case rangeAtLeast:
|
||||
return v.compare(r.base) >= 0
|
||||
case rangeCaret:
|
||||
// ^ 锁住**最左边的非零段**:
|
||||
// ^1.2.3 → >=1.2.3 <2.0.0
|
||||
// ^0.2.3 → >=0.2.3 <0.3.0
|
||||
// ^0.0.3 → >=0.0.3 <0.0.4
|
||||
if v.compare(r.base) < 0 {
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case r.base.major > 0:
|
||||
return v.major == r.base.major
|
||||
case r.base.minor > 0:
|
||||
return v.major == 0 && v.minor == r.base.minor
|
||||
default:
|
||||
return v.major == 0 && v.minor == 0 && v.patch == r.base.patch
|
||||
}
|
||||
case rangeTilde:
|
||||
// ~1.2.3 / ~1.2 → 锁 1.2;~1 → 锁 1
|
||||
if v.compare(r.base) < 0 {
|
||||
return false
|
||||
}
|
||||
if r.parts == 1 {
|
||||
return v.major == r.base.major
|
||||
}
|
||||
return v.major == r.base.major && v.minor == r.base.minor
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pick 从一堆版本里挑满足范围的最高的那个。
|
||||
func pick(versions []string, r versionRange) (string, error) {
|
||||
var best version
|
||||
var bestRaw string
|
||||
|
||||
for _, raw := range versions {
|
||||
v, err := parseVersion(raw)
|
||||
if err != nil {
|
||||
continue // 注册表里偶尔有畸形版本号,跳过就是
|
||||
}
|
||||
if !r.allows(v) {
|
||||
continue
|
||||
}
|
||||
if bestRaw == "" || v.compare(best) > 0 {
|
||||
best, bestRaw = v, raw
|
||||
}
|
||||
}
|
||||
if bestRaw == "" {
|
||||
return "", fmt.Errorf("没有版本满足要求")
|
||||
}
|
||||
return bestRaw, nil
|
||||
}
|
||||
Reference in New Issue
Block a user