package jscriptx import ( "errors" "fmt" "sort" "strconv" "strings" ) // VersionSep 分隔脚本名和版本标签。叠层时用 "脚本名@版本" 点名要哪一层的实现。 const VersionSep = "@" // OverlayLoader 把若干个 Loader 叠成一个,**后面的盖前面的**。 // 由 Overlay 创建。 type OverlayLoader struct { loaders []Loader prepared bool // 所有成员一致,构造时已校验过 byVer map[string]int // 版本标签 -> 层号,贴了标签的层才在里面 } // Versioned 由带版本标签的 Loader 实现(比如 esm.Loader 配 WithVersion)。 // 叠层时这个标签就是调用点用来点名的那个:Load("Foo/Bar@v1")。 // // 没实现这个接口、或者标签是空串的层,只能通过"上层盖下层"取到,点不了名。 type Versioned interface { // Version 返回这一层的版本标签。 Version() string } // Tag 给任意 Loader 贴一个版本标签,让它在叠层里能被点名。 // 自带标签的 Loader(比如 esm.NewLoader 配了 WithVersion)不用它。 // // e, _ := jscriptx.New(jscriptx.WithLoader( // disk, // jscriptx.Tag("hotfix", dbLoader), // 之后可以 Load("Foo/Bar@hotfix") // )) func Tag(version string, l Loader) Loader { return &taggedLoader{Loader: l, version: version} } type taggedLoader struct { Loader version string } func (t *taggedLoader) Version() string { return t.version } // Prepared 透传底下那个 Loader 的取值,别让贴标签这件事改了它的性质。 func (t *taggedLoader) Prepared() bool { return isPrepared(t.Loader) } func versionOf(l Loader) string { v, ok := l.(Versioned) if !ok { return "" } return v.Version() } // Overlay 把多个 Loader 叠成一个:取脚本时从最后一个往前找,谁先有就用谁的。 // 排在后面的因此能覆盖前面的同名脚本——把"定制层"放最后,业务侧放一份同名脚本 // 就能改写默认实现,不用动被覆盖的那一份: // // base, _ := esm.NewLoader("app/src") // custom, _ := esm.NewLoader("custom/src") // 配置可以跟 base 完全不同 // // loader, err := jscriptx.Overlay(base, custom) // custom 盖 base // e, _ := jscriptx.New(jscriptx.WithLoader(loader)) // // 每个成员是独立的 Loader,各有各的配置(入口规则、目标版本、node_modules 位置、 // 扩展模块),来源也可以不同——一层来自磁盘目录,另一层来自数据库都行。 // // 给某一层贴了版本标签(esm 的 WithVersion,或者 Tag),调用点就能点名要它: // // v1, _ := esm.NewLoader("app/v1/src", esm.WithVersion("v1")) // v2, _ := esm.NewLoader("app/v2/src", esm.WithVersion("v2")) // // e.New(ctx, "Resource/ResCreateController") // 不点名:上层盖下层,拿到 v2 // e.New(ctx, "Resource/ResCreateController@v1") // 点名:只在 v1 那层找,不回落 // // 点名是"只认这一层":那层没有这个脚本就直接报不存在,不会掉到别的层去—— // 不然点名要 v1 却跑了 v2 的实现,比报错难查得多。 // // 成员的 Prepared 必须一致:要么都是自己打好包的(比如 jscriptx/esm 的 Loader), // 要么都交出原始源码由引擎打包。混着来会返回错误,因为引擎只能对整个 Loader // 做一次判断,没法分脚本区别对待。真要混,把原始源码那层用 LoaderFunc 包一下, // 里面自己调 Bundle,它就跟其它层一样是"打好包的"了。 func Overlay(loaders ...Loader) (*OverlayLoader, error) { if len(loaders) == 0 { return nil, errors.New("jscriptx: Overlay 至少要给一个 Loader") } for i, l := range loaders { if l == nil { return nil, fmt.Errorf("jscriptx: Overlay 的第 %d 个 Loader 是 nil", i) } } prepared := isPrepared(loaders[0]) for i, l := range loaders[1:] { if isPrepared(l) != prepared { return nil, fmt.Errorf( "jscriptx: Overlay 的成员 Prepared 不一致(第 0 个是 %v,第 %d 个是 %v);"+ "要么都自己打包,要么都交出原始源码", prepared, i+1, !prepared) } } byVer := map[string]int{} for i, l := range loaders { v := versionOf(l) if v == "" { continue } if strings.Contains(v, VersionSep) { return nil, fmt.Errorf("jscriptx: 版本标签 %q 里不能有 %q", v, VersionSep) } if j, dup := byVer[v]; dup { return nil, fmt.Errorf("jscriptx: 版本标签 %q 重了(第 %d 层和第 %d 层)", v, j, i) } byVer[v] = i } return &OverlayLoader{ loaders: append([]Loader(nil), loaders...), prepared: prepared, byVer: byVer, }, nil } // Load 从最后一个成员往前找,返回第一个找到的脚本。 // // 成员报"脚本不存在"就继续往前找;报别的错直接返回——加载出故障不该被 // 后面那层的结果悄悄盖掉。 func (o *OverlayLoader) Load(name string) (string, string, error) { if bare, version, ok := splitVersion(name); ok { return o.loadFrom(bare, version) } var notFound error for i := len(o.loaders) - 1; i >= 0; i-- { source, version, err := o.loaders[i].Load(name) if err != nil { if errors.Is(err, ErrScriptNotFound) { notFound = err continue } return "", "", err } // 版本号带上是第几层给的:覆盖层的脚本删掉后会落回下面那层, // 两层的版本号万一撞上,不带层号就看不出脚本已经换了人。 if version != "" { version = strconv.Itoa(i) + ":" + version } return source, version, nil } if notFound == nil { notFound = ErrScriptNotFound } return "", "", fmt.Errorf("%w: %s(%d 层都没有)", notFound, name, len(o.loaders)) } // loadFrom 只在点名的那一层找,找不到就报不存在,不回落到别的层。 func (o *OverlayLoader) loadFrom(name, version string) (string, string, error) { i, ok := o.byVer[version] if !ok { return "", "", fmt.Errorf("%w: %s(没有版本 %q 这一层,有的是 %v)", ErrScriptNotFound, name, version, o.Versions()) } source, ver, err := o.loaders[i].Load(name) if err != nil { return "", "", err } if ver != "" { ver = strconv.Itoa(i) + ":" + ver } return source, ver, nil } // splitVersion 把 "Foo/Bar@v1" 拆成 "Foo/Bar" 和 "v1"。 // 用最后一个分隔符,脚本名里真带了 @ 也不会拆错。 func splitVersion(name string) (bare, version string, ok bool) { i := strings.LastIndex(name, VersionSep) if i <= 0 || i == len(name)-1 { return name, "", false // 没有分隔符,或者两边空着 } return name[:i], name[i+1:], true } // Versions 返回各层的版本标签,按叠放顺序,没贴标签的层跳过。 func (o *OverlayLoader) Versions() []string { var out []string for _, l := range o.loaders { if v := versionOf(l); v != "" { out = append(out, v) } } return out } // Prepared 返回成员们一致的取值,见 Overlay 的说明。 func (o *OverlayLoader) Prepared() bool { return o.prepared } // Loaders 返回成员,顺序即叠放顺序(后面的盖前面的)。 func (o *OverlayLoader) Loaders() []Loader { return append([]Loader(nil), o.loaders...) } // Names 汇总所有成员的脚本名,去重后按字典序排列。 // 成员得有 Names() []string 方法才算得上,没有的(比如数据库来源)就跳过。 func (o *OverlayLoader) Names() []string { seen := map[string]bool{} var out []string for _, l := range o.loaders { lister, ok := l.(interface{ Names() []string }) if !ok { continue } for _, n := range lister.Names() { if !seen[n] { seen[n] = true out = append(out, n) } } } sort.Strings(out) return out } // Rebuild 挨个让成员重建。成员没有 Rebuild() error 方法就跳过。 // 有成员失败时其余的照样会走一遍,返回的错误里带上所有失败。 func (o *OverlayLoader) Rebuild() error { var errs []error for i, l := range o.loaders { r, ok := l.(interface{ Rebuild() error }) if !ok { continue } if err := r.Rebuild(); err != nil { errs = append(errs, fmt.Errorf("第 %d 层: %w", i, err)) } } return errors.Join(errs...) } var _ Prepared = (*OverlayLoader)(nil)