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 }