有些包把东西放在子路径下(es-toolkit 的 toString 只在 compat 里,主入口没有),
而摊平之后原包的 exports 映射就没了,import "es-toolkit/compat" 解析不到。
现在直接写子路径就行:
esm.Install(ctx, "es-toolkit/compat", "app/node_modules")
拉的是根包,摊平的是子路径,落到 node_modules/es-toolkit/compat/。根包和子路径
可以共存——子路径目录嵌在根包目录里,而最小 package.json 不写 exports,
所以解析器认得出来。
npm.SplitPath 负责拆名字,scoped 包名自带一个斜杠所以前两段才是包名。
242 lines
6.3 KiB
Go
242 lines
6.3 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
|
||
}
|
||
|
||
// Satisfies 判断某个版本满不满足一个范围。
|
||
//
|
||
// 给「已经装过了,还用不用重装」这类判断用:配置里写 qs@^6,装着的是 6.16.0
|
||
// 就跳过,是 5.x 就得换。
|
||
//
|
||
// 范围的写法支持范围见本文件开头;解析不了会返回错误,调用方可以选择当作
|
||
// 「需要重装」还是直接报错。
|
||
func Satisfies(version, rangeSpec string) (bool, error) {
|
||
v, err := parseVersion(version)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
r, err := parseRange(rangeSpec)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return r.allows(v), nil
|
||
}
|