零依赖的包下个 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。
223 lines
5.7 KiB
Go
223 lines
5.7 KiB
Go
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
|
||
}
|