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:
2026-09-07 10:11:51 +08:00
parent efb3141734
commit 00d95c496f
6 changed files with 1029 additions and 4 deletions
+222
View File
@@ -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.01.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
}