Files
what ad722b12f9 fix(esm): Vendor 支持子路径,不然 es-toolkit/compat 这种装不了
有些包把东西放在子路径下(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 包名自带一个斜杠所以前两段才是包名。
2026-09-07 10:58:34 +08:00

242 lines
6.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
// 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
}